12 KiB
WebSocket
ws 提供通用 Go Client、Server 和单次物理连接 Session。核心使用标准 RFC 6455 WebSocket,可连接浏览器或第三方 WS 服务。hub、rpc、observe 按需引入;不预设用户、租户、设备、错误码或数据存储。
独立 Server 与 Client
server, err := ws.NewServer(ws.ServerConfig{
MaxConnections: 4096,
// 省略时允许同源和无 Origin 的非浏览器请求。
AllowedOrigins: []string{"https://console.example.com"},
})
if err != nil { return err }
mux.Handle("/ws", server.Handler(ws.Handlers{
OnMessage: func(ctx context.Context, session *ws.Session, msg ws.Message) error {
return session.Send(ctx, msg)
},
}))
// HTTP 宿主退出后、依赖释放前调用 server.Shutdown(ctx)。
client, err := ws.NewClient(ws.ClientConfig{ProxyMode: "direct"})
if err != nil { return err }
session, err := client.Dial(workCtx, "wss://service.example.com/ws", ws.Handlers{
OnMessage: func(ctx context.Context, session *ws.Session, msg ws.Message) error {
// msg.Type 是 ws.Text 或 ws.Binary;此回调中处理业务消息。
return nil
},
})
if err != nil { return err }
err = session.SendJSON(ctx, map[string]string{"message": "hello"})
// 退出前 client.Shutdown(ctx),确保回调结束后再关闭其依赖。
NewServer / NewClient 只验证并构造,无监听或拨号。Server 是 http.Handler,复用 HTTP 监听器及 TLS;客户端 TLSConfig 支持系统信任链、自定义 CA 与客户端证书。注入的证书/CA/回调对象由调用方保持并发安全且不再修改。TLS 默认正常验证,不自动跳过验证。
Server.Accept(w,r,handlers) 会写出握手成功或错误响应,成功后启动托管会话并返回。原 HTTP Handler 可以立即返回;不要保留整个 *httpx.Context、Request 或可变鉴权对象。Server 的会话生命周期由 ServerConfig.Context(默认 Background)管理,握手 context 仅用于握手。Client.Dial 的 ctx 持有会话寿命,其配置的 HandshakeTimeout 只限制握手。
握手与安全配置
ServerConfig 支持 Session、MaxConnections、HandshakeTimeout、AllowedOrigins、RequireOrigin、Subprotocols、RequireSubprotocol、EnableCompression、HandshakeRate、HandshakeBurst。连接上限包含进行中的握手。速率为每秒令牌数、burst 为突发数,零 rate 默认关闭;超过速率返回 429,连接上限/停机返回 503。HTTP 读头超时和头大小限制仍由宿主配置。
Origin 校验与 CORS 独立:
- 默认只接受与请求的真实 scheme/host/port 一致的 Origin;缺少 Origin 默认允许,便于 Go 等非浏览器客户端,设置 RequireOrigin 可拒绝。
- 显式 AllowedOrigins 是完整来源白名单,支持明确的
http:///https://和端口;不支持通配*、路径、userinfo、null 或重复 Origin。默认端口归一化。 - 不信任 Forwarded/X-Forwarded-* 来推断来源。TLS 在代理终止时,应填写实际外部 Origin 白名单。
- Origin 不是身份认证。
Prepare(ctx,*http.Request)(Handlers,error)在升级前验证业务身份,同时可为每连接创建 handlers。失败默认 401;返回*ws.RejectError{StatusCode:403}可选择 4xx。响应不包含底层错误。 - 浏览器原生 WebSocket 不能自定义 Authorization Header,业务可采用已受保护的 Cookie 或短期握手票据。若使用 Cookie,继续严格检查 Origin;避免在 URL 中放长期凭证,并配置代理访问日志脱敏。
RequireSubprotocol 要求协商成功,否则握手失败;纯 WS 默认无必选子协议。RPC 使用 go-omni.rpc.v1,两端显式声明并要求它。
Session 的收发与关闭
Session 提供 ID / Context / Done / Err / Subprotocol / Stats / Send / TrySend / SendJSON / Close / Abort / Wait。
| 操作 | 语义 |
|---|---|
Send(ctx,Message) |
有界队列入场后等待写入结果;队列满立即返回 ErrQueueFull,不无限等待空位。 |
TrySend(Message) |
只确认入队,随后仍可能写失败;用于广播、单向通知。 |
SendJSON(ctx,value) |
JSON 编码后按 Text 发送,适用于应用自己的协议。 |
Close(code,reason) |
幂等启动关闭,取消回调 context,尽力发送已接纳且未取消的出站消息,再发关闭帧;不等待自身回调。 |
Abort() |
立即关闭 socket,仍需 Wait 才能证明回调退出。 |
Wait(ctx) / Done() |
等待读、写、分发、监控及 OnClose 全部结束;Wait 返回运行错误或等待超时。 |
文本严格验证 UTF-8;二进制保持字节内容。入队复制发送 payload,Send/TrySend 返回后调用方可以复用原切片。不得在调用进行中并发修改它。接收 payload 不复用,回调可以保留,但应用自行承担保留后的内存。业务不得直接抢占底层 reader/writer。
单个会话 OnOpen、OnMessage 顺序执行,OnClose 在 I/O 和消息回调结束后执行;不同会话可并发。回调必须响应 ctx 取消,不要在回调内调用自身 Wait、Client/Server.Shutdown 或 App.Close,可调用 Session.Close 或 Server.Stop。OnOpen 中不要等待必须依赖 OnMessage 才完成的操作;RPC Call/订阅恢复应在 WaitReady 后从独立受管理任务调用。
排队时 Send ctx 取消会跳过该消息;写入期间取消可能已部分送出,此时终止物理连接。成功写出也不代表对端业务处理成功。框架不缓存离线消息、不自动重放;业务需要 ACK、去重、持久化时显式设计。Close 不是业务事务提交屏障,会取消当前业务并丢弃尚未分发的入站消息。
资源上限与心跳
SessionConfig 零字段使用默认值,负值或超过支持边界的值在构造前报错。明确关闭心跳使用 DisableHeartbeat,零时间不表示禁用。
| 配置 | 默认 |
|---|---|
| MaxMessageBytes | 1 MiB,包括解压后的消息 |
| ReadQueueSize / WriteQueueSize | 每方向 64 条 |
| ReadQueueBytes / WriteQueueBytes | 每方向 4 MiB |
| WriteTimeout | 10 秒 |
| PingInterval / PongTimeout | 30 / 60 秒 |
| CloseTimeout | 5 秒 |
队列计数包含正在执行的回调/正在写的消息;此外还需计入一条正在解码的消息、协议缓冲和应用持有的对象。实例总内存还受连接数影响。接收队列溢出关闭该连接;发送满明确报错,广播逐连接返回错误。有限队列允许持续处理控制帧,不让慢业务无限阻塞 reader。压缩默认关闭;启用后仍执行解压大小限制,但会消耗额外 CPU,应结合并发连接限额验证。
协议 Ping/Pong 由底层持续处理;浏览器自动回复 Pong。心跳监测连接活性,不证明业务健康。
Client 重连与网络设置
ClientConfig 支持 Session、HandshakeTimeout(默认 10s)、MaxConnections(默认 64)、Subprotocols/RequireSubprotocol、EnableCompression、ProxyMode/ProxyURL、TLSConfig、Headers、HeaderProvider、Prepare、Reconnect、ShouldReconnect。
Dial只拨一次。Run(ctx,url,handlers)管理一条重连链;同 Client 可以并发运行多条连接,统一限制和关闭。Reconnect.Enabled显式开启;默认 minBackoff=100ms、maxBackoff=30s、stableAfter=30s。指数退避带抖动;短暂反复断线不重置预算。MaxAttempts/MaxElapsed 可限制重试次数和时间;开启重连后两者为零表示不限。- 默认不重试 401/403、协议不兼容、证书验证错误、正常 1000 关闭;远端先发 1001(going away)、1012 等重启关闭码和临时网络故障可重试。本地主动 Close/Client.Stop 不触发重连。ShouldReconnect 可改变分类,不能绕过显式关闭和预算。
- 每次握手调用 HeaderProvider(ctx),用于刷新凭证;返回头会复制。Prepare(ctx) 可为每代创建新的 RPC Peer/handlers,禁止复用已经绑定的 Peer。
- ProxyMode 默认为 environment,可显式 direct 或 url;固定代理支持 HTTP CONNECT、socks5、socks5h。HTTPS 代理当前明确拒绝,不会降级直连。
- 自有 HTTP 包装链
httpc.Do、telemetry.Transport、resilience.Transport不直接用于 WS 升级;WS 独立处理握手、消息与连接寿命。
Client.Close / Stop 停止入场、取消握手/退避并发起会话关闭;Drain / Shutdown 等待所有 Run、握手、会话和回调结束。Server 同样提供 Stop/Drain/Shutdown。终止后对象不可重新启动。
Hub 与扩展
hub.New(Config) 提供 Add / Remove / Join / Leave / Send / SendGroup / Broadcast / Kick / Stats / Close。Session 实现其 Connection 接口,也可实现其他有界非阻塞连接适配器。组键是业务自定义字符串,Join 前由业务鉴权。
Send/Broadcast/SendGroup 返回 Result{Attempted,Queued,Errors},逐连接尝试入队,慢连接不会等待网络写出。Queued 不等于远端收到。Hub 借用连接,Close 只清注册和 watcher;Kick 才主动关闭,Session.Done 后自动清理。上限可配置连接数、组数、总成员关系、每连接组数量。
Hub 是单进程注册表。跨节点广播由业务消息总线投递到各节点的本地 Hub;此版不含分布式路由、离线存储或 exactly-once 保证。通过接口扩展时仍需保持 TrySend 有界、非阻塞、并发安全。
GoOmni 接入与停机
独立 App:
application := omni.New()
server, err := ws.NewServer(ws.ServerConfig{})
if err != nil { return err }
if err = wsomni.Mount(application, "/ws", server, handlers); err != nil { return err }
// Mount 同时登记 Server.Stop/Drain,App.OnShutdown 的资源晚于会话释放。
return application.RunContext(ctx, ":8080")
wsomni 的 import 是 git.yuntuo.hk/k2safe/go-omni/ws/omni。App 新增通用 OnStop(name,hook) / OnDrain(name,hook):HTTP 结束 → 全部 Stop → 全部 Drain → OnShutdown。复用 lifecycle 配置及 ErrResourcesRetained 规则;只在用到后台组件时创建额外 Runtime。
生成项目在 svc.Init 创建 server,Context 设为 svc.WorkContext,并登记 svc.OnStop("ws",server.Stop) / svc.OnDrain("ws",server.Drain)。项目可手写以下方法,新版生成 NewApp 自动调用:
func (c *ServiceContext) RegisterCustomRoutes(app *omni.App) error {
app.GET("/ws", wsomni.Handler(c.WebSocket, c.WebSocketHandlers))
return nil
}
不要同时把同一组件登记到 App 和 svc;依赖归属与排空屏障必须一致。自定义路由在生成路由与内建端点之后注册,重复路径由路由器拒绝,不会覆盖现有接口。重新运行 api generate 更新入口,无需修改受管文件;普通项目没有此方法时行为不变。握手成功会记录 HTTP 101,HTTP 日志只计算握手时间。
ServerConfig/ClientConfig 直接组合进项目的 Config/Default/Validate;文件参数仍采用显式 ${NAME} 引用。框架不新增 WS DSL 或自动业务 Token。
兼容范围:默认 nethttp 的 HTTP/1.1 Upgrade、标准 HTTP/1.1 反向代理、WS/WSS。自研 native 和独立实验 http2 Runner 目前不支持 WS;Mount 在配置时拒绝,普通 Handler 遇到不支持接管的 ResponseWriter 返回 501。不宣称支持 HTTP/2 extended CONNECT。切换 Runner 时仍需遵守此边界。
错误与观测
errors.Is 判断 ErrClosed、ErrNotConnected、ErrQueueFull、ErrMessageTooLarge、ErrInvalidMessage、ErrUnsupportedTransport、ErrConnectionLimit;errors.As 获取 HandshakeError.StatusCode、CloseError.Code、OpError.Op。Cause 保留诊断链,但可能包含敏感数据;默认错误文本不包含 URL、认证头、payload 或对端 reason,不要无条件递归记录 Cause。
Server.Stats 提供握手数、活跃连接、累计接受/拒绝;Client.Stats 提供活跃连接、握手、Run数、关闭状态及累计 DialAttempts、HandshakeFailures、Reconnects;Session.Stats 提供消息/字节和队列占用。observe 提供安全 logx 日志和跨会话统计快照,可由项目导出到 metrics/OTel。连接 ID 不适合作为指标标签;不要把整条长连接当成持续数小时的 HTTP Span。业务消息 tracing 由业务协议显式传播。