重构服务端为接口式(WebSocketServer(port)+addHandler 工厂,每连接独立 handler),组管理与发送收敛为链式门面 group(name)/conn(connId),统一 send 三重重载,升级版本 1.1.0

This commit is contained in:
2026-08-24 23:52:15 +08:00
parent 0a14859e8e
commit 9b79e6690d
8 changed files with 701 additions and 273 deletions
+101 -41
View File
@@ -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`
- **wssTLS**`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.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 子包)
@@ -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
## 尚未实现(后续方向)
+2 -2
View File
@@ -2,8 +2,8 @@
cjc-version = "1.1.3"
name = "websocket"
organization = "simcu"
description = "SimApi WebSocket 库(RFC 6455):事件回调式客户端与服务端。客户端 connect/send/close + onOpen/onMessage/onError/onClose;服务端 on('connection'/'message'/'close'/'error') + Connection send/close/terminate,支持广播与心跳"
version = "1.0.0"
description = "SimApi WebSocket 库(RFC 6455):事件回调式客户端与接口式服务端。客户端 connect/send(三重重载)/ping/close/terminate + onOpen/onMessage/onError/onClose;服务端 WebSocketServer(port) + addHandler(path, factory) 按路径路由、每连接独立 handler,链式门面 group(name)/conn(connId) 支持组管理/组播/单播/广播,连接断开自动清组"
version = "1.1.0"
target-dir = ""
output-type = "static"
+4 -4
View File
@@ -7,7 +7,7 @@
* client.onMessage = { (m) => println(m.text) }
* client.onClose = { (code, reason) => println("关闭: ${code} ${reason}") }
* client.connect()
* client.sendText("hello")
* client.send("hello")
* client.close() // 或 client.terminate() 立即断开
*
* 说明:
@@ -117,13 +117,13 @@ public class WebSocketClient {
}
}
/// 发送文本消息。
public func sendText(text: String): Unit {
/// 发送文本消息(三重重载:WebSocketMessage / String / Array<Byte>
public func send(text: String): Unit {
frame.getOrThrow().writeText(text)
}
/// 发送二进制消息。
public func sendBinary(data: Array<Byte>): Unit {
public func send(data: Array<Byte>): Unit {
frame.getOrThrow().writeBinary(data)
}
+26
View File
@@ -0,0 +1,26 @@
/*
* simcu::websocket.server —— WebSocket 服务端 handler 接口。
*
* handler 只关心连接生命周期回调:onConnect / onMessage / onClose / onError。
* 端点路径由 WebSocketServer.addHandler(path, factory) 注册时指定,handler 自身不感知。
* 每个连接在握手成功时由工厂创建一个独立的 handler 实例,因此 handler 内可持有该
* 连接的会话状态(如 connectionId / 组信息),但不要持有会跨连接共享的 scoped 服务。
*/
package simcu::websocket.server
import simcu::websocket.common.WebSocketMessage
/// WebSocket 服务端 handler 接口(多 handler 按注册路径路由)。
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
}
+71 -56
View File
@@ -1,11 +1,9 @@
/*
* simcu::websocket.server —— 服务端单条 WebSocket 连接。
*
* 由 WebSocketServer 在握手成功后创建并暴露给 on('connection') 回调
* 提供 send/sendText/sendBinary/ping/close/terminate,以及连接级事件:
* conn.on("message", { (msg) => ... })
* conn.on("close", { (code, reason) => ... })
* conn.on("error", { (e) => ... })
* 由 WebSocketServer 在握手成功后创建并交给匹配的 IWebsocketHandler
* 提供 send(三重重载:WebSocketMessage / String / Array<Byte>/ ping / close / terminate,以及只读属性
* connectionId / readyState / remoteAddress / requestPath / requestQuery。
*/
package simcu::websocket.server
@@ -19,11 +17,11 @@ import simcu::websocket.common.MessageType
import simcu::websocket.common.ReadyState
import simcu::websocket.common.WebSocketMessage
import simcu::websocket.common.WebSocketException
import simcu::websocket.common.WsEvents
/// 服务端单条 WebSocket 连接(事件回调)。
/// 服务端单条 WebSocket 连接(由对应 handler 处理回调)。
public class WebSocketConnection {
private let parent: ?WebSocketServer
private let handler: IWebsocketHandler
private let frame: FrameStream
private let stateLock = Mutex()
private var state: ReadyState = ReadyState.Open
@@ -31,17 +29,54 @@ public class WebSocketConnection {
/// 连接唯一编号(由服务端分配,用于连接集合的引用比较)。
internal let connId: Int64
/// 连接级事件:收到消息(不含父级 connection 参数)。
public var onMessage: (WebSocketMessage) -> Unit = { m => () }
/// 连接级事件:连接关闭,参数 (关闭码, 关闭原因)。
public var onClose: (Int64, String) -> Unit = { c, r => () }
/// 连接级事件:运行时错误。
public var onError: (Exception) -> Unit = { e => () }
/// 连接唯一编号(对外只读,业务层用于关联连接状态)。
public prop connectionId: Int64 {
get() { connId }
}
internal init(parent: ?WebSocketServer, frame: FrameStream, connId: Int64) {
/// 握手请求路径(不含 query,如 "/ws/server")。
public let requestPath: String
/// 握手请求 query 字符串(原始形式,不含 '?';无 query 时为空字符串,如 "serverId=srv-1")。
public let requestQuery: String
/// 回调归属的 handler(由 WebSocketServer 按路径匹配后注入)。
internal prop wsHandler: IWebsocketHandler {
get() { handler }
}
/// 归属的服务端(用于组管理 / 单播 / 组播委托,None 表示已脱离服务端)。
public prop server: ?WebSocketServer {
get() { parent }
}
internal init(parent: ?WebSocketServer, handler: IWebsocketHandler, frame: FrameStream,
connId: Int64, requestPath: String, requestQuery: String) {
this.parent = parent
this.handler = handler
this.frame = frame
this.connId = connId
this.requestPath = requestPath
this.requestQuery = requestQuery
}
/// 取 query 参数值(如 "?serverId=srv-1" → queryParam("serverId") = Some("srv-1"))。
/// 参数不存在返回 None;参数存在但无值时返回 Some("")。
public func queryParam(name: String): ?String {
if (requestQuery.isEmpty()) {
return None
}
for (pair in requestQuery.split("&")) {
if (let Some(e) <- pair.indexOf("=")) {
if (pair[0..e] == name) {
return Some(pair[e + 1..])
}
} else {
if (pair == name) {
return Some("")
}
}
}
None
}
/// 当前连接状态。
@@ -66,34 +101,7 @@ public class WebSocketConnection {
}
}
/// 注册连接级事件。支持 "message" / "close" / "error"
public func on(event: String, listener: (WebSocketMessage) -> Unit): Unit {
if (event == WsEvents.message) {
onMessage = listener
} else {
throw WebSocketException("不支持的事件: ${event}")
}
}
/// 注册连接级事件。支持 "close" / "error"。
public func on(event: String, listener: (Int64, String) -> Unit): Unit {
if (event == WsEvents.close) {
onClose = listener
} else {
throw WebSocketException("不支持的事件: ${event}")
}
}
/// 注册连接级事件。支持 "error"。
public func on(event: String, listener: (Exception) -> Unit): Unit {
if (event == WsEvents.error) {
onError = listener
} else {
throw WebSocketException("不支持的事件: ${event}")
}
}
/// 发送消息。
/// 发送消息(三重重载:WebSocketMessage / String 文本 / Array<Byte> 二进制)
public func send(message: WebSocketMessage): Unit {
ensureOpen()
if (message.`type` == MessageType.Text) {
@@ -104,17 +112,33 @@ public class WebSocketConnection {
}
/// 发送文本消息。
public func sendText(text: String): Unit {
public func send(text: String): Unit {
ensureOpen()
frame.writeText(text)
}
/// 发送二进制消息。
public func sendBinary(data: Array<Byte>): Unit {
public func send(data: Array<Byte>): Unit {
ensureOpen()
frame.writeBinary(data)
}
// ===== 组管理(经 server 转发) =====
/// 将当前连接加入组(组不存在则自动创建)。
public func joinGroup(group: String): Unit {
if (let Some(s) <- parent) {
s.group(group).join(this)
}
}
/// 将当前连接移出组。
public func leaveGroup(group: String): Unit {
if (let Some(s) <- parent) {
s.group(group).leave(this)
}
}
/// 发送 Ping(心跳保活)。
public func ping(): Unit {
ensureOpen()
@@ -193,20 +217,14 @@ public class WebSocketConnection {
} else {
WebSocketMessage(MessageType.Binary, f.payload)
}
onMessage(msg)
if (let Some(p) <- parent) {
p.fireMessage(this, msg)
}
handler.onMessage(this, msg)
}
}
}
} catch (e: Exception) {
code = 1006
if (state != ReadyState.Closing) {
onError(e)
if (let Some(p) <- parent) {
p.fireError(e)
}
handler.onError(this, e)
}
}
synchronized(stateLock) {
@@ -220,9 +238,6 @@ public class WebSocketConnection {
if (let Some(p) <- parent) {
p.removeConnection(this)
}
onClose(code, reason)
if (let Some(p) <- parent) {
p.fireClose(this, code, reason)
}
handler.onClose(this, code, reason)
}
}
+188
View File
@@ -0,0 +1,188 @@
/*
* simcu::websocket.server —— 链式门面(组管理 + 发送)。
*
* 组管理与发送的实现都收敛在这里,由 WebSocketServer.group(name)/conn(connId) 返回:
* ws.group("authed").join(conn).send("hello").send(data) // 入组 + 组播(send 三重重载)
* ws.conn(connId).send("hi").send(data) // 单播
* ws.group("authed").count() / isEmpty() / connIds() // 组信息
* ws.conn(connId).groups() // 连接所属组
*
* 组存储(server.connGroups)由 server 持有并负责连接断开自动清组;
* 门面在 server.connLock 保护下直接操作。连接不存在/发送失败静默忽略,单个失败不影响其余。
*/
package simcu::websocket.server
import std.collection.*
import simcu::websocket.common.WebSocketMessage
/// 判断连接Id是否在列表中(组管理辅助)。
internal func listHasConnId(list: ArrayList<Int64>, id: Int64): Bool {
for (x in list) {
if (x == id) {
return true
}
}
false
}
/// 从列表中移除所有匹配的连接Id(组管理辅助)。
internal func removeConnId(list: ArrayList<Int64>, id: Int64): Unit {
var i: Int64 = 0
while (i < list.size) {
if (list[i] == id) {
list.remove(i..(i + 1))
} else {
i += 1
}
}
}
/// 组门面:由 WebSocketServer.group(name) 返回,支持链式 join/leave/sendsend 三重重载:WebSocketMessage / String / Array<Byte>)与 count/isEmpty/connIds。
public class WebSocketGroupSender {
private let server: WebSocketServer
private let group: String
internal init(server: WebSocketServer, group: String) {
this.server = server
this.group = group
}
/// 向组内所有连接发送消息(链式;组/连接不存在或发送失败均静默忽略)。
public func send(message: WebSocketMessage): WebSocketGroupSender {
for (id in connIds()) {
if (let Some(c) <- server.getConn(id)) {
try {
c.send(message)
} catch (_) {
()
}
}
}
this
}
/// 向组内所有连接发送文本(链式)。
public func send(text: String): WebSocketGroupSender {
send(WebSocketMessage.fromText(text))
}
/// 向组内所有连接发送二进制(链式)。
public func send(data: Array<Byte>): WebSocketGroupSender {
send(WebSocketMessage.fromBinary(data))
}
/// 将连接加入组(组不存在则自动创建;链式)。
public func join(conn: WebSocketConnection): WebSocketGroupSender {
join(conn.connId)
}
/// 将连接加入组(按连接Id;链式)。
public func join(connId: Int64): WebSocketGroupSender {
synchronized(server.connLock) {
if (let Some(l) <- server.connGroups.get(group)) {
if (!listHasConnId(l, connId)) {
l.add(connId)
}
} else {
let l = ArrayList<Int64>()
l.add(connId)
server.connGroups[group] = l
}
}
this
}
/// 将连接移出组(组为空则删除组;链式)。
public func leave(conn: WebSocketConnection): WebSocketGroupSender {
leave(conn.connId)
}
/// 将连接移出组(按连接Id;链式)。
public func leave(connId: Int64): WebSocketGroupSender {
synchronized(server.connLock) {
if (let Some(l) <- server.connGroups.get(group)) {
removeConnId(l, connId)
if (l.size == 0) {
server.connGroups.remove(group)
}
}
}
this
}
/// 组内连接数。
public func count(): Int64 {
synchronized(server.connLock) {
if (let Some(l) <- server.connGroups.get(group)) {
l.size
} else {
0
}
}
}
/// 组是否为空。
public func isEmpty(): Bool {
count() == 0
}
/// 组内所有连接Id(快照)。
public func connIds(): ArrayList<Int64> {
synchronized(server.connLock) {
let copy = ArrayList<Int64>()
if (let Some(l) <- server.connGroups.get(group)) {
for (x in l) {
copy.add(x)
}
}
copy
}
}
}
/// 连接门面:由 WebSocketServer.conn(connId) 返回,支持链式 send(三重重载:WebSocketMessage / String / Array<Byte>)与 groups()。
public class WebSocketConnSender {
private let server: WebSocketServer
private let connId: Int64
internal init(server: WebSocketServer, connId: Int64) {
this.server = server
this.connId = connId
}
/// 向指定连接发送消息(链式;连接不存在或发送失败均静默忽略)。
public func send(message: WebSocketMessage): WebSocketConnSender {
if (let Some(c) <- server.getConn(connId)) {
try {
c.send(message)
} catch (_) {
()
}
}
this
}
/// 向指定连接发送文本(链式)。
public func send(text: String): WebSocketConnSender {
send(WebSocketMessage.fromText(text))
}
/// 向指定连接发送二进制(链式)。
public func send(data: Array<Byte>): WebSocketConnSender {
send(WebSocketMessage.fromBinary(data))
}
/// 连接所属的所有组名(快照)。
public func groups(): ArrayList<String> {
synchronized(server.connLock) {
let res = ArrayList<String>()
for ((k, v) in server.connGroups) {
if (listHasConnId(v, connId)) {
res.add(k)
}
}
res
}
}
}
+94 -96
View File
@@ -1,23 +1,23 @@
/*
* simcu::websocket.server —— 事件回调式 WebSocket 服务端(RFC 6455)。
* simcu::websocket.server —— WebSocket 服务端(RFC 6455handler 接口式
*
* 用法:
* let server = WebSocketServer(bindAt: 8080)
* server.on("connection", { (conn) =>
* println("新连接: ${conn.remoteAddress}")
* conn.on("message", { (msg) => conn.sendText("echo: ${msg.text}") })
* })
* server.listen()
* let server = WebSocketServer(8080)
* server.addHandler("/ws") { => MyHandler() } // 路径 -> 工厂,每次握手创建独立 handler 实例
* server.start()
* ...
* server.close() // 停止监听并断开所有连接
*
* 服务端事件:on('connection') / on('message') / on('close') / on('error')。
* 连接方法:send / sendText / sendBinary / ping / close / terminate
* 集群方法:broadcast / broadcastText / broadcastBinary / connectionCount
* 多 handler 按注册路径路由:握手时按请求路径匹配 addHandler 的 path,匹配则调用工厂
* 创建一个新的 handler 实例(每连接一个),无匹配返回 404
* 连接方法:send / ping / close / terminatesend 三重重载:WebSocketMessage / String / Array<Byte>
* 组管理(链式):ws.group(name).join/leave/count/isEmpty/connIdsws.conn(connId).groups()ws.groups()。
* 发送门面(链式):group(name).send / conn(connId).send(均为三重重载)。
* 广播:broadcast(三重重载)/ connectionCount。
*/
package simcu::websocket.server
import std.collection.ArrayList
import std.collection.{ArrayList, HashMap}
import std.net.IPSocketAddress
import std.net.SocketException
import std.net.TcpServerSocket
@@ -25,78 +25,37 @@ import std.net.TcpSocket
import std.sync.Mutex
import simcu::websocket.common.FrameStream
import simcu::websocket.common.MessageType
import simcu::websocket.common.WebSocketMessage
import simcu::websocket.common.WebSocketException
import simcu::websocket.common.WsEvents
/// 事件回调式 WebSocket 服务端。
/// WebSocket 服务端(按路径工厂路由:每连接创建独立 handler 实例,支持组管理/单播/组播/广播)
public class WebSocketServer {
private let bindPort: UInt16
private let pathFilter: ?String
private let maxPayload: Int64
private var pathFactories = HashMap<String, () -> IWebsocketHandler>()
private var serverSock: ?TcpServerSocket = None
private var running = false
private var connections = ArrayList<WebSocketConnection>()
private var connections = HashMap<Int64, WebSocketConnection>()
internal var connGroups = HashMap<String, ArrayList<Int64>>()
private var nextConnId: Int64 = 1
private let connLock = Mutex()
internal let connLock = Mutex()
/// 服务端事件:有连接完成握手时触发(参数为连接对象)。
public var onConnection: (WebSocketConnection) -> Unit = { conn => () }
/// 服务端事件:任意连接收到消息时触发(参数为连接对象与消息)。
public var onMessage: (WebSocketConnection, WebSocketMessage) -> Unit = { conn, msg => () }
/// 服务端事件:任意连接关闭时触发(参数为连接对象、关闭码、原因)。
public var onClose: (WebSocketConnection, Int64, String) -> Unit = { conn, code, reason => () }
/// 服务端事件:监听或连接运行时错误时触发(参数为异常)。
public var onError: (Exception) -> Unit = { e => () }
/// @param bindAt 监听端口,0 表示随机空闲端口(listen 后通过 localPort 读取)。
/// @param path 仅接受该路径的握手请求(None 表示不限制)。
/// @param port 监听端口,0 表示随机空闲端口(start 后通过 localPort 读取)。
/// @param maxPayload 单条消息最大字节数,默认 64KB。
public init(bindAt!: UInt16 = 0, path!: ?String = None, maxPayload!: Int64 = 65536) {
this.bindPort = bindAt
this.pathFilter = path
public init(port: Int64, maxPayload!: Int64 = 65536) {
this.bindPort = UInt16(port)
this.maxPayload = maxPayload
}
/// 注册服务端事件。支持 "connection"
public func on(event: String, listener: (WebSocketConnection) -> Unit): Unit {
if (event == WsEvents.connection) {
onConnection = listener
} else {
throw WebSocketException("不支持的事件: ${event}")
}
}
/// 注册服务端事件。支持 "message"。
public func on(event: String, listener: (WebSocketConnection, WebSocketMessage) -> Unit): Unit {
if (event == WsEvents.message) {
onMessage = listener
} else {
throw WebSocketException("不支持的事件: ${event}")
}
}
/// 注册服务端事件。支持 "close"。
public func on(event: String, listener: (WebSocketConnection, Int64, String) -> Unit): Unit {
if (event == WsEvents.close) {
onClose = listener
} else {
throw WebSocketException("不支持的事件: ${event}")
}
}
/// 注册服务端事件。支持 "error"。
public func on(event: String, listener: (Exception) -> Unit): Unit {
if (event == WsEvents.error) {
onError = listener
} else {
throw WebSocketException("不支持的事件: ${event}")
}
/// 注册一个 handler 工厂并绑定到端点路径(多 handler 按路径路由)
/// 每次握手成功都会调用工厂创建一个新的 handler 实例(每连接一个),
/// 因此不要在工厂里复用有状态对象(如 scoped 服务),应让各回调按需现场解析。
public func addHandler(path: String, factory: () -> IWebsocketHandler): Unit {
pathFactories[path] = factory
}
/// 启动监听:bind + accept 循环,每连接一个协程处理握手与读循环。
public func listen(backlog!: Int64 = 128): Unit {
public func start(backlog!: Int64 = 128): Unit {
if (running) {
throw WebSocketException("服务端已在监听")
}
@@ -129,7 +88,7 @@ public class WebSocketServer {
}
}
/// 停止监听并强制断开所有连接(每连接会触发自身 close 事件)。
/// 停止监听并强制断开所有连接(每连接会触发自身 close 回调)。
public func close(): Unit {
running = false
if (let Some(s) <- serverSock) {
@@ -158,33 +117,44 @@ public class WebSocketServer {
}
}
/// 广播文本消息。
public func broadcastText(text: String): Unit {
/// 广播文本消息(三重重载:WebSocketMessage / String / Array<Byte>
public func broadcast(text: String): Unit {
broadcast(WebSocketMessage.fromText(text))
}
/// 广播二进制消息。
public func broadcastBinary(data: Array<Byte>): Unit {
public func broadcast(data: Array<Byte>): Unit {
broadcast(WebSocketMessage.fromBinary(data))
}
// ===== 内部回调转发(供 WebSocketConnection 调用)=====
internal func fireMessage(conn: WebSocketConnection, msg: WebSocketMessage): Unit {
onMessage(conn, msg)
/// 所有组名(快照)。
public func groups(): ArrayList<String> {
synchronized(connLock) {
let copy = ArrayList<String>()
for ((k, _) in connGroups) {
copy.add(k)
}
copy
}
}
internal func fireClose(conn: WebSocketConnection, code: Int64, reason: String): Unit {
onClose(conn, code, reason)
// ===== 链式发送门面 =====
/// 组门面(链式): ws.group("authed").join(conn).send("hi").send(data)count/isEmpty/connIds 读组信息。
public func group(name: String): WebSocketGroupSender {
WebSocketGroupSender(this, name)
}
internal func fireError(e: Exception): Unit {
onError(e)
/// 连接门面(链式): ws.conn(connId).send("hi").send(data)groups() 查连接所属组。
public func conn(connId: Int64): WebSocketConnSender {
WebSocketConnSender(this, connId)
}
// ===== 内部连接管理 =====
internal func addConnection(conn: WebSocketConnection): Unit {
synchronized(connLock) {
connections.add(conn)
connections[conn.connId] = conn
}
}
@@ -198,23 +168,36 @@ public class WebSocketServer {
internal func removeConnection(conn: WebSocketConnection): Unit {
synchronized(connLock) {
var idx: Int64 = -1
for (i in 0..connections.size) {
if (connections[i].connId == conn.connId) {
idx = i
break
connections.remove(conn.connId)
removeFromAllGroups(conn.connId)
}
}
if (idx >= 0) {
connections.remove(at: idx)
internal func getConn(connId: Int64): ?WebSocketConnection {
synchronized(connLock) {
connections.get(connId)
}
}
private func removeFromAllGroups(connId: Int64): Unit {
var toRemove = ArrayList<String>()
for ((g, list) in connGroups) {
if (listHasConnId(list, connId)) {
removeConnId(list, connId)
if (list.size == 0) {
toRemove.add(g)
}
}
}
for (g in toRemove) {
connGroups.remove(g)
}
}
private func snapshotConnections(): ArrayList<WebSocketConnection> {
synchronized(connLock) {
let copy = ArrayList<WebSocketConnection>()
for (c in connections) {
for (c in connections.values()) {
copy.add(c)
}
copy
@@ -223,6 +206,11 @@ public class WebSocketServer {
// ===== 监听与握手 =====
/// 按请求路径匹配 handler 工厂。无匹配返回 None。
private func findFactory(path: String): ?() -> IWebsocketHandler {
pathFactories.get(path)
}
private func acceptLoop(): Unit {
while (running) {
try {
@@ -237,12 +225,12 @@ public class WebSocketServer {
if (!running) {
return
}
onError(e)
println("[websocket] accept 失败: ${e}")
} catch (e: Exception) {
if (!running) {
return
}
onError(e)
println("[websocket] accept 失败: ${e}")
}
}
}
@@ -261,12 +249,20 @@ public class WebSocketServer {
reject(sock, 400, "Bad Request")
return
}
if (let Some(p) <- pathFilter) {
if (parts[1] != p) {
// 拆出路径与 query(如 "/ws/server?serverId=srv-1" → path="/ws/server", query="serverId=srv-1"
let target = parts[1]
var requestPath = target
var requestQuery = ""
if (let Some(qi) <- target.indexOf("?")) {
requestPath = target[0..qi]
requestQuery = target[qi + 1..]
}
// 按 WsPath 路由到 handler 工厂,匹配则调用工厂创建新的 handler 实例(每连接一个)
let factory = findFactory(requestPath)
if (let None <- factory) {
reject(sock, 404, "Not Found")
return
}
}
var upgradeOk = false
var connectionOk = false
var key = ""
@@ -302,13 +298,15 @@ public class WebSocketServer {
sock.write(resp.toString().toArray())
sock.flush()
let conn = WebSocketConnection(Some(this), fs, allocateConnId())
let fac = factory.getOrThrow()
let h = fac()
let conn = WebSocketConnection(Some(this), h, fs, allocateConnId(), requestPath, requestQuery)
addConnection(conn)
onConnection(conn)
h.onConnect(conn)
conn.runReadLoop()
} catch (e: Exception) {
// 握手阶段失败:上报服务端 error 事件
onError(e)
// 握手阶段失败:记录日志(连接尚未建立,无 handler 可回调)
println("[websocket] 握手失败: ${e}")
}
}
+209 -68
View File
@@ -1,7 +1,8 @@
/*
* simcu::websocket.tests —— 端到端集成测试(cjpm test)。
* 覆盖:文本回声 / 广播 / 关闭握手(客户端主动、服务端主动)/ terminate /
* ping 心跳后连接仍可用 / 路径过滤 / maxPayload 超限(1009)
* ping 心跳后连接仍可用 / 路径路由(多 handler 按注册路径)/ maxPayload 超限(1009) /
* 组播(group.send/ 单播(conn.send)。
*
* 运行:在 websocket-cj 目录执行 cjpm test
*/
@@ -17,6 +18,7 @@ import simcu::websocket.client.WebSocketClient
import simcu::websocket.common.MessageType
import simcu::websocket.common.WebSocketException
import simcu::websocket.common.WebSocketMessage
import simcu::websocket.server.IWebsocketHandler
import simcu::websocket.server.WebSocketConnection
import simcu::websocket.server.WebSocketServer
@@ -45,27 +47,115 @@ private func waitUntil(timeoutMs: Int64, condition: () -> Bool): Bool {
condition()
}
/// 测试用 handler:把事件回调逻辑收进接口方法,行为写入 harness。
class TestWsHandler <: IWebsocketHandler {
let harness: WsTestHarness
init(harness: WsTestHarness) {
this.harness = harness
}
public override func onConnect(conn: WebSocketConnection): Unit {
harness.serverConn = Some(conn)
}
public override func onMessage(conn: WebSocketConnection, msg: WebSocketMessage): Unit {
harness.connMessage = msg.text.getOrThrow()
conn.send("echo: ${msg.text.getOrThrow()}")
}
public override func onClose(conn: WebSocketConnection, code: Int64, reason: String): Unit {
harness.connCloseCode = code
harness.connCloseReason = reason
}
public override func onError(conn: WebSocketConnection, e: Exception): Unit {
if (let we: WebSocketException <- e) {
harness.connErrorCode = we.code
}
}
}
/// 广播场景:harness1 收到广播后转发文本。
class EchoHandler <: IWebsocketHandler {
let harness: WsTestHarness
init(harness: WsTestHarness) {
this.harness = harness
}
public override func onConnect(conn: WebSocketConnection): Unit {
()
}
public override func onMessage(conn: WebSocketConnection, msg: WebSocketMessage): Unit {
harness.connMessage = msg.text.getOrThrow()
}
public override func onClose(conn: WebSocketConnection, code: Int64, reason: String): Unit {
()
}
public override func onError(conn: WebSocketConnection, e: Exception): Unit {
()
}
}
/// 服务端主动关闭场景:连接建立后立即 close。
class CloseOnConnectHandler <: IWebsocketHandler {
init() {}
public override func onConnect(conn: WebSocketConnection): Unit {
conn.close(code: 4000, reason: "svr")
}
public override func onMessage(conn: WebSocketConnection, msg: WebSocketMessage): Unit {
()
}
public override func onClose(conn: WebSocketConnection, code: Int64, reason: String): Unit {
()
}
public override func onError(conn: WebSocketConnection, e: Exception): Unit {
()
}
}
/// 分组场景:连接建立后加入 "room" 组,便于验证组播/单播。
class GroupJoinHandler <: IWebsocketHandler {
public override func onConnect(conn: WebSocketConnection): Unit {
conn.joinGroup("room")
}
public override func onMessage(conn: WebSocketConnection, msg: WebSocketMessage): Unit {
()
}
public override func onClose(conn: WebSocketConnection, code: Int64, reason: String): Unit {
()
}
public override func onError(conn: WebSocketConnection, e: Exception): Unit {
()
}
}
@Test
public class WebSocketTest {
/// 1. 文本回声:client.sendText → server 收到后回发 → client 收到
/// 1. 文本回声:client.send → server 收到后回发 → client 收到
@TestCase
public func echoText(): Unit {
let h = WsTestHarness()
let server = WebSocketServer(bindAt: 0)
server.on("connection", { conn: WebSocketConnection =>
h.serverConn = Some(conn)
conn.on("message", { m: WebSocketMessage =>
h.connMessage = m.text.getOrThrow()
conn.sendText("echo: ${m.text.getOrThrow()}")
})
})
server.listen()
let client = WebSocketClient("127.0.0.1", server.localPort)
let server = WebSocketServer(0)
server.addHandler("/ws") { => TestWsHandler(h) }
server.start()
let client = WebSocketClient("127.0.0.1", server.localPort, path: "/ws")
client.onMessage = { m: WebSocketMessage =>
h.clientMessage = m.text.getOrThrow()
}
client.connect()
client.sendText("hello")
client.send("hello")
@Expect(waitUntil(5000) { => h.connMessage == Some("hello") }, true)
@Expect(waitUntil(5000) { => h.clientMessage == Some("echo: hello") }, true)
@@ -73,27 +163,28 @@ public class WebSocketTest {
server.close()
}
/// 2. 广播:server.broadcastText 两个客户端都收到
/// 2. 广播:server.broadcast 两个客户端都收到
@TestCase
public func broadcast(): Unit {
let h1 = WsTestHarness()
let h2 = WsTestHarness()
let server = WebSocketServer(bindAt: 0)
server.listen()
let server = WebSocketServer(0)
server.addHandler("/ws") { => EchoHandler(h1) }
server.start()
let c1 = WebSocketClient("127.0.0.1", server.localPort)
let c1 = WebSocketClient("127.0.0.1", server.localPort, path: "/ws")
c1.onMessage = { m: WebSocketMessage =>
h1.clientMessage = m.text.getOrThrow()
}
c1.connect()
let c2 = WebSocketClient("127.0.0.1", server.localPort)
let c2 = WebSocketClient("127.0.0.1", server.localPort, path: "/ws")
c2.onMessage = { m: WebSocketMessage =>
h2.clientMessage = m.text.getOrThrow()
}
c2.connect()
@Expect(waitUntil(5000) { => server.connectionCount == 2 }, true)
server.broadcastText("hi-all")
server.broadcast("hi-all")
@Expect(waitUntil(5000) { => h1.clientMessage == Some("hi-all") }, true)
@Expect(waitUntil(5000) { => h2.clientMessage == Some("hi-all") }, true)
c1.close()
@@ -105,15 +196,10 @@ public class WebSocketTest {
@TestCase
public func closeHandshake(): Unit {
let h = WsTestHarness()
let server = WebSocketServer(bindAt: 0)
server.on("connection", { conn: WebSocketConnection =>
conn.on("close", { code: Int64, reason: String =>
h.connCloseCode = code
h.connCloseReason = reason
})
})
server.listen()
let client = WebSocketClient("127.0.0.1", server.localPort)
let server = WebSocketServer(0)
server.addHandler("/ws") { => TestWsHandler(h) }
server.start()
let client = WebSocketClient("127.0.0.1", server.localPort, path: "/ws")
client.onClose = { code: Int64, reason: String =>
h.clientCloseCode = code
h.clientCloseReason = reason
@@ -130,13 +216,10 @@ public class WebSocketTest {
@TestCase
public func serverClose(): Unit {
let h = WsTestHarness()
let server = WebSocketServer(bindAt: 0)
server.on("connection", { conn: WebSocketConnection =>
h.serverConn = Some(conn)
conn.close(code: 4000, reason: "svr")
})
server.listen()
let client = WebSocketClient("127.0.0.1", server.localPort)
let server = WebSocketServer(0)
server.addHandler("/ws") { => CloseOnConnectHandler() }
server.start()
let client = WebSocketClient("127.0.0.1", server.localPort, path: "/ws")
client.onClose = { code: Int64, reason: String =>
h.clientCloseCode = code
h.clientCloseReason = reason
@@ -151,15 +234,10 @@ public class WebSocketTest {
@TestCase
public func terminate(): Unit {
let h = WsTestHarness()
let server = WebSocketServer(bindAt: 0)
server.on("connection", { conn: WebSocketConnection =>
conn.on("close", { code: Int64, reason: String =>
h.connCloseCode = code
h.connCloseReason = reason
})
})
server.listen()
let client = WebSocketClient("127.0.0.1", server.localPort)
let server = WebSocketServer(0)
server.addHandler("/ws") { => TestWsHandler(h) }
server.start()
let client = WebSocketClient("127.0.0.1", server.localPort, path: "/ws")
client.connect()
client.terminate()
@@ -171,29 +249,27 @@ public class WebSocketTest {
@TestCase
public func ping(): Unit {
let h = WsTestHarness()
let server = WebSocketServer(bindAt: 0)
server.on("connection", { conn: WebSocketConnection =>
conn.on("message", { m: WebSocketMessage =>
h.connMessage = m.text.getOrThrow()
})
})
server.listen()
let client = WebSocketClient("127.0.0.1", server.localPort)
let server = WebSocketServer(0)
server.addHandler("/ws") { => TestWsHandler(h) }
server.start()
let client = WebSocketClient("127.0.0.1", server.localPort, path: "/ws")
client.connect()
client.ping()
sleep(Duration.millisecond * 100)
client.sendText("after-ping")
client.send("after-ping")
@Expect(waitUntil(5000) { => h.connMessage == Some("after-ping") }, true)
client.close()
server.close()
}
/// 7. 路径过滤:path=Some("/ws") 时 /ws 握手成功,/other 被拒(connect 抛异常)
/// 7. 路径路由:注册 /ws 与 /admin 两个 handler/ws 握手成功,/other 被拒(connect 抛异常)
@TestCase
public func pathFilter(): Unit {
let server = WebSocketServer(bindAt: 0, path: Some("/ws"))
server.listen()
public func pathRoute(): Unit {
let server = WebSocketServer(0)
server.addHandler("/ws") { => TestWsHandler(WsTestHarness()) }
server.addHandler("/admin") { => TestWsHandler(WsTestHarness()) }
server.start()
let ok = WebSocketClient("127.0.0.1", server.localPort, path: "/ws")
ok.connect()
@@ -214,24 +290,89 @@ public class WebSocketTest {
@TestCase
public func maxPayload(): Unit {
let h = WsTestHarness()
let server = WebSocketServer(bindAt: 0, maxPayload: 10)
server.on("connection", { conn: WebSocketConnection =>
conn.on("error", { e: Exception =>
if (let we: WebSocketException <- e) {
h.connErrorCode = we.code
}
})
})
server.listen()
let client = WebSocketClient("127.0.0.1", server.localPort)
let server = WebSocketServer(0, maxPayload: 10)
server.addHandler("/ws") { => TestWsHandler(h) }
server.start()
let client = WebSocketClient("127.0.0.1", server.localPort, path: "/ws")
client.onError = { e: Exception =>
h.clientErrorCount += 1
}
client.connect()
client.sendText("this message is way too long")
client.send("this message is way too long")
@Expect(waitUntil(5000) { => h.connErrorCode == 1009 }, true)
client.terminate()
server.close()
}
/// 9. 组播:两个连接加入同组("room"),group("room").send 两连接都收到;关闭一个后组计数减一
@TestCase
public func groupMulticast(): Unit {
let h1 = WsTestHarness()
let h2 = WsTestHarness()
let server = WebSocketServer(0)
server.addHandler("/ws") { => GroupJoinHandler() }
server.start()
let c1 = WebSocketClient("127.0.0.1", server.localPort, path: "/ws")
c1.onMessage = { m: WebSocketMessage =>
h1.clientMessage = m.text.getOrThrow()
}
c1.connect()
let c2 = WebSocketClient("127.0.0.1", server.localPort, path: "/ws")
c2.onMessage = { m: WebSocketMessage =>
h2.clientMessage = m.text.getOrThrow()
}
c2.connect()
@Expect(waitUntil(5000) { => server.group("room").count() == 2 }, true)
server.group("room").send("team-msg")
@Expect(waitUntil(5000) { => h1.clientMessage == Some("team-msg") }, true)
@Expect(waitUntil(5000) { => h2.clientMessage == Some("team-msg") }, true)
// 关闭一个连接后自动退组,组计数减一
c1.close()
@Expect(waitUntil(5000) { => server.group("room").count() == 1 }, true)
server.group("room").send("after-leave")
@Expect(waitUntil(5000) { => h2.clientMessage == Some("after-leave") }, true)
c2.close()
server.close()
}
/// 10. 单播:conn(connId).send 只发到指定连接Id,另一连接收不到
@TestCase
public func unicast(): Unit {
let h1 = WsTestHarness()
let h2 = WsTestHarness()
let server = WebSocketServer(0)
server.addHandler("/ws") { => GroupJoinHandler() }
server.start()
let c1 = WebSocketClient("127.0.0.1", server.localPort, path: "/ws")
c1.onMessage = { m: WebSocketMessage =>
h1.clientMessage = m.text.getOrThrow()
}
c1.connect()
let c2 = WebSocketClient("127.0.0.1", server.localPort, path: "/ws")
c2.onMessage = { m: WebSocketMessage =>
h2.clientMessage = m.text.getOrThrow()
}
c2.connect()
@Expect(waitUntil(5000) { => server.group("room").connIds().size == 2 }, true)
h1.clientMessage = None
h2.clientMessage = None
let ids = server.group("room").connIds()
let target = ids[0]
server.conn(target).send("only-you")
@Expect(waitUntil(5000) { => h1.clientMessage == Some("only-you") || h2.clientMessage == Some("only-you") }, true)
// 稍等片刻后断言:仅目标接收,另一连接未收到
sleep(Duration.millisecond * 200)
let exactlyOne = (h1.clientMessage == Some("only-you") && h2.clientMessage != Some("only-you")) ||
(h2.clientMessage == Some("only-you") && h1.clientMessage != Some("only-you"))
@Expect(exactlyOne, true)
c1.close()
c2.close()
server.close()
}
}