Files
websocket-cj/README.md
T

281 lines
13 KiB
Markdown
Raw 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`10 个端到端用例全通过)
## 功能特性
- **客户端**`connect()` / `send()`(三重重载)/ `ping()` / `close()` / `terminate()` + 事件回调 `onOpen` / `onMessage` / `onError` / `onClose`
- **服务端**`WebSocketServer(port)` + `addHandler(path, factory)` 按路径路由,每连接一个独立 handler 实例;`start()` / `close()`
- **链式门面**:组管理与发送统一收敛到 `ws.group(name)` / `ws.conn(connId)`,同一个功能只有一种入口
- **组管理**:入组 / 出组 / 组计数 / 组判空 / 组内连接快照 / 连接所属组 / 全部组名,连接断开自动清组
- **发送(三重重载)**`send(message)` / `send(text)` / `send(data)` 覆盖消息对象、文本、二进制;客户端 / 连接 / 组播 / 单播 / 广播统一
- **RFC 6455 完整帧层**:掩码、分片重组、Ping/Pong、Close 握手、`maxPayload` 上限(超限 1009
- **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.WebSocketHandler
import simcu::websocket.server.WebSocketConnection
import simcu::websocket.common.WebSocketMessage
```
## 目录结构
```
src/
├── websocket.cj # 模块锚点
├── common/
│ ├── ws_types.cj # WebSocketMessage / ReadyState / WebSocketException / CloseCodes / WsEvents
│ └── frame_stream.cj # RFC 6455 帧层(握手工具、掩码、分片重组、maxPayload)
├── client/
│ └── websocket_client.cj # 客户端
├── server/
│ ├── i_websocket_handler.cj # 服务端 handler 接口(onConnect/onMessage/onClose/onError
│ ├── websocket_server.cj # 监听 / 握手 / 按路径路由 / 广播 / 组存储
│ ├── websocket_senders.cj # 链式门面(组管理 + 组播/单播发送的实现)
│ └── websocket_connection.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.send("hello") // 发送文本(String 重载)
client.send(byteArray) // 发送二进制(Array<Byte> 重载)
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(8080) // 端口,0 表示随机空闲端口
server.addHandler("/ws") { => MyHandler() } // 路径 -> 工厂,每次握手创建独立 handler 实例
server.start() // 开始监听(异步 accept
...
server.close() // 停止监听并断开所有连接
server.localPort // 实际监听端口(端口 0 时用)
server.connectionCount // 当前在线连接数
server.broadcast("hi-all") // 广播文本
server.broadcast(bytes) // 广播二进制
server.groups() // 所有组名(快照)
```
构造参数:
| 参数 | 默认 | 说明 |
|---|---|---|
| `port: Int64` | — | 监听端口,`0` 表示随机空闲端口(`start` 后读 `localPort` |
| `maxPayload: Int64` | `65536` | 单条消息最大字节数 |
### Handler 接口
`WebSocketHandler` 只关心连接生命周期回调;端点路径由 `addHandler(path, factory)` 注册时指定,handler 自身不感知。每个连接在握手成功时由工厂创建一个独立的 handler 实例,因此 handler 内可持有该连接的会话状态(如 connectionId / 组信息),但不要持有会跨连接共享的 scoped 服务。
```cangjie
public interface WebSocketHandler {
func onConnect(conn: WebSocketConnection): Unit
func onMessage(conn: WebSocketConnection, msg: WebSocketMessage): Unit
func onClose(conn: WebSocketConnection, code: Int64, reason: String): Unit
func onError(conn: WebSocketConnection, e: Exception): Unit
}
```
实现示例:
```cangjie
class MyHandler <: WebSocketHandler {
public override func onConnect(conn: WebSocketConnection): Unit {
conn.joinGroup("authed") // 握手成功即入组
}
public override func onMessage(conn: WebSocketConnection, msg: WebSocketMessage): Unit {
conn.send("echo: ${msg.text.getOrThrow()}") // send 三重重载:String / Array<Byte> / WebSocketMessage
}
public override func onClose(conn: WebSocketConnection, code: Int64, reason: String): Unit {
() // 连接断开由 server 自动清组,无需手动处理
}
public override func onError(conn: WebSocketConnection, e: Exception): Unit {
()
}
}
```
### 链式门面(组管理 + 发送)
组管理与发送的全部实现收敛在链式门面中,`WebSocketServer` 上不再暴露散落的组管理方法:
```cangjie
// 组管理
server.group("room").join(conn) // 入组(组不存在自动创建),也支持 join(connId)
server.group("room").leave(conn) // 出组(组空自动删除),也支持 leave(connId)
server.group("room").count() // 组内连接数
server.group("room").isEmpty() // 组是否为空
server.group("room").connIds() // 组内连接Id快照
// 组播 / 单播 / 广播(send 三重重载:WebSocketMessage / String / Array<Byte>
server.group("room").send("hi").send(bytes)
server.conn(connId).send("hi") // 连接不存在则静默忽略
server.broadcast("hi-all")
// 查询
server.conn(connId).groups() // 连接所属的所有组名
server.groups() // 所有组名
```
组为空时自动删除;连接断开由 server 自动从所有组移除,无需手动清理。
### 连接级 API
```cangjie
conn.send(message) / conn.send(text) / conn.send(bytes) // 三重重载
conn.ping() // 心跳
conn.close(code: 4000, reason: "svr") // 关闭握手
conn.terminate() // 立即断开
conn.joinGroup("room") / conn.leaveGroup("room") // 连接自己入组/出组(经门面实现)
conn.connectionId // 唯一连接Id
conn.readyState / conn.isOpen()
conn.remoteAddress // "ip:port"
conn.requestPath // 握手请求路径
conn.requestQuery // 握手请求 query
```
## 底层帧 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 // 常用关闭码常量
```
## 测试覆盖
| 用例 | 验证点 |
|---|---|
| echoText | 客户端发送 → 服务端回声 → 客户端收到 |
| broadcast | 双客户端广播均收到 |
| closeHandshake | client.close(1000, "bye") 双端 onClose 收到 (1000, "bye") |
| serverClose | conn.close(4000, "svr") → 客户端收 (4000, "svr") |
| terminate | client.terminate() → 服务端收 (1006, "") |
| ping | 心跳后连接仍可收发 |
| pathRoute | 按路径路由:/ws 命中 handler,其他路径拒绝 |
| maxPayload | 超限触发 onError(code=1009) |
| groupMulticast | 组播两连接均收到;连接断开自动退组、组计数减一 |
| unicast | conn(connId).send 只发到指定连接,另一连接收不到 |
## API 演进记录
对照最初需求(connect/send/close + onOpen/onMessage/onError/onClose),已补齐:
- **接口式服务端**`WebSocketServer(port)` + `addHandler(path, factory)`,按路径路由,每连接独立 handler 实例
- **send 三重重载**`send(WebSocketMessage)` / `send(String)` / `send(Array<Byte>)` 全库统一(客户端 / 连接 / 门面 / 广播),不再有 sendText / sendBinary / broadcastText 等散落命名
- **链式门面**`ws.group(name)` / `ws.conn(connId)` 收敛组管理与发送,同一个功能只有一种入口
- **组管理**join / leave / count / isEmpty / connIds / groupsOf / groups + 连接断开自动清组
- **ping()**:心跳保活
- **close(code, reason)**:携带关闭码与原因的关闭握手;onClose 回调带 (code, reason)
- **readyState / isOpen() / connectionId / remoteAddress / requestPath / requestQuery**:连接状态与元信息
- **localPort / connectionCount**:服务端元信息
- **maxPayload**:单条消息上限,超限报 1009
## 尚未实现(后续方向)
- **子协议协商(Sec-WebSocket-Protocol**:握手阶段携带子协议并校验
- **自动定时心跳**:目前为手动 `ping()`,未内置保活定时器
- **分片发送**:接收侧已支持分片重组;发送侧未提供手动分片 API
- **压缩扩展(permessage-deflate**