hgmRoomNotify

package module
v0.2.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 7, 2026 License: Unlicense Imports: 18 Imported by: 0

README

hgmRoomNotify

基于 WebSocket 的房间状态变更通知框架。服务端维护若干"房间", 数据变更时通知所有订阅了该房间的客户端。

核心是低延迟的"变更通知 + ajax 拉取": ws 戳一下告诉客户端某个房间变了, 真实数据和可靠性由业务自己的数据库 + ajax 兜底。 在此之上, 通知里可以顺带捎上两个加速字段, 命中就省一次 ajax 往返(RTT):

  • CVersionId(≤100 字节, 服务端存当前值): 直接装得下的短状态量——如"是否在线"、typing、未读数、数据版本号—— 客户端 onChange 读到就能直接更新, 进房/重连还会自动下发当前值, 常常完全不必再 ajax
  • LiveData(默认 ≤1024 字节, 仅实时传一次): 一条一次性增量——如新聊天消息、streaming 文本片段——命中就直接用, 省掉拉取。

两者都是尽力而为的加速缓存, 不是可靠传输: 没命中(没带/超限/缓冲满/重连/重启)时退回 ajax 拉全量即可。 所以框架既不是"只能戳一下", 也不是"可靠推内容"——它是"戳一下 + 顺带捎点数据省 RTT, 拉取兜底"。

功能概览

  1. 服务端维护若干"房间"(Room), 每个房间有一个字符串 id。
  2. 客户端通过 WebSocket 连接后, 可以加入/离开房间。
  3. 服务端调用 FireChange(roomId) 时, 所有订阅了该房间的客户端会收到变更通知。
  4. 变更通知携带以下信息:
    • RoomEpoch+ChangeSeq: 框架自动维护。RoomEpoch 是房间纪元 id(房间创建时生成, 房间被重建或服务器重启都会变化), ChangeSeq 在同一 RoomEpoch 下每次 FireChange 递增; 客户端用 (RoomEpoch, ChangeSeq) 判断是否有新变化并检测房间重建/服务器重启。
    • CVersionId: 调用者自定义的版本 id, 最大 100 字节。服务端内存单值存储(只存最新一个), 可选。 和 LiveData 一样不保证总是有: 没带的 FireChange 会把它清空、服务器重启会丢失、进未变更过的房间为空。 只能当"省一次拉取"的机会主义提示, 不能当跳过全量的唯一判据, 详见 doc/accelFieldsNotReliable.md
    • LiveData: 本次变更的附加数据, 默认最大 1024 字节(可调)。服务端不存储, 仅实时传递, 可选; 网络断线重连或服务器重启会丢失。
    • 两者如何选: 当前状态量("是什么", 如在线/typing、未读数、数据版本号)用 CVersionId(存当前值, 进房/重连自动下发, 丢一次会自动收敛, 必要时 ajax 保底); 一次性增量("发生了什么", 如新消息、streaming 片段)用 LiveData(尽力而为, 丢了走 ajax 补)。详见下文工作模式
  5. 客户端断线后自动重连, 重连后自动重新加入所有之前的房间, 并收到当前版本号。
  6. 认证(可选): 服务端配置 OnAllowFn 回调, 同时处理连接级和房间级准入(ctx.RoomId=="" 为连接级)。 不配置则全部放行(等同于无认证)。客户端发送 in-band identity(类似 sessionId/token, 本模块不解析), 也可由网络层(ws cookie/url query)经 OnAcceptFn 提供网络层 sessionId。两个身份来源互相独立。
  7. 运行时撤权/踢下线: 服务端调用 conn.CloseConn(isTemp, reason)CloseConnBySessionId(sessionId, isTemp, reason)isTemp=true 客户端会重连(重连重新过 OnAllowFn, 用于撤权); isTemp=false 客户端不再重连(永久封禁/下线)。
  8. 客户端配置 OnDenyFn 处理被拒(连接级/房间级)。不配置时遇到 deny 视为对接错误(断开且不再重连, 应修复 bug)。
  9. 提供 Go 客户端和浏览器 TypeScript 客户端两套实现。

快速开始

服务端 (Go)
  1. 创建 ServerManager 实例, 配置超时参数和认证回调:

    var wsServer hgmRoomNotify.ServerManager
    wsServer.OnAcceptFn = func(ctx *hgmRoomNotify.ServerOnAccept_ctx_t) {
        // 从 ctx.R 读 cookie 做认证
        ctx.SessionId = "userId"
    }
    
  2. ServerManager 注册为 http.Handler(它实现了 ServeHTTP):

    mux := http.NewServeMux()
    mux.Handle("/ws", &wsServer)
    httpServer := httptest.NewServer(mux) // 生产环境用 http.ListenAndServe(addr, mux)
    defer httpServer.Close()
    

    也可在已有框架中对接: 拿到 http.ResponseWriter*http.Request 后调用 wsServer.ServeHTTP(w, r)

  3. 数据变更时调用 FireChange:

    wsServer.FireChange(hgmRoomNotify.RoomEvent_t{
        RoomId:     "order:12345",
        CVersionId: "v3",                          // 可选
        LiveData:   []byte(`{"Status":"paid"}`),   // 可选, 默认最大 1024 字节
    })
    

    所有订阅了 "order:12345" 这个房间的客户端都会收到通知。

浏览器客户端 (TypeScript)
  1. 创建客户端实例并设置 URL:

    import { hgmRn_Client } from "./hgmRoomNotifyBrowserTs/index.ts"
    const client = new hgmRn_Client()
    client.setUrl("/ws")  // 会自动根据当前页面协议转为 wss:// 或 ws://
    
  2. 加入房间并监听变更:

    const leaveFn = client.roomEnter("order:12345", (ev) => {
        // ev.RoomId      房间 id
        // ev.RoomEpoch   房间纪元 id(房间重建/服务器重启会变化, 通常不需要关心)
        // ev.ChangeSeq   变化序号(用于去重, 通常不需要关心)
        // ev.CVersionId  自定义版本号
        // ev.LiveData    Uint8Array|null, 附加数据
        console.log("房间变更", ev.CVersionId)
        // 这里发 ajax 请求获取最新数据...
    })
    
  3. 不再需要时离开房间:

    leaveFn()  // 取消订阅。所有房间都离开后连接会在 20 秒后自动关闭。
    
Go 客户端
  1. 创建客户端实例:

    var client hgmRoomNotify.Client
    client.SetWsDialUrl("wss://example.com/ws")
    
  2. 加入房间:

    leaveFn := client.RoomEnter("order:12345", func(ev *hgmRoomNotify.RoomOnChange_t) {
        // ev.RoomId, ev.RoomEpoch, ev.ChangeSeq, ev.CVersionId, ev.LiveData
    })
    
  3. 离开房间:

    leaveFn()
    

