/* * 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): Unit { frame.getOrThrow().writeBinary(data) } /// 发送 Ping(心跳保活),对端 Pong 由帧层自动忽略。 public func ping(): Unit { frame.getOrThrow().writePing(Array(0, repeat: 0)) } /// 发送 Ping(携带 payload)。 public func ping(payload: Array): 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 } }