# simcu::websocket — 仓颉 WebSocket 库(RFC 6455) 事件回调式 WebSocket 客户端与服务端,纯仓颉实现,仅依赖标准库 + stdx(无第三方库)。 - 组织:`simcu` - 包名:`websocket` - 子包:`simcu::websocket.client` / `simcu::websocket.server` / `simcu::websocket.common` - 构建:`cjpm build`;测试:`cjpm test`(8 个端到端用例全通过) ## 功能特性 - **客户端**:`connect()` / `send()` / `close()` / `terminate()` + 事件回调 `onOpen` / `onMessage` / `onError` / `onClose` - **服务端**:`on('connection')` / `on('message')` / `send()` / `close()` / `terminate()` + 监听 `on('close')` / `on('error')` - **RFC 6455 完整帧层**:掩码、分片重组、Ping/Pong、Close 握手、`maxPayload` 上限(超限 1009) - **群发广播**:`broadcastText` / `broadcastBinary` - **wss(TLS)**:`FrameStream` 基于 `StreamingSocket`,可直接包裹 `TlsSocket`(已在 BotRoleHelper 实战验证) ## 依赖引入 在项目 `cjpm.toml` 中添加: ```toml [dependencies] "simcu::websocket" = { path = "../websocket-cj" } ``` ```cangjie import simcu::websocket.client.WebSocketClient import simcu::websocket.server.WebSocketServer import simcu::websocket.server.WebSocketConnection import simcu::websocket.common.WebSocketMessage ``` ## 目录结构 ``` src/ ├── websocket.cj # 模块锚点 ├── common/ │ ├── WsTypes.cj # WebSocketMessage / ReadyState / WebSocketException / CloseCodes / WsEvents │ └── FrameStream.cj # RFC 6455 帧层(握手工具、掩码、分片重组、maxPayload) ├── client/ │ └── WebSocketClient.cj # 客户端 ├── server/ │ ├── WebSocketServer.cj # 服务端监听 / 握手 / 广播 │ └── WebSocketConnection.cj # 服务端单条连接 └── tests/ └── websocket_test.cj # 端到端集成测试 ``` ## 客户端 API ```cangjie let client = WebSocketClient("127.0.0.1", 8080, path: "/ws", maxPayload: 65536) // 事件回调(连接建立后触发) client.onOpen = { => ... } // 握手完成 client.onMessage = { m: WebSocketMessage => ... } // 收到消息(m.text / m.bytes / m.type) client.onError = { e: Exception => ... } // 运行时错误(WebSocketException 含 .code) client.onClose = { code: Int64, reason: String => ... } // 关闭,携带关闭码与原因 client.connect(timeout: Duration.second * 30) // 建立连接;握手失败抛 WebSocketException client.send(WebSocketMessage) // 发送消息对象 client.sendText("hello") // 发送文本 client.sendBinary(byteArray) // 发送二进制 client.ping() // 心跳(服务端自动回 Pong) client.close(code: 1000, reason: "bye") // 关闭握手,5 秒兜底强制断开 client.terminate() // 立即断开(对端收 1006) client.readyState // Connecting / Open / Closing / Closed client.isOpen() ``` 构造参数: | 参数 | 默认 | 说明 | |---|---|---| | `host: String` | — | 服务端主机(IP 或域名) | | `port: UInt16` | — | 服务端端口 | | `path: String` | `"/"` | 请求路径 | | `maxPayload: Int64` | `65536` | 单条消息最大字节数,超出触发 `onError(code=1009)` | ## 服务端 API ```cangjie let server = WebSocketServer(bindAt: 8080, path: Some("/ws"), maxPayload: 65536) // 服务端事件 server.on("connection", { conn: WebSocketConnection => ... }) // 新连接完成握手 server.on("message", { conn, msg => ... }) // 任意连接收到消息 server.on("close", { conn, code, reason => ... }) // 任意连接关闭 server.on("error", { e => ... }) // 监听 / 连接错误 server.listen() // 开始监听(异步 accept) server.close() // 停止监听并关闭全部连接 server.broadcastText("hi-all") // 群发文本 server.broadcastBinary(bytes) // 群发二进制 server.localPort // 实际监听端口(bindAt=0 时随机) server.connectionCount // 当前连接数 ``` 构造参数: | 参数 | 默认 | 说明 | |---|---|---| | `bindAt: UInt16` | `0` | 监听端口,`0` 表示随机空闲端口(`listen` 后读 `localPort`) | | `path: ?String` | `None` | 仅接受该路径的握手请求,`None` 表示不限制 | | `maxPayload: Int64` | `65536` | 单条消息最大字节数 | ### 连接级 API ```cangjie // 连接级事件(conn.on 重载按监听器参数区分,也可直接赋 conn.onMessage 等属性) conn.on("message", { m: WebSocketMessage => ... }) conn.on("close", { code: Int64, reason: String => ... }) conn.on("error", { e: Exception => ... }) conn.send(message) / sendText(text) / sendBinary(bytes) / ping() conn.close(code: 4000, reason: "svr") // 关闭握手 conn.terminate() // 立即断开 conn.readyState / conn.isOpen() conn.remoteAddress // "ip:port" ``` ## 底层帧 API(common 子包) 高层 client/server 已封装全部帧逻辑;需要自定义握手、代理或 wss 包裹时,可直接使用 `FrameStream`(lurmix 的 BotRoleHelper 即基于它实现): ```cangjie import simcu::websocket.common.FrameStream // 构造:包裹 TCP(或 TLS)流;客户端必须 maskOutgoing=true,服务端 false let fs = FrameStream(conn, maxPayload, true) // 握手工具 FrameStream.generateWebSocketKey() // 生成 Sec-WebSocket-Key FrameStream.computeAccept(key) // 计算 Sec-WebSocket-Accept FrameStream.parseClosePayload(payload) // 解析 close 帧 -> (code, reason) // 读写 fs.readHttpHeader() // 读 HTTP 头(到 \r\n\r\n),帧字节保留在缓冲 fs.readMessage() // 读一条完整消息:分片重组、自动回 Pong;返回 ?WsFrame fs.writeText(text) / fs.writeBinary(data) fs.writeFrame(opcode, payload) // 发送任意 opcode 帧 fs.writePing(payload) / fs.writePong(payload) fs.writeClose(code, reason) fs.close() // 关闭底层流 fs.remoteAddress() // 远端 SocketAddress ``` `WsFrame` 字段:`fin: Bool`、`opcode: Int64`(1=text,2=binary,8=close)、`payload: Array`。 `readMessage()` 返回 `None` 表示对端关闭/EOF;收到 close 帧时已自动回写 close 并关闭流。 ### wss(TLS)示例 ```cangjie import stdx.net.tls.* import stdx.net.tls.common.* let tcp = TcpSocket(host, port) tcp.connect(timeout: Duration.second * 30) var tls = TlsClientConfig() tls.verifyMode = CertificateVerifyMode.TrustAll tls.serverName = Some(host) let tlsSocket = TlsSocket.client(tcp, session: None, clientConfig: tls) tlsSocket.handshake(timeout: Duration.second * 30) let fs = FrameStream(tlsSocket, maxPayload, true) // 客户端掩码 + TLS 包裹 = wss ``` 完整实战示例见 `lemon-lurmix-cj/src/helpers/BotRoleHelper.cj`。 ## 消息与错误码 ```cangjie // WebSocketMessage m.type // MessageType.Text / Binary m.bytes // 原始 payload m.text // 文本内容(二进制消息返回 None) WebSocketMessage.fromText("hi") / fromBinary(bytes) // WebSocketException(异常含 RFC 6455 关闭码) ex.code // 1002 协议错误 / 1009 消息过大 / 1006 对端异常断开 ... CloseCodes.messageTooBig // 常用关闭码常量 // 服务端事件名(on(event, ...) 使用) WsEvents.connection / WsEvents.message / WsEvents.close / WsEvents.error ``` ## 测试覆盖 | 用例 | 验证点 | |---|---| | echoText | 客户端发送 → 服务端回声 → 客户端收到 | | broadcast | 双客户端群发均收到 | | closeHandshake | client.close(1000, "bye") 双端 onClose 收到 (1000, "bye") | | serverClose | conn.close(4000, "svr") → 客户端收 (4000, "svr") | | terminate | client.terminate() → 服务端收 (1006, "") | | ping | 心跳后连接仍可收发 | | pathFilter | path 过滤:/ws 通过,/other 拒绝 | | maxPayload | 超限触发 onError(code=1009) | ## 已补齐的 API 缺口 对照最初需求(connect/send/close + onOpen/onMessage/onError/onClose),补齐了: - **sendText / sendBinary**:按文本 / 二进制发送的重载(`send(WebSocketMessage)` 之外) - **ping()**:心跳保活 - **close(code, reason)**:携带关闭码与原因的关闭握手;onClose 回调带 (code, reason) - **readyState / isOpen()**:连接状态查询 - **broadcastText / broadcastBinary**:服务端群发 - **localPort / connectionCount / remoteAddress**:服务端与连接元信息 - **服务端 path 过滤**:仅接受指定路径的握手 - **maxPayload**:单条消息上限,超限报 1009 ## 尚未实现(后续方向) - **子协议协商(Sec-WebSocket-Protocol)**:握手阶段携带子协议并校验 - **自动定时心跳**:目前为手动 `ping()`,未内置保活定时器 - **分片发送**:接收侧已支持分片重组;发送侧未提供手动分片 API - **压缩扩展(permessage-deflate)**