完整可运行的 Go 端 demo(服务端 + 客户端在一个进程里跑起来)见 example/SimpleDemo/, 运行 cd example && go run ./SimpleDemo。全部例子见示例

自定义 tls / 代理: Client.HttpClient

ClientWsDialReq_t.EnableTlsVerify 只有"完全不验证"和"走系统信任链标准验证"两档。需要别的 tls 行为 (公钥锁定、自定义 CA、双向认证)或者要走 http 代理 / 自定义 dialer 时, 给 Client.HttpClient 挂一个自己的 http.Client:

var client hgmRoomNotify.Client
client.HttpClient = &http.Client{
    Transport: &http.Transport{TLSClientConfig: myTlsConfig},
}
client.SetWsDialUrl("wss://example.com/ws")
  • nil完全以它为准, ClientWsDialReq_t.EnableTlsVerify 被忽略。
  • 生命周期归调用者: 本库只用它, 不持有也不关闭它(CloseForTest 也不动)。建一个复用即可, 要回收时自己 CloseIdleConnections()。不要在 WsDialReqFn 那种每次重连都会跑的地方造新实例, 会泄漏 transport。
  • 不用担心 HTTP/2: websocket 升级只能走 HTTP/1.1, 但 Go 的 net/http 已经内建处理了 —— 带 Connection: upgrade + Upgrade: websocket 的请求会被 Request.requiresHTTP1() 标成 onlyH1, 握手时清空 ALPN 并且不复用已缓存的 h2 连接。所以标准 *http.Transport 随便传(ForceAttemptHTTP2 开着也没事)。只有塞进只会 h2 的自定义 RoundTripper(如 x/net/http2.Transport)才会连不上。

内网自研客户端连自研服务端时最实用的用法是锁定服务端证书公钥(只认公钥, 不看 CA / 有效期 / 域名), 完整可运行例子见 example/TlsPubKeyPin/

工作模式: 通知 + 拉取

本框架的设计意图是作为"变更通知层", 配合 ajax 获取实际数据:

  1. 服务端数据变更时 → FireChange(roomId), 可选带少量 LiveData
  2. 客户端收到 onChange → 发 ajax 请求获取最新完整数据。
  3. 断线重连 → 重新加入房间 → 发现版本号变了 → ajax 取最新数据。

这种模式的好处:

  • 事件只存内存, 不需要数据库持久化事件, 没有事件存储/清理的复杂逻辑。
  • 实际业务数据的持久化由已有的数据库负责, ws 层不重复存储。
  • 性能容易优化: FireChange 只是内存操作 + 写 ws 缓冲, 不涉及 IO。
  • 慢客户端不影响其他客户端: 写缓冲满了就断开那一个连接, 不阻塞广播。
  • 不会爆内存: 每个连接独立的固定大小写缓冲(默认 64KB), LiveData 不存储。

对于 streaming 场景(如 AI streaming 输出)的经验:

  • LiveData 1024 字节放一次 200ms 间隔内的增量文本, 大多数情况够用(200ms 内 AI 产生的文本通常几十到几百字节)。
  • 偶尔一次增量超了 1024 字节, LiveData 会被丢弃, 但通知仍然发出, 前端发现 LiveData 为 null 时走 ajax 补取。
  • 断线重连本身就要 ajax 重新取完整状态, 所以这个降级路径本来就得有。
  • 不要试图通过 ws 推送完整的大块数据(如完整 streaming 内容), 否则会有阻塞/爆内存风险。 让 ws 只负责通知, 大数据走 ajax, 两条路径各司其职。

为什么是"戳一下 + 拉取"而不是"直接用 ws 推内容当可靠", 以及为什么在本库约束下这已是已知最优结构(剩下只能调参数), 见 doc/whyNotifyNotPush.md

适用场景

前提是 ws 通知 + ajax(或 http rpc)取数 两条路径配合: ws 只负责"戳一下"告诉客户端某个 room 变了, 真实数据和可靠性由业务自己的数据库 + ajax 负责。在这个前提下:

场景 是否适合
新消息提醒(告诉客户端"这个会话变了") 适合
客户端收到通知后拉取最新消息列表 适合
未读数、会话列表刷新通知 适合
typing / 在线状态这类当前状态量 适合(用 CVersionId 存当前状态, 进房/重连自动下发, 必要时 ajax 保底)
每条聊天消息可靠送达 适合(ws 通知 + ajax 兜底, 见 example/ReliableChat/; 纯靠 ws LiveData 当可靠送达不适合)
离线消息、消息历史、回放、断线补发 适合(历史存数据库 + ajax 拉取, ws 只负责戳一下, 见 example/ReliableChat/)
大规模多节点分布式通知 需要额外改造(本库是单进程房间表)。看起来有办法解决、且全广播档不用改库, 理论分析见 doc/multiNodeDistribute.md(仅理论, 未实践)
CVersionId 还是 LiveData

FireChange 可选携带 CVersionIdLiveData, 两者语义不同, 对接时容易选错:

服务端存储 进房/重连 适合什么
CVersionId(≤100 字节) 存内存当前值(只存最新一个) 进房/重连自动下发当前值 当前状态量("是什么"): 在线/typing、未读数、数据版本号
LiveData(默认 ≤1024 字节) 不存, 仅实时传一次 重连/缓冲满/超限就没了 一次性增量("发生了什么"): 新消息、streaming 文本片段

CVersionIdLiveData 一样不保证总是有(没带的 FireChange 会清空它、服务器重启会丢、进未变更过的房间为空, 且内容框架不解释、不保证单调可比)。 它是"省一次拉取"的机会主义提示, 不是可靠单调版本通道 —— 跳过全量的判据必须是"非空 + 可比 + 同 RoomEpoch + <= 本地", 其余一律回源拉全量。 把"CVersionId <= 本地 就直接跳过、连请求都不发"当唯一闸门会漏掉真实更新, 详见 doc/accelFieldsNotReliable.md

  • 状态量用 CVersionId。它是"当前完整状态", 丢一次通知没关系——下一次通知或进房/重连会带上最新值, 自动收敛到正确状态。 例如 typing / 在线状态: FireChange(RoomId, CVersionId="在线状态编码"), 客户端 onChange 直接读 ev.CVersionId 更新 UI, 不需要 ajax; 只在服务器重启(CVersionId 丢失、RoomEpoch 变化)时用 ajax 取一次当前完整状态保底。100 字节放状态编码或版本号通常够用, 不够就用 CVersionId 当版本号 + ajax 取完整列表。
  • 一次性增量用 LiveData。丢了(缓冲满/断线/超上限)就走 ajax 补。聊天消息属于这类(每条是新增内容, 不是"当前状态"), 见 example/ReliableChat/
可靠送达 / 历史 / 断线补发

