初始版本:事件回调式 WebSocket 客户端与服务端(RFC 6455)
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* simcu::websocket.client —— 事件回调式 WebSocket 客户端(RFC 6455)。
|
||||
*
|
||||
* 用法:
|
||||
* let client = WebSocketClient("127.0.0.1", 8080)
|
||||
* client.onOpen = { println("已连接") }
|
||||
* client.onMessage = { (m) => println(m.text) }
|
||||
* client.onClose = { (code, reason) => println("关闭: ${code} ${reason}") }
|
||||
* client.connect()
|
||||
* client.sendText("hello")
|
||||
* client.close() // 或 client.terminate() 立即断开
|
||||
*
|
||||
* 说明:
|
||||
* - connect() 抛出的异常表示握手失败;连接建立后的错误走 onError。
|
||||
* - close(code, reason) 发起 RFC 6455 关闭握手(等待对端回 close 帧,5 秒兜底);
|
||||
* terminate() 直接断开底层连接。
|
||||
* - 回调在后台读线程触发,禁止在回调中直接做耗时操作。
|
||||
*/
|
||||
package simcu::websocket.client
|
||||
|
||||
import std.net.TcpSocket
|
||||
import std.time.*
|
||||
import std.sync.Mutex
|
||||
|
||||
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
|
||||
|
||||
/// 事件回调式 WebSocket 客户端。
|
||||
public class WebSocketClient {
|
||||
private let host: String
|
||||
private let port: UInt16
|
||||
private let path: String
|
||||
private let maxPayload: Int64
|
||||
private var frame: ?FrameStream = None
|
||||
private var state: ReadyState = ReadyState.Closed
|
||||
private let stateLock = Mutex()
|
||||
|
||||
/// 连接建立(握手完成)后触发,无参数。
|
||||
public var onOpen: () -> Unit = { => () }
|
||||
/// 收到文本/二进制消息时触发,参数为消息。
|
||||
public var onMessage: (WebSocketMessage) -> Unit = { m => () }
|
||||
/// 连接建立后的运行时错误触发,参数为异常。
|
||||
public var onError: (Exception) -> Unit = { e => () }
|
||||
/// 连接关闭时触发,参数为 (关闭码, 关闭原因)。
|
||||
public var onClose: (Int64, String) -> Unit = { c, r => () }
|
||||
|
||||
/// @param host 服务端主机(IP 或域名)。
|
||||
/// @param port 服务端端口。
|
||||
/// @param path 请求路径,默认 "/"。
|
||||
/// @param maxPayload 单条消息最大字节数,默认 64KB,超出触发 onError(code=1009)。
|
||||
public init(host: String, port: UInt16, path!: String = "/", maxPayload!: Int64 = 65536) {
|
||||
this.host = host
|
||||
this.port = port
|
||||
this.path = path
|
||||
this.maxPayload = maxPayload
|
||||
}
|
||||
|
||||
/// 当前连接状态(Connecting/Open/Closing/Closed)。
|
||||
public prop readyState: ReadyState {
|
||||
get() { state }
|
||||
}
|
||||
|
||||
/// 连接是否处于 Open。
|
||||
public func isOpen(): Bool {
|
||||
state == ReadyState.Open
|
||||
}
|
||||
|
||||
/// 建立连接:TCP + WebSocket 握手。
|
||||
/// 成功 → 状态 Open 并触发 onOpen,随后后台线程持续读取消息。
|
||||
/// 失败(网络/握手被拒)→ 抛出 WebSocketException。
|
||||
public func connect(timeout!: ?Duration = None): Unit {
|
||||
if (state != ReadyState.Closed) {
|
||||
throw WebSocketException("重复 connect:当前 readyState=${state}")
|
||||
}
|
||||
state = ReadyState.Connecting
|
||||
let sock = TcpSocket(host, port)
|
||||
sock.connect(timeout: timeout)
|
||||
let fs = FrameStream(sock, maxPayload, true)
|
||||
|
||||
// 发送握手请求
|
||||
let key = FrameStream.generateWebSocketKey()
|
||||
let req = StringBuilder()
|
||||
req.append("GET ${path} HTTP/1.1\r\n")
|
||||
req.append("Host: ${host}:${port}\r\n")
|
||||
req.append("Upgrade: websocket\r\n")
|
||||
req.append("Connection: Upgrade\r\n")
|
||||
req.append("Sec-WebSocket-Key: ${key}\r\n")
|
||||
req.append("Sec-WebSocket-Version: 13\r\n\r\n")
|
||||
sock.write(req.toString().toArray())
|
||||
sock.flush()
|
||||
|
||||
// 读取并校验握手响应
|
||||
let response = fs.readHttpHeader()
|
||||
if (!validateHandshake(response, key)) {
|
||||
fs.close()
|
||||
sock.close()
|
||||
state = ReadyState.Closed
|
||||
throw WebSocketException("WebSocket 握手失败(服务端拒绝升级)")
|
||||
}
|
||||
|
||||
frame = Some(fs)
|
||||
state = ReadyState.Open
|
||||
onOpen()
|
||||
spawn { => readLoop() }
|
||||
}
|
||||
|
||||
/// 发送消息(按消息类型自动选择 text/binary 帧)。
|
||||
public func send(message: WebSocketMessage): Unit {
|
||||
let f = frame.getOrThrow()
|
||||
if (message.`type` == MessageType.Text) {
|
||||
f.writeFrame(1, message.bytes)
|
||||
} else {
|
||||
f.writeFrame(2, message.bytes)
|
||||
}
|
||||
}
|
||||
|
||||
/// 发送文本消息。
|
||||
public func sendText(text: String): Unit {
|
||||
frame.getOrThrow().writeText(text)
|
||||
}
|
||||
|
||||
/// 发送二进制消息。
|
||||
public func sendBinary(data: Array<Byte>): Unit {
|
||||
frame.getOrThrow().writeBinary(data)
|
||||
}
|
||||
|
||||
/// 发送 Ping(心跳保活),对端 Pong 由帧层自动忽略。
|
||||
public func ping(): Unit {
|
||||
frame.getOrThrow().writePing(Array<Byte>(0, repeat: 0))
|
||||
}
|
||||
|
||||
/// 发送 Ping(携带 payload)。
|
||||
public func ping(payload: Array<Byte>): Unit {
|
||||
frame.getOrThrow().writePing(payload)
|
||||
}
|
||||
|
||||
/// 发起关闭握手:发送 close 帧(code + reason),等待对端回执后触发 onClose。
|
||||
/// 对端 5 秒内不回执则强制断开。
|
||||
public func close(code!: Int64 = 1000, reason!: String = ""): Unit {
|
||||
synchronized(stateLock) {
|
||||
if (state != ReadyState.Open) {
|
||||
return
|
||||
}
|
||||
state = ReadyState.Closing
|
||||
}
|
||||
if (let Some(f) <- frame) {
|
||||
try {
|
||||
f.writeClose(code, reason)
|
||||
} catch (_) {
|
||||
terminate()
|
||||
return
|
||||
}
|
||||
}
|
||||
spawn { =>
|
||||
sleep(Duration.second * 5)
|
||||
if (state == ReadyState.Closing) {
|
||||
terminate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 立即断开底层连接(不发送 close 帧),读线程会触发 onClose(1006, "")。
|
||||
public func terminate(): Unit {
|
||||
synchronized(stateLock) {
|
||||
if (state == ReadyState.Closed) {
|
||||
return
|
||||
}
|
||||
state = ReadyState.Closed
|
||||
}
|
||||
if (let Some(f) <- frame) {
|
||||
try {
|
||||
f.close()
|
||||
} catch (_) {
|
||||
()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 后台读循环:数据帧 → onMessage;close 帧 → 回执并 onClose;EOF/异常 → onError(仅异常)+onClose。
|
||||
private func readLoop(): Unit {
|
||||
var code: Int64 = 1006
|
||||
var reason: String = ""
|
||||
try {
|
||||
while (true) {
|
||||
let m = frame.getOrThrow().readMessage()
|
||||
match (m) {
|
||||
case None =>
|
||||
break // EOF:对端直接断连
|
||||
case Some(f) =>
|
||||
if (f.opcode == 8) {
|
||||
let (c, r) = FrameStream.parseClosePayload(f.payload)
|
||||
code = c
|
||||
reason = r
|
||||
if (state == ReadyState.Open) {
|
||||
try {
|
||||
frame.getOrThrow().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)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
code = 1006
|
||||
if (state != ReadyState.Closing) {
|
||||
onError(e)
|
||||
}
|
||||
}
|
||||
synchronized(stateLock) {
|
||||
state = ReadyState.Closed
|
||||
}
|
||||
if (let Some(f) <- frame) {
|
||||
try {
|
||||
f.close()
|
||||
} catch (_) {
|
||||
()
|
||||
}
|
||||
}
|
||||
onClose(code, reason)
|
||||
}
|
||||
|
||||
/// 校验服务端握手响应:状态行 101 + Upgrade/Connection/Accept 头。
|
||||
private func validateHandshake(response: String, key: String): Bool {
|
||||
if (!response.startsWith("HTTP/1.1 101")) {
|
||||
return false
|
||||
}
|
||||
let expected = FrameStream.computeAccept(key)
|
||||
var foundUpgrade = false
|
||||
var foundAccept = false
|
||||
let lines = response.split("\r\n")
|
||||
for (line in lines) {
|
||||
let ci = line.indexOf(":")
|
||||
if (let Some(c) <- ci) {
|
||||
let name = line[0..c].toAsciiLower()
|
||||
let value = line[c + 1..].trimAscii()
|
||||
if (name == "upgrade" && value.toAsciiLower().contains("websocket")) {
|
||||
foundUpgrade = true
|
||||
}
|
||||
if (name == "sec-websocket-accept" && value == expected) {
|
||||
foundAccept = true
|
||||
}
|
||||
}
|
||||
}
|
||||
foundUpgrade && foundAccept
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user