初始版本:事件回调式 WebSocket 客户端与服务端(RFC 6455)
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* simcu::websocket.server —— 服务端单条 WebSocket 连接。
|
||||
*
|
||||
* 由 WebSocketServer 在握手成功后创建并暴露给 on('connection') 回调。
|
||||
* 提供 send/sendText/sendBinary/ping/close/terminate,以及连接级事件:
|
||||
* conn.on("message", { (msg) => ... })
|
||||
* conn.on("close", { (code, reason) => ... })
|
||||
* conn.on("error", { (e) => ... })
|
||||
*/
|
||||
package simcu::websocket.server
|
||||
|
||||
import std.net.IPSocketAddress
|
||||
import std.net.TcpSocket
|
||||
import std.sync.Mutex
|
||||
import std.time.*
|
||||
|
||||
import simcu::websocket.common.FrameStream
|
||||
import simcu::websocket.common.MessageType
|
||||
import simcu::websocket.common.ReadyState
|
||||
import simcu::websocket.common.WebSocketMessage
|
||||
import simcu::websocket.common.WebSocketException
|
||||
import simcu::websocket.common.WsEvents
|
||||
|
||||
/// 服务端单条 WebSocket 连接(事件回调式)。
|
||||
public class WebSocketConnection {
|
||||
private let parent: ?WebSocketServer
|
||||
private let frame: FrameStream
|
||||
private let stateLock = Mutex()
|
||||
private var state: ReadyState = ReadyState.Open
|
||||
|
||||
/// 连接唯一编号(由服务端分配,用于连接集合的引用比较)。
|
||||
internal let connId: Int64
|
||||
|
||||
/// 连接级事件:收到消息(不含父级 connection 参数)。
|
||||
public var onMessage: (WebSocketMessage) -> Unit = { m => () }
|
||||
/// 连接级事件:连接关闭,参数 (关闭码, 关闭原因)。
|
||||
public var onClose: (Int64, String) -> Unit = { c, r => () }
|
||||
/// 连接级事件:运行时错误。
|
||||
public var onError: (Exception) -> Unit = { e => () }
|
||||
|
||||
internal init(parent: ?WebSocketServer, frame: FrameStream, connId: Int64) {
|
||||
this.parent = parent
|
||||
this.frame = frame
|
||||
this.connId = connId
|
||||
}
|
||||
|
||||
/// 当前连接状态。
|
||||
public prop readyState: ReadyState {
|
||||
get() { state }
|
||||
}
|
||||
|
||||
/// 连接是否处于 Open。
|
||||
public func isOpen(): Bool {
|
||||
state == ReadyState.Open
|
||||
}
|
||||
|
||||
/// 远端地址(ip:port 或套接字地址字符串)。
|
||||
public prop remoteAddress: String {
|
||||
get() {
|
||||
let sa = frame.remoteAddress()
|
||||
if (let Some(ip) <- (sa as IPSocketAddress)) {
|
||||
"${ip.address}:${ip.port}"
|
||||
} else {
|
||||
sa.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 注册连接级事件。支持 "message" / "close" / "error"。
|
||||
public func on(event: String, listener: (WebSocketMessage) -> Unit): Unit {
|
||||
if (event == WsEvents.message) {
|
||||
onMessage = listener
|
||||
} else {
|
||||
throw WebSocketException("不支持的事件: ${event}")
|
||||
}
|
||||
}
|
||||
|
||||
/// 注册连接级事件。支持 "close" / "error"。
|
||||
public func on(event: String, listener: (Int64, String) -> Unit): Unit {
|
||||
if (event == WsEvents.close) {
|
||||
onClose = listener
|
||||
} else {
|
||||
throw WebSocketException("不支持的事件: ${event}")
|
||||
}
|
||||
}
|
||||
|
||||
/// 注册连接级事件。支持 "error"。
|
||||
public func on(event: String, listener: (Exception) -> Unit): Unit {
|
||||
if (event == WsEvents.error) {
|
||||
onError = listener
|
||||
} else {
|
||||
throw WebSocketException("不支持的事件: ${event}")
|
||||
}
|
||||
}
|
||||
|
||||
/// 发送消息。
|
||||
public func send(message: WebSocketMessage): Unit {
|
||||
ensureOpen()
|
||||
if (message.`type` == MessageType.Text) {
|
||||
frame.writeFrame(1, message.bytes)
|
||||
} else {
|
||||
frame.writeFrame(2, message.bytes)
|
||||
}
|
||||
}
|
||||
|
||||
/// 发送文本消息。
|
||||
public func sendText(text: String): Unit {
|
||||
ensureOpen()
|
||||
frame.writeText(text)
|
||||
}
|
||||
|
||||
/// 发送二进制消息。
|
||||
public func sendBinary(data: Array<Byte>): Unit {
|
||||
ensureOpen()
|
||||
frame.writeBinary(data)
|
||||
}
|
||||
|
||||
/// 发送 Ping(心跳保活)。
|
||||
public func ping(): Unit {
|
||||
ensureOpen()
|
||||
frame.writePing(Array<Byte>(0, repeat: 0))
|
||||
}
|
||||
|
||||
/// 发起关闭握手:发送 close 帧,等待对端回执;5 秒兜底强制断开。
|
||||
public func close(code!: Int64 = 1000, reason!: String = ""): Unit {
|
||||
synchronized(stateLock) {
|
||||
if (state != ReadyState.Open) {
|
||||
return
|
||||
}
|
||||
state = ReadyState.Closing
|
||||
}
|
||||
try {
|
||||
frame.writeClose(code, reason)
|
||||
} catch (_) {
|
||||
terminate()
|
||||
return
|
||||
}
|
||||
spawn { =>
|
||||
sleep(Duration.second * 5)
|
||||
if (state == ReadyState.Closing) {
|
||||
terminate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 立即断开底层连接(不发送 close 帧)。
|
||||
public func terminate(): Unit {
|
||||
synchronized(stateLock) {
|
||||
if (state == ReadyState.Closed) {
|
||||
return
|
||||
}
|
||||
state = ReadyState.Closed
|
||||
}
|
||||
try {
|
||||
frame.close()
|
||||
} catch (_) {
|
||||
()
|
||||
}
|
||||
}
|
||||
|
||||
private func ensureOpen(): Unit {
|
||||
if (state != ReadyState.Open) {
|
||||
throw WebSocketException("连接未打开(readyState=${state})")
|
||||
}
|
||||
}
|
||||
|
||||
/// 读循环(由 WebSocketServer 的连接处理协程调用)。
|
||||
internal func runReadLoop(): Unit {
|
||||
var code: Int64 = 1006
|
||||
var reason: String = ""
|
||||
try {
|
||||
while (true) {
|
||||
let m = frame.readMessage()
|
||||
match (m) {
|
||||
case None =>
|
||||
break
|
||||
case Some(f) =>
|
||||
if (f.opcode == 8) {
|
||||
let (c, r) = FrameStream.parseClosePayload(f.payload)
|
||||
code = c
|
||||
reason = r
|
||||
if (state == ReadyState.Open) {
|
||||
try {
|
||||
frame.writeClose(c, r)
|
||||
} catch (_) {
|
||||
()
|
||||
}
|
||||
}
|
||||
break
|
||||
} else {
|
||||
let msg = if (f.opcode == 1) {
|
||||
WebSocketMessage(MessageType.Text, f.payload)
|
||||
} else {
|
||||
WebSocketMessage(MessageType.Binary, f.payload)
|
||||
}
|
||||
onMessage(msg)
|
||||
if (let Some(p) <- parent) {
|
||||
p.fireMessage(this, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
code = 1006
|
||||
if (state != ReadyState.Closing) {
|
||||
onError(e)
|
||||
if (let Some(p) <- parent) {
|
||||
p.fireError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
synchronized(stateLock) {
|
||||
state = ReadyState.Closed
|
||||
}
|
||||
try {
|
||||
frame.close()
|
||||
} catch (_) {
|
||||
()
|
||||
}
|
||||
if (let Some(p) <- parent) {
|
||||
p.removeConnection(this)
|
||||
}
|
||||
onClose(code, reason)
|
||||
if (let Some(p) <- parent) {
|
||||
p.fireClose(this, code, reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
/*
|
||||
* simcu::websocket.server —— 事件回调式 WebSocket 服务端(RFC 6455)。
|
||||
*
|
||||
* 用法:
|
||||
* let server = WebSocketServer(bindAt: 8080)
|
||||
* server.on("connection", { (conn) =>
|
||||
* println("新连接: ${conn.remoteAddress}")
|
||||
* conn.on("message", { (msg) => conn.sendText("echo: ${msg.text}") })
|
||||
* })
|
||||
* server.listen()
|
||||
* ...
|
||||
* server.close() // 停止监听并断开所有连接
|
||||
*
|
||||
* 服务端事件:on('connection') / on('message') / on('close') / on('error')。
|
||||
* 连接方法:send / sendText / sendBinary / ping / close / terminate。
|
||||
* 集群方法:broadcast / broadcastText / broadcastBinary / connectionCount。
|
||||
*/
|
||||
package simcu::websocket.server
|
||||
|
||||
import std.collection.ArrayList
|
||||
import std.net.IPSocketAddress
|
||||
import std.net.SocketException
|
||||
import std.net.TcpServerSocket
|
||||
import std.net.TcpSocket
|
||||
import std.sync.Mutex
|
||||
|
||||
import simcu::websocket.common.FrameStream
|
||||
import simcu::websocket.common.MessageType
|
||||
import simcu::websocket.common.WebSocketMessage
|
||||
import simcu::websocket.common.WebSocketException
|
||||
import simcu::websocket.common.WsEvents
|
||||
|
||||
/// 事件回调式 WebSocket 服务端。
|
||||
public class WebSocketServer {
|
||||
private let bindPort: UInt16
|
||||
private let pathFilter: ?String
|
||||
private let maxPayload: Int64
|
||||
private var serverSock: ?TcpServerSocket = None
|
||||
private var running = false
|
||||
private var connections = ArrayList<WebSocketConnection>()
|
||||
private var nextConnId: Int64 = 1
|
||||
private let connLock = Mutex()
|
||||
|
||||
/// 服务端事件:有连接完成握手时触发(参数为连接对象)。
|
||||
public var onConnection: (WebSocketConnection) -> Unit = { conn => () }
|
||||
/// 服务端事件:任意连接收到消息时触发(参数为连接对象与消息)。
|
||||
public var onMessage: (WebSocketConnection, WebSocketMessage) -> Unit = { conn, msg => () }
|
||||
/// 服务端事件:任意连接关闭时触发(参数为连接对象、关闭码、原因)。
|
||||
public var onClose: (WebSocketConnection, Int64, String) -> Unit = { conn, code, reason => () }
|
||||
/// 服务端事件:监听或连接运行时错误时触发(参数为异常)。
|
||||
public var onError: (Exception) -> Unit = { e => () }
|
||||
|
||||
/// @param bindAt 监听端口,0 表示随机空闲端口(listen 后通过 localPort 读取)。
|
||||
/// @param path 仅接受该路径的握手请求(None 表示不限制)。
|
||||
/// @param maxPayload 单条消息最大字节数,默认 64KB。
|
||||
public init(bindAt!: UInt16 = 0, path!: ?String = None, maxPayload!: Int64 = 65536) {
|
||||
this.bindPort = bindAt
|
||||
this.pathFilter = path
|
||||
this.maxPayload = maxPayload
|
||||
}
|
||||
|
||||
/// 注册服务端事件。支持 "connection"。
|
||||
public func on(event: String, listener: (WebSocketConnection) -> Unit): Unit {
|
||||
if (event == WsEvents.connection) {
|
||||
onConnection = listener
|
||||
} else {
|
||||
throw WebSocketException("不支持的事件: ${event}")
|
||||
}
|
||||
}
|
||||
|
||||
/// 注册服务端事件。支持 "message"。
|
||||
public func on(event: String, listener: (WebSocketConnection, WebSocketMessage) -> Unit): Unit {
|
||||
if (event == WsEvents.message) {
|
||||
onMessage = listener
|
||||
} else {
|
||||
throw WebSocketException("不支持的事件: ${event}")
|
||||
}
|
||||
}
|
||||
|
||||
/// 注册服务端事件。支持 "close"。
|
||||
public func on(event: String, listener: (WebSocketConnection, Int64, String) -> Unit): Unit {
|
||||
if (event == WsEvents.close) {
|
||||
onClose = listener
|
||||
} else {
|
||||
throw WebSocketException("不支持的事件: ${event}")
|
||||
}
|
||||
}
|
||||
|
||||
/// 注册服务端事件。支持 "error"。
|
||||
public func on(event: String, listener: (Exception) -> Unit): Unit {
|
||||
if (event == WsEvents.error) {
|
||||
onError = listener
|
||||
} else {
|
||||
throw WebSocketException("不支持的事件: ${event}")
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动监听:bind + accept 循环,每连接一个协程处理握手与读循环。
|
||||
public func listen(backlog!: Int64 = 128): Unit {
|
||||
if (running) {
|
||||
throw WebSocketException("服务端已在监听")
|
||||
}
|
||||
let s = TcpServerSocket(bindAt: bindPort)
|
||||
s.backlogSize = backlog
|
||||
s.bind()
|
||||
serverSock = Some(s)
|
||||
running = true
|
||||
spawn { => acceptLoop() }
|
||||
}
|
||||
|
||||
/// 实际监听端口(bindAt=0 时用于发现随机端口)。
|
||||
public prop localPort: UInt16 {
|
||||
get() {
|
||||
let sa = serverSock.getOrThrow().localAddress
|
||||
if (let Some(ip) <- (sa as IPSocketAddress)) {
|
||||
ip.port
|
||||
} else {
|
||||
throw WebSocketException("无法解析本地监听端口")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 当前在线连接数。
|
||||
public prop connectionCount: Int64 {
|
||||
get() {
|
||||
synchronized(connLock) {
|
||||
connections.size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 停止监听并强制断开所有连接(每连接会触发自身 close 事件)。
|
||||
public func close(): Unit {
|
||||
running = false
|
||||
if (let Some(s) <- serverSock) {
|
||||
try {
|
||||
s.close()
|
||||
} catch (_) {
|
||||
()
|
||||
}
|
||||
serverSock = None
|
||||
}
|
||||
let snapshot = snapshotConnections()
|
||||
for (c in snapshot) {
|
||||
c.terminate()
|
||||
}
|
||||
}
|
||||
|
||||
/// 向所有在线连接广播消息(单个连接发送失败不影响其余连接)。
|
||||
public func broadcast(message: WebSocketMessage): Unit {
|
||||
let snapshot = snapshotConnections()
|
||||
for (c in snapshot) {
|
||||
try {
|
||||
c.send(message)
|
||||
} catch (_) {
|
||||
()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 广播文本消息。
|
||||
public func broadcastText(text: String): Unit {
|
||||
broadcast(WebSocketMessage.fromText(text))
|
||||
}
|
||||
|
||||
/// 广播二进制消息。
|
||||
public func broadcastBinary(data: Array<Byte>): Unit {
|
||||
broadcast(WebSocketMessage.fromBinary(data))
|
||||
}
|
||||
|
||||
// ===== 内部回调转发(供 WebSocketConnection 调用)=====
|
||||
|
||||
internal func fireMessage(conn: WebSocketConnection, msg: WebSocketMessage): Unit {
|
||||
onMessage(conn, msg)
|
||||
}
|
||||
|
||||
internal func fireClose(conn: WebSocketConnection, code: Int64, reason: String): Unit {
|
||||
onClose(conn, code, reason)
|
||||
}
|
||||
|
||||
internal func fireError(e: Exception): Unit {
|
||||
onError(e)
|
||||
}
|
||||
|
||||
internal func addConnection(conn: WebSocketConnection): Unit {
|
||||
synchronized(connLock) {
|
||||
connections.add(conn)
|
||||
}
|
||||
}
|
||||
|
||||
internal func allocateConnId(): Int64 {
|
||||
synchronized(connLock) {
|
||||
let id = nextConnId
|
||||
nextConnId += 1
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
internal func removeConnection(conn: WebSocketConnection): Unit {
|
||||
synchronized(connLock) {
|
||||
var idx: Int64 = -1
|
||||
for (i in 0..connections.size) {
|
||||
if (connections[i].connId == conn.connId) {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if (idx >= 0) {
|
||||
connections.remove(at: idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func snapshotConnections(): ArrayList<WebSocketConnection> {
|
||||
synchronized(connLock) {
|
||||
let copy = ArrayList<WebSocketConnection>()
|
||||
for (c in connections) {
|
||||
copy.add(c)
|
||||
}
|
||||
copy
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 监听与握手 =====
|
||||
|
||||
private func acceptLoop(): Unit {
|
||||
while (running) {
|
||||
try {
|
||||
let s = serverSock
|
||||
if (let Some(sock) <- s) {
|
||||
let client = sock.accept()
|
||||
spawn { => handleConnection(client) }
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} catch (e: SocketException) {
|
||||
if (!running) {
|
||||
return
|
||||
}
|
||||
onError(e)
|
||||
} catch (e: Exception) {
|
||||
if (!running) {
|
||||
return
|
||||
}
|
||||
onError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleConnection(sock: TcpSocket): Unit {
|
||||
let fs = FrameStream(sock, maxPayload, false)
|
||||
try {
|
||||
let request = fs.readHttpHeader()
|
||||
let lines = request.split("\r\n")
|
||||
if (lines.size == 0 || !lines[0].startsWith("GET ")) {
|
||||
reject(sock, 400, "Bad Request")
|
||||
return
|
||||
}
|
||||
let parts = lines[0].split(" ")
|
||||
if (parts.size < 3) {
|
||||
reject(sock, 400, "Bad Request")
|
||||
return
|
||||
}
|
||||
if (let Some(p) <- pathFilter) {
|
||||
if (parts[1] != p) {
|
||||
reject(sock, 404, "Not Found")
|
||||
return
|
||||
}
|
||||
}
|
||||
var upgradeOk = false
|
||||
var connectionOk = false
|
||||
var key = ""
|
||||
for (i in 1..lines.size) {
|
||||
let line = lines[i]
|
||||
let ci = line.indexOf(":")
|
||||
if (let Some(c) <- ci) {
|
||||
let name = line[0..c].toAsciiLower()
|
||||
let value = line[c + 1..].trimAscii()
|
||||
if (name == "upgrade") {
|
||||
upgradeOk = value.toAsciiLower().contains("websocket")
|
||||
}
|
||||
if (name == "connection") {
|
||||
connectionOk = value.toAsciiLower().contains("upgrade")
|
||||
}
|
||||
if (name == "sec-websocket-key") {
|
||||
key = value
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!upgradeOk || !connectionOk || key == "") {
|
||||
reject(sock, 400, "Bad Request")
|
||||
return
|
||||
}
|
||||
|
||||
// 101 响应
|
||||
let accept = FrameStream.computeAccept(key)
|
||||
let resp = StringBuilder()
|
||||
resp.append("HTTP/1.1 101 Switching Protocols\r\n")
|
||||
resp.append("Upgrade: websocket\r\n")
|
||||
resp.append("Connection: Upgrade\r\n")
|
||||
resp.append("Sec-WebSocket-Accept: ${accept}\r\n\r\n")
|
||||
sock.write(resp.toString().toArray())
|
||||
sock.flush()
|
||||
|
||||
let conn = WebSocketConnection(Some(this), fs, allocateConnId())
|
||||
addConnection(conn)
|
||||
onConnection(conn)
|
||||
conn.runReadLoop()
|
||||
} catch (e: Exception) {
|
||||
// 握手阶段失败:上报服务端 error 事件
|
||||
onError(e)
|
||||
}
|
||||
}
|
||||
|
||||
private func reject(sock: TcpSocket, status: Int64, message: String): Unit {
|
||||
try {
|
||||
let resp = "HTTP/1.1 ${status} ${message}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
||||
sock.write(resp.toArray())
|
||||
sock.flush()
|
||||
} catch (_) {
|
||||
()
|
||||
}
|
||||
try {
|
||||
sock.close()
|
||||
} catch (_) {
|
||||
()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user