本框架可以做到"聊天消息可靠送达"和"离线消息/消息历史/回放/断线补发":把 ws 当"戳一下"的低延迟通道, 把 ajax(或 http rpc)当真实数据来源, 两者配合即可。对接代码很简单, 代价仅仅是某些情况(LiveData 不够用时)多一个 RTT。

核心思路(完整可运行代码 + 自动测试见 example/ReliableChat/):

  1. 真正的可靠性锚点是业务消息序号(每个房间内单调递增, 存数据库), 不是 ws 层的 ChangeSeq (ChangeSeq 只是"戳一下"信号, 服务器重启会重置)。
  2. 服务端每来一条消息: 先写库分配序号, 再 FireChange, 把整条消息塞进 LiveData(尽力而为)。
  3. 客户端收到 onChange:
    • LiveData 有内容, 且序号正好接在本地最后一条之后 → 直接用 LiveData 应用, 0 额外 RTT(快路径, 即"直接推送每一条聊天消息")。
    • 否则(LiveData 没有/被丢弃/超上限/重连/服务器重启, 或者序号跳号说明中间漏了)→ ajax 拉取本地最后序号之后的全部消息补齐。
  4. "是不是断过线"不需要单独判断: 任何中断都会表现为"LiveData 缺失"或"序号跳号", 被上面的 ajax 路径统一兜住。 这条 ajax 路径同时就是"离线消息/历史/回放/断线补发"的实现——新客户端进房、断线重连、服务器重启后, 都靠它把缺的消息补回来。

运行例子: cd example && go run ./ReliableChat; 跑自动测试: cd example && go test ./ReliableChat。 自动测试覆盖: 逐条快路径送达、突发连发不丢消息、超大 LiveData 降级 ajax、进房回放历史、ws 重启后断线补发。

客户端状态查询

客户端提供以下方法用于查询当前运行状态, 方便调试和 UI 展示。

Go 客户端:

client.GetUiStatusToUser()          // 返回 "synced"/"syncing"/"offline"/"needManual"
client.GetSinceLastServerConfirm()  // 返回 time.Duration, 距离上次收到服务端有效消息的时间。从未收到过返回 -1。
client.GetNeedManualMsg()           // 返回 needManual 状态的原因文本。空字符串表示不在 needManual 状态。
client.GetRoomCount()               // 返回当前订阅的房间数量。
client.IsConnectedSucc()            // 返回 ws 是否已连接。
client.GetClientStatus()            // 返回 ClientStatus_t 结构体(含 Type/HasNeed/LastCloseReason/LastCloseLog/IsStopListen)。

TypeScript 客户端:

client.GetUiStatusToUser()          // 返回 "synced"/"syncing"/"offline"/"needManual"
client.GetSinceLastServerConfirm()  // 返回毫秒数。从未收到过返回 -1。
client.GetNeedManualMsg()           // 返回 needManual 状态的原因文本。空字符串表示不在 needManual 状态。
client.GetRoomCount()               // 返回当前订阅的房间数量。
client.IsConnectedSucc()            // 返回 ws 是否已连接。

前端调试面板示例:

const status = client.GetUiStatusToUser()
const sinceLast = client.GetSinceLastServerConfirm()
const manualMsg = client.GetNeedManualMsg()
debugDiv.textContent = `status=${status} lastConfirm=${sinceLast}ms rooms=${client.GetRoomCount()} needManual=${manualMsg}`

语义保证

本框架的保障是一条活性命题: 只要客户端与服务端最终都在线、且网络最终双向通畅并保持一段足够完成一次收敛的时间(此前允许任意长时间的断开、丢包、换网、服务器重启), 客户端最终一定收敛到房间的最新状态(中间变更次数会丢失 / 塌缩)。这依赖两件各自独立、缺一不可的事——每次(重)连接做一次全量重新对齐, 以及独立主动探活(不能假设"TCP 没报错就等于连接正常", 中间盒丢包 / 客户端换 IP 都会造成无报错的"静默死链")。自己实现 ws / SSE / pg NOTIFY 刷状态时同样必须做对这两件事, 详见 doc/deliveryGuarantee.md(含检查清单)。

保证:

  • 在网络正常时, 客户端最终一定能感知到房间"是否发生了变化"(通过 RoomEpoch+ChangeSeq 去重)。 即: 如果服务端 FireChange 了, 客户端一定会收到一次 onChange 回调(去重后); 如果服务端没有 FireChange, 客户端不会收到虚假的 onChange 回调。
  • 断线重连后, 客户端重新加入房间会收到当前版本号, 如果和断线前不同则触发 onChange。 所以客户端不会错过"最终状态有变化"这件事。

不保证:

  • 中间状态可能丢失。比如服务端连续 FireChange 了 3 次(v1→v2→v3), 客户端可能只收到 v3。 断线重连期间的所有中间变更都会丢失, 客户端只看到重连后的最新版本。客户端处理太慢, 中间变更也可能会丢失。
  • LiveData 不保证送达。服务端写缓冲满时丢弃, 断线时丢失, 超过上限时丢弃。 LiveData 是尽力而为的附加数据, 不是可靠传输。 注意: "不可靠"不等于"无用/该删"——它是可选的延迟优化(命中省一个 RTT + 避免 ajax 惊群), 不传时本库就是纯变化触发器, 不付任何代价。把它误当"可靠增量重放流"才是错的, 详见 doc/accelFieldsNotReliable.md
  • CVersionId 不保证总是有, 也不保证单调可比。没带 CVersionIdFireChange 会把它清空, 服务器重启会丢失, 进未变更过的房间下发为空; 框架不解释其内容、不保证它是数字或递增。 它是和 LiveData 同级的尽力而为提示, 只能当"省一次拉取"的机会主义优化, 不能当跳过全量的唯一判据(否则会漏掉真实更新)。详见 doc/accelFieldsNotReliable.md

不实现:

  • 不实现注册订阅模式(即不存储事件历史, 不支持从某个版本号开始回放)。 客户端的职责是: 收到变更通知后, 自己通过 ajax 去获取最新完整数据。 本框架只负责"戳一下"告诉客户端该去取了, 不负责传输完整业务数据。

