Files

221 lines
9.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`
- **wssTLS**`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"
```
## 底层帧 APIcommon 子包)
高层 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=text2=binary8=close)、`payload: Array<Byte>`
`readMessage()` 返回 `None` 表示对端关闭/EOF;收到 close 帧时已自动回写 close 并关闭流。
### wssTLS)示例
```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**