Files
websocket-cj/src/client/WebSocketClient.cj
T

259 lines
8.9 KiB
Plaintext
Raw Normal View History

/*
* 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 (_) {
()
}
}
}
/// 后台读循环:数据帧 → onMessageclose 帧 → 回执并 onCloseEOF/异常 → 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
}
}