限制

  • LiveData 默认最大 1024 字节, 可由 ServerManager.LiveDataMaxSize 调大(最大 16MB)。超过该上限的数据静默丢弃(不发送 LiveData 但通知仍然发出)。
    • LiveDataMaxSize 必须 ≤ WriteBufMaxBytes(每连接写缓冲, 默认 64KB, 最大 64MB)的 25%, 否则初始化时 panic。
    • LiveDataMaxSize/WriteBufMaxBytes/RoomEnterMaxPerConn 必须在首次调用 API 前配置好, 之后不可更改(无锁读取, _init 把默认值写回字段本身)。这三个字段 0 表示用默认值, 负值视为调用者 bug, _init 时直接 panic(不会被静默当成默认值)。
    • 超过单 frame(约 64KB)的 LiveData 自动用 roomValueMore...roomValue 分块传输。调大 WriteBufMaxBytes 时, 客户端的 ReadMsgMaxBytes 必须 ≥ 服务端 WriteBufMaxBytes(单个 websocket message 最大可达该值), 否则客户端会因消息过大断开。默认值(两端 64KB)下行为与旧版完全一致。
    • 注意: LiveData 越大, 越偏离"通知"定位, 热房间 fanout 下每条连接各缓存一份, 内存放大明显。大体积仅适合连接数少、低频的场景, 默认 1024 已覆盖绝大多数"一次性增量"需求。
  • CVersionId 最大 100 字节。超过会 panic。
  • RoomId 最大 1024 字节(服务端校验)。超过会断开连接。
  • 服务端单条协议消息(序列化后)最大 65535 字节(下层 frame 上限)。更大的 LiveData 通过分块跨多条消息传输。
  • 服务端不存储 LiveData, 客户端断线重连后不会补发之前的 LiveData
  • 房间没有历史记录, 客户端只能收到订阅后的变更。

配置参数

本 package 对外可配置的全部参数(ServerManager / Client / TimeoutCfg_t)及其默认值、上限、效果, 见 doc/config.md

协议

  • 二进制协议, 一个 WebSocket message 可以包含多个协议消息。
  • 每个协议消息格式: [uint16LE 长度][消息体]
  • 消息类型: ping(1), setTimeCfg(2), roomEnter(3), roomLeave(4), roomValue(5), identity(6), connAllow(7), deny(8), closeConn(9), roomValueMore(10)
    • roomValueMore: 服务端→客户端。LiveData 超过单 frame 上限时, 一次 roomValue 拆成 [roomValueMore...][roomValue] 分块传输(More=后面还有, 最后一片是普通 roomValue 携带元数据, 与不分块时同构)。同连接上整组分片连续到达、中间不插其它消息, 客户端把累积的 More 分片拼到最后那条 roomValue 前面重组。
    • identity: 客户端→服务端首包(opaque 凭证)。
    • connAllow: 服务端→客户端连接已批准(携带 AuthEnabled)。
    • deny: 服务端→客户端拒绝(连接/房间)。
    • closeConn: 服务端→客户端要求关闭(临时/永久)。
  • 认证流程: 客户端连上先发 identity, 服务端配置了 OnAllowFn 则等批准(connAllow)再发 roomEnter; 未配置则服务端立即发 connAllow(AuthEnabled=false), 零额外往返。
  • setTimeCfg 使用 KV 格式: [count: uint8][ [fieldId: uint8][value: int64LE] ] * countfieldIdTimeoutCfg_t 注释, 不认识的 fieldId 跳过(前向兼容)。
  • 心跳: 客户端空闲时主动发 ping, 服务端回复 ping。超时未收到数据则断线重连。
  • 服务端写缓冲满时主动断开该连接(客户端太慢, 丢弃)。

示例

所有例子在 example/ 目录下, 是一个独立的 go module(自己的 go.mod), 这样浏览器真机测试用到的 chromedp 等测试依赖不会泄漏进本库(hgmRoomNotify)的 go.mod。 例子通过 replace 指向同仓库本体源码, 始终对着当前 commit 编译。运行前先 cd hgmRoomNotify/example

例子 说明 运行 / 测试
SimpleDemo/ 最小闭环: 同进程起服务端 + Go 客户端, FireChange 通知。 go run ./SimpleDemo / go test ./SimpleDemo
ReliableChat/ ws 戳一下 + ajax 兜底实现可靠聊天: 可靠送达 / 离线消息 / 历史回放 / 断线补发(纯 Go)。 go run ./ReliableChat / go test ./ReliableChat
SoftwareUpdate/ 软件自动更新对接: 启动先 check api(可靠数据源), 已最新再用 ws + CVersionId(加速字段)实时下发新版本。演示 CVersionId 正确用法(纯 Go)。 go run ./SoftwareUpdate / go test ./SoftwareUpdate
TlsPubKeyPin/ Client.HttpClient 锁定服务端证书的公钥(只认公钥, 不看 CA / 有效期 / 域名)。含"标准验证失败 / 锁对公钥连上 / 锁错公钥连不上"三场景对比与真 tls 自动测试。 go run ./TlsPubKeyPin / go test ./TlsPubKeyPin
WebChat/ 浏览器 React 前端 + Go 后端(内存库)的可靠聊天室, 复用 ReliableChat 的可靠模式; 浏览器客户端编译前复制进前端(gitignore, 仓库不留第二份)。含真 Chrome 真机自动测试。 go run ./WebChat/WebChatRun(一条命令自动编译前端+起后端) / go test ./WebChat

设计文档

doc/ 目录下的设计与分析记录:

文档 内容
doc/deliveryGuarantee.md 最终收敛保障的精确目标, 以及任何同类系统(含 pg NOTIFY / 手写 SSE)都必须做对的两件正交的事: 每次(重)连接全量重新对齐 + 独立主动探活(应对中间盒丢包 / 换 IP 造成的无报错静默死链)。含通知级联时"短板决定整条链、hgmRoomNotify 补不回上游丢的"分析与自查清单。
doc/config.md 全部可配置参数(ServerManager / Client / TimeoutCfg_t)的默认值、上限与效果。
doc/whyNotifyNotPush.md 为什么用"ws 戳一下 + DB 拉取"而非"ws 直推内容当可靠", 以及与 Kafka 等方案的对比。
doc/accelFieldsNotReliable.md LiveDataCVersionId 是同一类加速字段(命中省一次回源, 没命中就回源), 不是可靠字段。回应两个对称误区: "LiveData 不可靠、该删" 与 "CVersionId 是可靠单调版本号、<= 本地 就能跳过全量"。
doc/hotRoomFanout.md 热房间扇出成本与合并缓冲分析(当前有意不实现合并缓冲的原因)。
doc/multiNodeDistribute.md 多节点分布式通知的理论分析(仅理论, 未实践)。

License

Unlicense(public domain)

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ObsDefaultFn func(ev *ObsEvent_t) = ObsDefaultStdoutFn

package 级别的默认观测函数. 初始值为 ObsDefaultStdoutFn. 设置为 nil 表示全局关闭默认观测.

Functions

func ObsDefaultStdoutFn

func ObsDefaultStdoutFn(ev *ObsEvent_t)

默认 stdout 输出函数. 只输出异常/正确性相关事件.

func ObsEventType_toString

func ObsEventType_toString(t ObsEventType_t) string

转换为人类可读字符串.

Types

type Client

