重构服务端为接口式(WebSocketServer(port)+addHandler 工厂,每连接独立 handler),组管理与发送收敛为链式门面 group(name)/conn(connId),统一 send 三重重载,升级版本 1.1.0
This commit is contained in:
@@ -1,18 +1,20 @@
|
||||
# simcu::websocket — 仓颉 WebSocket 库(RFC 6455)
|
||||
|
||||
事件回调式 WebSocket 客户端与服务端,纯仓颉实现,仅依赖标准库 + stdx(无第三方库)。
|
||||
事件回调式 WebSocket 客户端 + 接口式服务端,纯仓颉实现,仅依赖标准库 + stdx(无第三方库)。
|
||||
|
||||
- 组织:`simcu`
|
||||
- 包名:`websocket`
|
||||
- 子包:`simcu::websocket.client` / `simcu::websocket.server` / `simcu::websocket.common`
|
||||
- 构建:`cjpm build`;测试:`cjpm test`(8 个端到端用例全通过)
|
||||
- 构建:`cjpm build`;测试:`cjpm test`(10 个端到端用例全通过)
|
||||
|
||||
## 功能特性
|
||||
|
||||
- **客户端**:`connect()` / `send()` / `close()` / `terminate()` + 事件回调 `onOpen` / `onMessage` / `onError` / `onClose`
|
||||
- **服务端**:`on('connection')` / `on('message')` / `send()` / `close()` / `terminate()` + 监听 `on('close')` / `on('error')`
|
||||
- **客户端**:`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)
|
||||
- **群发广播**:`broadcastText` / `broadcastBinary`
|
||||
- **wss(TLS)**:`FrameStream` 基于 `StreamingSocket`,可直接包裹 `TlsSocket`(已在 BotRoleHelper 实战验证)
|
||||
|
||||
## 依赖引入
|
||||
@@ -27,6 +29,7 @@
|
||||
```cangjie
|
||||
import simcu::websocket.client.WebSocketClient
|
||||
import simcu::websocket.server.WebSocketServer
|
||||
import simcu::websocket.server.IWebsocketHandler
|
||||
import simcu::websocket.server.WebSocketConnection
|
||||
import simcu::websocket.common.WebSocketMessage
|
||||
```
|
||||
@@ -42,7 +45,9 @@ src/
|
||||
├── client/
|
||||
│ └── WebSocketClient.cj # 客户端
|
||||
├── server/
|
||||
│ ├── WebSocketServer.cj # 服务端监听 / 握手 / 广播
|
||||
│ ├── IWebsocketHandler.cj # 服务端 handler 接口(onConnect/onMessage/onClose/onError)
|
||||
│ ├── WebSocketServer.cj # 监听 / 握手 / 按路径路由 / 广播 / 组存储
|
||||
│ ├── WebSocketSenders.cj # 链式门面(组管理 + 组播/单播发送的实现)
|
||||
│ └── WebSocketConnection.cj # 服务端单条连接
|
||||
└── tests/
|
||||
└── websocket_test.cj # 端到端集成测试
|
||||
@@ -61,8 +66,8 @@ client.onClose = { code: Int64, reason: String => ... } // 关闭,携带关
|
||||
|
||||
client.connect(timeout: Duration.second * 30) // 建立连接;握手失败抛 WebSocketException
|
||||
client.send(WebSocketMessage) // 发送消息对象
|
||||
client.sendText("hello") // 发送文本
|
||||
client.sendBinary(byteArray) // 发送二进制
|
||||
client.send("hello") // 发送文本(String 重载)
|
||||
client.send(byteArray) // 发送二进制(Array<Byte> 重载)
|
||||
client.ping() // 心跳(服务端自动回 Pong)
|
||||
client.close(code: 1000, reason: "bye") // 关闭握手,5 秒兜底强制断开
|
||||
client.terminate() // 立即断开(对端收 1006)
|
||||
@@ -83,43 +88,98 @@ client.isOpen()
|
||||
## 服务端 API
|
||||
|
||||
```cangjie
|
||||
let server = WebSocketServer(bindAt: 8080, path: Some("/ws"), maxPayload: 65536)
|
||||
let server = WebSocketServer(8080) // 端口,0 表示随机空闲端口
|
||||
server.addHandler("/ws") { => MyHandler() } // 路径 -> 工厂,每次握手创建独立 handler 实例
|
||||
server.start() // 开始监听(异步 accept)
|
||||
...
|
||||
server.close() // 停止监听并断开所有连接
|
||||
|
||||
// 服务端事件
|
||||
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 // 当前连接数
|
||||
server.localPort // 实际监听端口(端口 0 时用)
|
||||
server.connectionCount // 当前在线连接数
|
||||
server.broadcast("hi-all") // 广播文本
|
||||
server.broadcast(bytes) // 广播二进制
|
||||
server.groups() // 所有组名(快照)
|
||||
```
|
||||
|
||||
构造参数:
|
||||
|
||||
| 参数 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `bindAt: UInt16` | `0` | 监听端口,`0` 表示随机空闲端口(`listen` 后读 `localPort`) |
|
||||
| `path: ?String` | `None` | 仅接受该路径的握手请求,`None` 表示不限制 |
|
||||
| `port: Int64` | — | 监听端口,`0` 表示随机空闲端口(`start` 后读 `localPort`) |
|
||||
| `maxPayload: Int64` | `65536` | 单条消息最大字节数 |
|
||||
|
||||
### Handler 接口
|
||||
|
||||
`IWebsocketHandler` 只关心连接生命周期回调;端点路径由 `addHandler(path, factory)` 注册时指定,handler 自身不感知。每个连接在握手成功时由工厂创建一个独立的 handler 实例,因此 handler 内可持有该连接的会话状态(如 connectionId / 组信息),但不要持有会跨连接共享的 scoped 服务。
|
||||
|
||||
```cangjie
|
||||
public interface IWebsocketHandler {
|
||||
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 <: IWebsocketHandler {
|
||||
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.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.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.remoteAddress // "ip:port"
|
||||
conn.requestPath // 握手请求路径
|
||||
conn.requestQuery // 握手请求 query
|
||||
```
|
||||
|
||||
## 底层帧 API(common 子包)
|
||||
@@ -181,9 +241,6 @@ 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
|
||||
```
|
||||
|
||||
## 测试覆盖
|
||||
@@ -191,25 +248,28 @@ WsEvents.connection / WsEvents.message / WsEvents.close / WsEvents.error
|
||||
| 用例 | 验证点 |
|
||||
|---|---|
|
||||
| echoText | 客户端发送 → 服务端回声 → 客户端收到 |
|
||||
| broadcast | 双客户端群发均收到 |
|
||||
| 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 拒绝 |
|
||||
| pathRoute | 按路径路由:/ws 命中 handler,其他路径拒绝 |
|
||||
| maxPayload | 超限触发 onError(code=1009) |
|
||||
| groupMulticast | 组播两连接均收到;连接断开自动退组、组计数减一 |
|
||||
| unicast | conn(connId).send 只发到指定连接,另一连接收不到 |
|
||||
|
||||
## 已补齐的 API 缺口
|
||||
## API 演进记录
|
||||
|
||||
对照最初需求(connect/send/close + onOpen/onMessage/onError/onClose),补齐了:
|
||||
对照最初需求(connect/send/close + onOpen/onMessage/onError/onClose),已补齐:
|
||||
|
||||
- **sendText / sendBinary**:按文本 / 二进制发送的重载(`send(WebSocketMessage)` 之外)
|
||||
- **接口式服务端**:`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()**:连接状态查询
|
||||
- **broadcastText / broadcastBinary**:服务端群发
|
||||
- **localPort / connectionCount / remoteAddress**:服务端与连接元信息
|
||||
- **服务端 path 过滤**:仅接受指定路径的握手
|
||||
- **readyState / isOpen() / connectionId / remoteAddress / requestPath / requestQuery**:连接状态与元信息
|
||||
- **localPort / connectionCount**:服务端元信息
|
||||
- **maxPayload**:单条消息上限,超限报 1009
|
||||
|
||||
## 尚未实现(后续方向)
|
||||
|
||||
Reference in New Issue
Block a user