simcu::websocket — 仓颉 WebSocket 库(RFC 6455

事件回调式 WebSocket 客户端 + 接口式服务端,纯仓颉实现,仅依赖标准库 + stdx(无第三方库)。

  • 组织:simcu
  • 包名:websocket
  • 子包:simcu::websocket.client / simcu::websocket.server / simcu::websocket.common
  • 构建:cjpm build;测试:cjpm test10 个端到端用例全通过)

功能特性

  • 客户端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
  • wssTLSFrameStream 基于 StreamingSocket,可直接包裹 TlsSocket(已在 BotRoleHelper 实战验证)

依赖引入

在项目 cjpm.toml 中添加:

[dependencies]
"simcu::websocket" = { path = "../websocket-cj" }
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

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

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 服务。

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
}

实现示例:

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 上不再暴露散落的组管理方法:

// 组管理
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

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 包裹时,可直接使用 FrameStreamlurmix 的 BotRoleHelper 即基于它实现):

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: Boolopcode: Int641=text2=binary8=close)、payload: Array<Byte>readMessage() 返回 None 表示对端关闭/EOF;收到 close 帧时已自动回写 close 并关闭流。

wssTLS)示例

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

消息与错误码

// 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
S
Description
事件回调式 WebSocket 客户端与服务端,纯仓颉实现,无第三方依赖(仅标准库 + stdx)。
Readme
68 KiB
Languages
Cangjie 100%