type Client struct {
	// 第一次连接或者重连时 连接的信息.
	// 用回调是为了让调用者可以动态修改 sessionId 参数.
	WsDialReqFn func() ClientWsDialReq_t
	// 服务端拒绝(连接或房间)的处理回调. 可选.
	// 不注册时: 服务端发来 deny -> 客户端判定为对接错误(应修复bug), 断开且不再重连, 后续 RoomEnter 报错.
	// 注册后默认行为: 连接级被拒 -> 连接保持/继续重连, 但进/离房间无效果, 新 RoomEnter 本地直接回 OnDenyFn(不找服务端), 重连时复位;
	//               房间级被拒 -> 该房间留在 intent 里(随重连自动重试), 只是当前不更新数据.
	OnDenyFn   func(ev *ClientDeny_t)
	TimeoutCfg zlibSync.Var[TimeoutCfg_t]
	// 观测事件回调. nil 表示使用 ObsDefaultFn. 设置为空函数表示关闭观测.
	ObsFn func(ev *ObsEvent_t)
	// websocket message 最大读取字节数. 0表示使用默认值64KB, 负值 panic(_init 会把默认值写回本字段).
	// 必须 >= 服务端 WriteBufMaxBytes(单个 websocket message 最大可达该值), 否则会因消息过大断开.
	ReadMsgMaxBytes int
	// 发起 websocket 升级请求(含 tls 握手)用的 http.Client. 可选.
	// nil: 用本库内置的, 按 ClientWsDialReq_t.EnableTlsVerify 决定验不验证 tls 证书.
	// 非 nil: 完全以本字段为准, ClientWsDialReq_t.EnableTlsVerify 被忽略(tls 由本 http.Client 的
	//   Transport.TLSClientConfig 决定). 用途: 自定义 tls.Config(公钥锁定/自定义 CA/双向认证)、http 代理、自定义 dialer.
	// 生命周期完全由调用者负责: 本库只读取和使用它, 不持有语义, 也不关闭它 —— CloseForTest 不会动它,
	//   用完后的连接清理(CloseIdleConnections)由调用者自己做.
	// 必须在第一次 RoomEnter 之前设置好, 之后不再修改(无锁保护, 每次(重)连时直接读本字段).
	// 不用担心 HTTP/2: websocket 升级只能走 HTTP/1.1, 而 Go 的 net/http 已内建处理 —— 带
	//   "Connection: upgrade"+"Upgrade: websocket" 的请求会被 Request.requiresHTTP1() 标成 onlyH1,
	//   握手时清空 ALPN 且不复用已缓存的 h2 连接. 所以标准 *http.Transport 随便传(开着
	//   ForceAttemptHTTP2 也行). 只有塞进只会 h2 的自定义 RoundTripper(如 x/net/http2.Transport)才会废.
	HttpClient *http.Client
	// contains filtered or unexported fields
}

* 客户端连接状态分为:

  • 正常连接. (已确认正常连接上服务器)(注意,可能没有需求)
  • 无需求 (没有连接上,且没有需求)
  • 网络失败 (上次网络连接失败,并且有需求,并且当前没有连上)
  • 连接中 (上次没有连接失败,有需求,并且当前没有连上)
  • 已关闭 (调用者要求关闭本对象,后续一定不会再连接了)

func (*Client) CloseForTest

func (c *Client) CloseForTest()

func (*Client) GetClientStatus

func (c *Client) GetClientStatus() ClientStatus_t

func (*Client) GetIsStopListen

func (c *Client) GetIsStopListen() (isStop bool)

func (*Client) GetNeedManualMsg

func (c *Client) GetNeedManualMsg() string

获取 needManual 状态的原因文本. 空字符串表示不在 needManual 状态.

func (*Client) GetRoomCount

func (c *Client) GetRoomCount() int

获取当前订阅的房间数量.

func (*Client) GetSendEnterRoomMsgNum

func (c *Client) GetSendEnterRoomMsgNum() uint32

func (*Client) GetSinceLastServerConfirm

func (c *Client) GetSinceLastServerConfirm() time.Duration

获取距离上次收到服务端有效消息的时间. 从未收到过时返回 -1.

func (*Client) GetUiStatusToUser

func (c *Client) GetUiStatusToUser() UiStatusToUser_t

获取当前面向终端用户的ui状态. 判断优先级: needManual > offline > syncing > synced.

func (*Client) GetWsDialNum

func (c *Client) GetWsDialNum() uint32

func (*Client) HasNeed

func (c *Client) HasNeed() bool

func (*Client) IsConnectedSucc

func (c *Client) IsConnectedSucc() bool

func (*Client) RoomEnter

func (c *Client) RoomEnter(roomId string, onChangeFn func(ev *RoomOnChange_t)) (leaveFn func())

客户端加入房间. 本函数 不会报错,不会阻塞. 请注意不要向已经关闭的客户端发送 roomEnter, 此时该请求会被忽略.

func (*Client) SetIsStopListen

func (c *Client) SetIsStopListen(isStop bool)

func (*Client) SetWsDialUrl

func (c *Client) SetWsDialUrl(url string)

type ClientDeny_t

type ClientDeny_t struct {
	IsConn bool
	RoomId string
	Reason string
}

服务端拒绝事件, 传给 OnDenyFn. IsConn=true 表示连接级拒绝(identity 认证失败); IsConn=false 表示房间级拒绝(进入 RoomId 被拒).

type ClientStatusCtx_t

type ClientStatusCtx_t struct {
	Type              ClientStatusType_t
	LastCloseReason   CloseReason_t
	LastCloseLog      string
	CanSetCloseReason bool // 是否可以设置关闭原因(开始链接时,配置为 true,设置过第一个 关闭原因后配置为 false.)
	// contains filtered or unexported fields
}

最后关闭原因日志.

type ClientStatusType_t

type ClientStatusType_t string

表示当前客户端的状态.

const ClientStatus_closeForTest ClientStatusType_t = "closeForTest" // 测试关闭了链接.
const ClientStatus_connected ClientStatusType_t = "connected" // 客户端认为自己已经连上了,没有断开,并且当前有需求.
const ClientStatus_connectedNoNeed ClientStatusType_t = "connectedNoNeed" // 当前没有需求,链接上了 (在 ClientNoNeedIdleDur 时间内)
const ClientStatus_dialing ClientStatusType_t = "dialing" // 链接中
const ClientStatus_noConnectNoNeed ClientStatusType_t = "noConnectNoNeed" // 当前没有需求,并且没有链接(注意有需求了还能链接)
const ClientStatus_stopListen ClientStatusType_t = "stopListen" // 停止监听
const ClientStatus_waitReconnect ClientStatusType_t = "waitReconnect" // 等待重连 (在 ClientReconnectMinDur 时间内)

type ClientStatus_t

type ClientStatus_t struct {
	Type            ClientStatusType_t // 当前状态
	HasNeed         bool               // 当前是否有需求.
	LastCloseReason CloseReason_t      // 最后关闭原因.(注意当前处于链接中的时候,这两项没有)
	LastCloseLog    string             // 最后关闭时的详细信息.(注意当前处于链接中的时候,这两项没有)
	IsStopListen    bool               // 当前是否停止监听.
}

type ClientWsDialReq_t

type ClientWsDialReq_t struct {
	Url             string // like wss://10.10.10.10:5845/ws
	CookieS         string // like wssSession=abc
	EnableTlsVerify bool   // 注意默认不验证tls证书(当前使用环境下 tls 证书太难搞了)
	Identity        string // in-band identity(类似 sessionId/token). 每次(重)连后发给服务端. 可空. 本模块不解析.
}

type CloseReason_t

type CloseReason_t string
const CloseReason_clientNoNeed CloseReason_t = "clientNoNeed"
const CloseReason_closeByReadTimeout CloseReason_t = "closeByReadTimeout"
const CloseReason_dialFail CloseReason_t = "dialFail"
const CloseReason_protocolNoMatch CloseReason_t = "protocolNoMatch"
const CloseReason_readFail CloseReason_t = "readFail"
const CloseReason_serverCloseConnTemp CloseReason_t = "serverCloseConnTemp"
const CloseReason_testClose CloseReason_t = "testClose"
const CloseReason_writeFail CloseReason_t = "writeFail"

type Cmd_t

type Cmd_t = uint8
const Cmd_closeConn Cmd_t = 9 // 服务端->客户端. 要求客户端关闭连接. IsTemp+Reason.
const Cmd_connAllow Cmd_t = 7 // 服务端->客户端. 连接已批准. 携带 AuthEnabled(服务端是否启用认证).
const Cmd_deny Cmd_t = 8 // 服务端->客户端. 拒绝(连接或房间). DenyScope+RoomId+Reason.
const Cmd_identity Cmd_t = 6 // 客户端->服务端. 连接后首包. 携带 opaque identity 字符串. 重连重发.
const Cmd_ping Cmd_t = 1
const Cmd_roomEnter Cmd_t = 3
const Cmd_roomLeave Cmd_t = 4
const Cmd_roomValue Cmd_t = 5
const Cmd_roomValueMore Cmd_t = 10 // 服务端->客户端. LiveData 分片, 后面还有. LiveData(分片) 有效.

LiveData 超过单 frame 上限时, 一次 roomValue 拆成 [roomValueMore...][roomValue] 分块传输(服务端->客户端). roomValueMore 只携带一段 LiveData 分片(后面还有); 最后一段用普通 Cmd_roomValue(携带全部元数据), 与不分块时的单条 roomValue 同构. 同一连接上一次分块序列由写缓冲原子整组写入, 中间不会插入其它消息, 客户端把之前累积的 roomValueMore 分片拼到收到的 roomValue 前面即可.

const Cmd_setTimeCfg Cmd_t = 2

type DenyScope_t

type DenyScope_t = uint8

Cmd_deny 的 DenyScope 取值.

const DenyScope_conn DenyScope_t = 1 // 连接级拒绝(identity 认证失败).
const DenyScope_room DenyScope_t = 2 // 房间级拒绝(进入某房间被拒).

type Msg_t

type Msg_t struct {
	Cmd         Cmd_t
	RoomId      string
	RoomEpoch   string // 房间纪元id. 每次房间被创建时由 zlibIdGen.NewId() 生成. 用于检测房间被重建(包括服务器重启).
	ChangeSeq   uint64 // 变化序号. 同一个 RoomEpoch 下递增表示有新变化.
	CVersionId  string // 自定义版本id. 服务器内存存储该数据. 调用者用于追踪实际数据变化. 最大100字节.
	LiveData    []byte // 事件发生时的附加实时数据. 本模块不存储. 默认上限1024字节, 可由服务端 LiveDataMaxSize 调大(最大16MB). 超过单 frame 时拆分为 roomValueMore...roomValue.
	TimeoutCfg  *TimeoutCfg_t
	Identity    string      // Cmd_identity. opaque 凭证. 最大65535字节.
	AuthEnabled bool        // Cmd_connAllow. 服务端是否配置了认证(用于客户端"漏接 onDenyFn 当场告警").
	DenyScope   DenyScope_t // Cmd_deny. 拒绝的作用域.
	Reason      string      // Cmd_deny / Cmd_closeConn. 原因文本. 最大65535字节.
	IsTemp      bool        // Cmd_closeConn. true=临时(客户端应重连); false=永久(客户端不再重连).
}

协议消息. 按照Cmd选择有效字段进行序列化. Cmd_ping: 无额外字段. Cmd_setTimeCfg: TimeoutCfg 有效. Cmd_roomEnter: RoomId 有效. Cmd_roomLeave: RoomId 有效. Cmd_roomValue: RoomId, RoomEpoch, ChangeSeq, CVersionId, LiveData 有效. Cmd_identity: Identity 有效. Cmd_connAllow: AuthEnabled 有效. Cmd_deny: DenyScope, RoomId(房间级时), Reason 有效. Cmd_closeConn: IsTemp, Reason 有效.

func UnmarshalMsg

func UnmarshalMsg(data []byte) (msg Msg_t, errMsg string)

从二进制数据反序列化Msg_t. 按Cmd选择字段解析.

func (*Msg_t) BinarySize

func (msg *Msg_t) BinarySize() (size int, errMsg string)

计算序列化后的字节数, 同时校验输入合法性. errMsg 非空表示输入无法序列化.

func (*Msg_t) MarshalBinary

func (msg *Msg_t) MarshalBinary() ([]byte, string)

按Cmd选择字段的二进制序列化格式(小端法): Cmd_ping: [Cmd: uint8] Cmd_setTimeCfg: [Cmd: uint8][count: uint8][ [fieldId: uint8][value: int64LE] ] * count (fieldId见TimeoutCfg_t注释, 不认识的fieldId跳过) Cmd_roomEnter: [Cmd: uint8][RoomId: uint16LE长度 + 内容] Cmd_roomLeave: [Cmd: uint8][RoomId: uint16LE长度 + 内容] Cmd_roomValue: [Cmd: uint8][RoomId: uint16LE长度 + 内容][RoomEpoch: uint8长度 + 内容][ChangeSeq: uvarint][CVersionId: uint8长度 + 内容][LiveData: uint16LE长度 + 内容] Cmd_roomValueMore: [Cmd: uint8][LiveData分片: uint16LE长度 + 内容] Cmd_identity: [Cmd: uint8][Identity: uint16LE长度 + 内容] Cmd_connAllow: [Cmd: uint8][AuthEnabled: uint8] Cmd_deny: [Cmd: uint8][DenyScope: uint8][RoomId: uint16LE长度 + 内容][Reason: uint16LE长度 + 内容] Cmd_closeConn: [Cmd: uint8][IsTemp: uint8][Reason: uint16LE长度 + 内容]

func (*Msg_t) MarshalBinaryInto

func (msg *Msg_t) MarshalBinaryInto(buf []byte)

将消息序列化写入预分配的字节切片. 调用者保证 len(buf) >= BinarySize().

func (*Msg_t) MarshalBinaryTo

func (msg *Msg_t) MarshalBinaryTo(w *zlibBytes.BufWriter)

将消息序列化写入 BufWriter. 格式与 MarshalBinaryInto 一致.

type ObsEventType_t

type ObsEventType_t = uint8
const ObsEventType_clientConnClose ObsEventType_t = 25
const ObsEventType_clientConnConnected ObsEventType_t = 21
const ObsEventType_clientConnDialFail ObsEventType_t = 22
const ObsEventType_clientConnDialing ObsEventType_t = 20

客户端事件 20-39

const ObsEventType_clientConnNoNeed ObsEventType_t = 24
const ObsEventType_clientConnWaitReconnect ObsEventType_t = 23
const ObsEventType_clientNeedManual ObsEventType_t = 27
const ObsEventType_clientServerCloseConn ObsEventType_t = 26
const ObsEventType_serverAuthTimeout ObsEventType_t = 8
const ObsEventType_serverCloseConnTimeout ObsEventType_t = 9
const ObsEventType_serverConnAccept ObsEventType_t = 1

服务端事件 1-19

const ObsEventType_serverConnClose ObsEventType_t = 2
const ObsEventType_serverFireChange ObsEventType_t = 3
const ObsEventType_serverLiveDataDropped ObsEventType_t = 4
const ObsEventType_serverMsgTooLarge ObsEventType_t = 11
const ObsEventType_serverProtocolError ObsEventType_t = 10
const ObsEventType_serverRoomOverLimit ObsEventType_t = 6
const ObsEventType_serverUnknownCmd ObsEventType_t = 7
const ObsEventType_serverWriteBufFull ObsEventType_t = 5

type ObsEvent_t

type ObsEvent_t struct {
	Type          ObsEventType_t
	RemoteAddr    string // serverConnAccept/serverConnClose/serverWriteBufFull 等
	SessionId     string // serverConnAccept/serverConnClose 等
	RoomId        string // serverFireChange/serverLiveDataDropped/serverWriteBufFull 等
	CloseReason   string // serverConnClose/clientConnClose 时有效
	CloseDetail   string // serverConnClose/clientConnDialFail/clientConnClose 时有效
	NotifiedCount int    // serverFireChange 时有效
	LiveDataLen   int    // serverLiveDataDropped 时有效
}

观测事件. 使用 sync.Pool 复用, 回调内有效, 离开回调后字段值不保证. 如果回调内需要异步处理, 必须自行复制需要的字段.

func (*ObsEvent_t) Reset

func (ev *ObsEvent_t) Reset()

type RoomEvent_t

type RoomEvent_t struct {
	RoomId     string
	LiveData   []byte // 可选. 调用者负责序列化. 超过 ServerManager.LiveDataMaxSize(默认1024) 则该次静默忽略(发 obs). 超过单 frame 时自动分块传输.
	CVersionId string // 可选. 自定义版本id. 最大100字节,超过panic. 用于调用者追踪实际数据变化.
}

FireChange的输入参数.

type RoomOnChange_t

type RoomOnChange_t struct {
	RoomId     string // 房间id.
	RoomEpoch  string // 房间纪元id. 每次房间被创建时生成. 不同的 RoomEpoch 表示房间被重建过(包括服务器重启).
	ChangeSeq  uint64 // 变化序号. 同一个 RoomEpoch 下递增表示有新变化.
	CVersionId string // 自定义版本id. 服务器内存存储该数据. 调用者用于追踪实际数据变化. 最大100字节.
	LiveData   []byte // hgmBjson编码的实时数据. 长度为0表示本次没有传输. 只读,多个listener共享同一底层数组.
	ErrMsg     string // 回调中设置此字段表示报错. 非空时客户端进入 needManual 状态.
}

房间变化事件,传递给RoomEnter的回调.

type ServerConn_t

type ServerConn_t struct {
	SessionIdNet   string // 网络层 sessionId(来自 OnAcceptFn, 如 ws cookie/url query). 可能为空.
	IdentityInBand string // 客户端 in-band 发来的 identity(Cmd_identity). 可能为空.
	Userdata       any    // 调用者可读写. 用于跨回调传递数据(如连接级解析出的角色, 房间级复用).
	// contains filtered or unexported fields
}

暴露给 OnAllowFn 的连接对象. 同一连接的多次回调拿到的是同一个对象(指针), 因此 Userdata 可以在连接级回调里设置, 在后续房间级回调里读取(跨调用传递数据).

func (*ServerConn_t) CloseConn

func (c *ServerConn_t) CloseConn(isTemp bool, reason string)

要求客户端关闭当前连接. isTemp=true 客户端应重连(重连会重新认证); isTemp=false 客户端不再重连. reason 为原因文本. 用于禁用某 sessionId / 把客户端从某房间踢出(踢出靠重连时重新过 OnAllowFn).

type ServerManager

type ServerManager struct {
	TimeoutCfg zlibSync.Var[TimeoutCfg_t]
	// 接受请求中间件. 用于从网络层(ws cookie/url query)读取网络层 sessionId(写入 ctx.SessionId),
	// 或直接终止请求(ctx.IsStop). 注意这是"网络层身份", 与客户端 in-band 发来的 identity 互相独立.
	OnAcceptFn func(ctx *ServerOnAccept_ctx_t)
	// 认证回调. 可选. nil 表示不认证(连接和进房全部放行, 和没有认证系统一样).
	// 同一个回调同时处理连接级(ctx.RoomId=="")和房间级(ctx.RoomId!="")认证, 由 ctx 区分.
	// 回调内不显式 ctx.Deny() 即视为允许. 回调是同步的, 可以阻塞(查库/调权限服务),
	// 阻塞期间该连接的 keepalive 与其它房间数据照常流动(回调跑在每条连接独立的 cmd goroutine 上, 不在读循环上).
	OnAllowFn func(ctx *ServerOnAllow_ctx_t)
	// 观测事件回调. nil 表示使用 ObsDefaultFn. 设置为空函数表示关闭观测.
	ObsFn func(ev *ObsEvent_t)
	// 单连接最大房间数. 0表示使用默认值1024, 负值 _init 时 panic. 超过上限会给客户端报错并断开连接.
	RoomEnterMaxPerConn int
	// 写入缓冲最大字节数(每条连接). 0表示使用默认值64KB, 最大可配置 64MB(负值或超限 _init 时 panic). 调用者发现缓冲满了服务端主动断开连接.
	// 注意: 调大后客户端的 ReadMsgMaxBytes 必须 >= 本值(单个 websocket message 最大可达本值), 否则客户端会因消息过大断开.
	// 本字段必须在首次调用 API(ServeHTTP/FireChange) 之前配置好, 之后不再更改(并发安全靠此文档约束, 不加锁/atomic; _init 会把默认值写回本字段).
	WriteBufMaxBytes int
	// 单次 LiveData 最大字节数. 0表示使用默认值1024, 最大可配置 16MB(负值或超限 _init 时 panic). 超过则该次 FireChange 的 LiveData 被静默丢弃(发 obs).
	// 必须 <= WriteBufMaxBytes 的 25%, 否则 _init 时 panic. 超过单 frame 上限时自动用 roomValueMore...roomValue 分块传输.
	// 本字段必须在首次调用 API(ServeHTTP/FireChange) 之前配置好, 之后不再更改(并发安全靠此文档约束, 不加锁/atomic; _init 会把默认值写回本字段).
	LiveDataMaxSize int
	// contains filtered or unexported fields
}

func (*ServerManager) Close

func (s *ServerManager) Close()

关闭 ServerManager, 停止后台定时器. 只能调用一次.

func (*ServerManager) CloseConnBySessionId

func (s *ServerManager) CloseConnBySessionId(sessionId string, isTemp bool, reason string)

按 sessionId(网络层 sessionId 或 in-band identity)找到所有连接, 要求它们关闭. isTemp=true 客户端重连(重连重新过 OnAllowFn, 用于踢出/撤权); isTemp=false 客户端不再重连(永久封禁).

func (*ServerManager) FireChange

func (s *ServerManager) FireChange(ev RoomEvent_t)

有个房间发生了变化. 房间不存在时自动创建.

func (*ServerManager) GetSnapshot

func (s *ServerManager) GetSnapshot() ServerSnapshot_t

查询服务端当前状态快照.

func (*ServerManager) ServeHTTP

func (s *ServerManager) ServeHTTP(w http.ResponseWriter, r *http.Request)

type ServerOnAccept_ctx_t

type ServerOnAccept_ctx_t struct {
	W         http.ResponseWriter
	R         *http.Request
	IsStop    bool   // 调用者已经该请求,终止框架处理.
	SessionId string // 网络层 sessionId. 与客户端 in-band identity 互相独立.
}

type ServerOnAllow_ctx_t

type ServerOnAllow_ctx_t struct {
	Conn   *ServerConn_t
	RoomId string
	// contains filtered or unexported fields
}

OnAllowFn 的回调参数. RoomId=="" 表示连接级认证(identity); RoomId!="" 表示房间级认证(进入该房间). 回调内不调用 Deny 即视为允许.

func (*ServerOnAllow_ctx_t) Deny

func (ctx *ServerOnAllow_ctx_t) Deny(reason string)

拒绝本次(连接或房间). reason 为原因文本, 会下发给客户端.

type ServerSnapshot_t

type ServerSnapshot_t struct {
	ConnCount int // 当前活跃连接数
	RoomCount int // 当前房间数
}

服务端当前状态快照.

type TimeoutCfg_t

type TimeoutCfg_t struct {
	ClientReconnectMinDur         time.Duration // fieldId=1. 客户端 最小重连时间间隔。比如 1秒。
	ClientIdleToSendKeepAliveDur  time.Duration // fieldId=2. 客户端 网络idle(最后发包时间/最后收包时间的较小值)到 发送keep alive 的时间间隔。比如1秒。
	ClientLastReadToReconnectDur  time.Duration // fieldId=3. 客户端 最后收包时间 到关闭连接 开始重连的时间间隔。比如 5秒。
	ClientWsDialTimeoutDur        time.Duration // fieldId=4. 客户端 websocket.Dial 这个步骤的 最大等待时间间隔。比如 2秒。 (注意 这个步骤有个tcp.Dial+http req/resp ws最少:2rtt, wss最少:3rtt)
	ClientNoNeedIdleDur           time.Duration // fieldId=5. 客户端没有需求,到 关闭连接的超时.
	ClientLastReadToUiNoWorkDur   time.Duration // fieldId=6. 客户端 最后收包时间 超过此值 ui状态显示为离线. 默认10秒.
	ServerLastReadToCloseDur      time.Duration // 不参与二进制序列化(仅服务端使用). 服务端 最后收包时间 到关闭连接 的时间间隔。比如 2分钟。
	ServerAuthTimeoutDur          time.Duration // 不参与二进制序列化(仅服务端使用). 服务端 未认证连接的超时关闭时间间隔。比如 30秒。
	ServerAskStopListenTimeoutDur time.Duration // 不参与二进制序列化(仅服务端使用). 服务端 发送askStopListen后等待客户端断开的超时时间间隔。比如 30秒。
}

func (*TimeoutCfg_t) InitWithDefault

func (tc *TimeoutCfg_t) InitWithDefault()

type UiStatusToUser_t

type UiStatusToUser_t = string

面向终端用户的ui状态. 用于在界面上展示当前数据同步状态. "现在距离上次收到服务端确认连接有效的时间" 是客户端对象上的跨连接字段(lastReadSuccTimeAll), 收到任意一个能证明当前ws应用层链路活着的服务端入站消息时更新.

const UiStatusToUser_needManual UiStatusToUser_t = "needManual"

需要手动: 存在无法自动恢复的问题, 需要用户手动介入(比如刷新页面). 触发条件: 开发者传入的 onChangeFn 回调报错(通过 ErrMsg 或 panic/throw), 或 isStopListen 为 true(服务端发送 Cmd_askStopListen 或外部调用 SetIsStopListen(true)).

const UiStatusToUser_offline UiStatusToUser_t = "offline"

离线: 距离上次收到服务端有效消息已超过 ClientLastReadToUiNoWorkDur. 网络大概率不正常, 用户应检查网络. 网络恢复后可自动回到已同步.

const UiStatusToUser_synced UiStatusToUser_t = "synced"

已同步: 最近收到服务端有效消息(在 ClientLastReadToReconnectDur 内), 且当前没有房间变化回调正在运行, 且ws连接正常. 表示数据是最新的.

const UiStatusToUser_syncing UiStatusToUser_t = "syncing"

同步中: 最近收到服务端有效消息(在 ClientLastReadToUiNoWorkDur 内), 但存在以下任一情况: 距离上次收到有效消息已超过 ClientLastReadToReconnectDur, 或当前有房间变化回调正在运行, 或ws连接不正常(断开/重连中). 预期网络大概率正常, 可以自动恢复到已同步.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL