Documentation
¶
Overview ¶
Package realtime 基于 websocketx 提供连接身份、独立业务状态和 JSON/PB 双向消息。 Router 按上行 scene/action 分发请求,业务处理器负责解码正文和回复。
构造与消息处理 ¶
业务消息由业务项目的 .proto 定义。下例使用标准 protobuf 消息展示公开 API, 需导入 context、net/http、realtime 和 wrapperspb;代码放在返回 error 的函数中:
register := func(r *realtime.Router) error {
return r.Handle("home", "hello", func(ctx context.Context, session *realtime.Session, message *realtime.Message) error {
var request wrapperspb.StringValue
if err := message.Decode(&request); err != nil {
return session.ReplyError(ctx, message, "request.error", "INVALID_ARGUMENT", "请求参数错误")
}
return session.Reply(ctx, message, "home.greeting", wrapperspb.String("hello "+request.Value))
})
}
server, err := realtime.NewServer(realtime.Config{
Authenticate: func(ctx context.Context, r *http.Request) (realtime.ClientInfo, error) {
// 示例显式允许匿名;生产业务应校验凭据并返回用户、设备等可信信息。
return realtime.ClientInfo{}, nil
},
RegisterHandlers: register,
})
if err != nil {
return err
}
// Server 同时作为 HTTP Handler 和常驻服务;两者的生命周期分别管理。
mux := http.NewServeMux()
mux.Handle("GET /ws", server)
// 应用运行 server.Run,并由 HTTP 服务监听 mux;Ready 为 true 后接受连接。
// 使用 bootstrap 时,显式将 Server 的生命周期方法填入 apprun.Service,与 HTTP 服务分别登记。
NewServer 调用 RegisterHandlers,在构造阶段封闭路由表。每个服务拥有独立的注册清单。 Handle 精确匹配 scene 和 action,注册函数应返回非法标识、空处理器或重复注册产生的错误。 默认未命中回复 request.error / NOT_FOUND,可用 Config.OnNotFound 定制。 Authenticate 和 RegisterHandlers 必填。NewServer 只构造,Run 一次并阻塞到真实收尾。 Run 前或 Stop 后拒绝握手。HTTP 关闭不自动关闭已升级连接。 独立端口可用 bootstrap.NewWebSocketService(name) 一并管理 HTTP 与 realtime, 先登记并初始化命名 Server,调用方不得再单独运行或登记内部 Server;各端使用不同名称和配置文件。 关闭时 Stop 全部服务,再以同一总期限 Wait;失败时 Cancel,共享资源随后回收,日志最后关闭。 Stop 不取消已经接纳的业务,Wait 超时只结束等待,Done 只在回调和网络实际结束后关闭。
协议 ¶
握手提供 SubprotocolJSON 或 SubprotocolProtobuf,分别使用 Text/Binary; 同时提供时优先 JSON。无协议或无交集拒绝升级,同连接上下行编码一致且不可切换。 JSON/PB 的 kind 共用 protocol.Kind 枚举值:请求为 1、响应为 2、事件为 3; JSON 只接受整数,不再接受字符串 request/response/event、枚举名称或数字字符串。 Outgoing.Kind 直接使用 protocol.Kind,业务发送使用 protocol.Kind_RESPONSE、protocol.Kind_EVENT。 上行以 scene/action 路由;下行不携带这两个字段,使用业务显式指定的 type 标识消息类型。 响应、错误响应和事件均必填 type;响应只回显 requestId,事件不带 requestId。 JSON 请求示例,data 是 wrapperspb.StringValue 的 ProtoJSON 标量:
{"kind":1,"requestId":"r1","scene":"home","action":"hello","data":"world"}
PB 使用 protocol.Envelope,data 为业务 proto.Marshal 字节。JSON 信封的 data 直接嵌入 ProtoJSON 值,不能将整个 protobuf 正文改为 base64;业务 bytes 字段本身仍 遵循 base64、int64/uint64 字段遵循字符串等标准 ProtoJSON 规则。业务 JSON 未知字段报错。 requestId、scene、action、type 各为 1–128 字节,允许 ASCII 字母/数字/点/下划线/短横线。 JSON 信封使用 encoding/json.Unmarshal,字段名大小写兼容、重复字段和未知字段沿用标准库行为。 仍校验请求方向、关联及路由标识、data 存在且无 error(含 null),拒绝格式错误、尾随内容和必要字段类型错误。 文本帧的 UTF-8 和大小由 websocketx 校验,信封层不重复检查 UTF-8 或扫描正文容器深度。 PB 请求信封拒绝顶层 group 和 error 字段,跳过普通未知字段。错误帧类型关闭 1003,非法信封关闭 1007。 信封的 PB 解码与 Message.Decode 的 ProtoJSON/PB 解码均沿用 protobuf 默认递归保护,具体上限由依赖版本决定。 PB 正文中的未知 group 沿用 protobuf 自身的跳过规则。 Send/Reply 按完整帧检查 MaxSendBytes,超长返回 websocketx.ErrMessageTooLarge。 发送不额外限制 JSON 正文容器深度,接收方在解码时执行自己的递归限制。 业务 Decode 错误由 Handler 显式处理;普通 Handler error 只脱敏观察,不自动回复或断连。 panic/超时由 websocketx 处理。ReplyError 内容由业务授权公开,不应传入内部错误原文。
Message 和其正文只在业务 Handler 回调期间有效,每次 Decode 使用独立可写对象。 Reply/ReplyError 由业务传入消息类型,不从路由或 protobuf 类型名推导;检查所属 Session, 并在入队时检查原请求与调用方的 ctx;回调结束或超时后拒绝迟到回复。 活跃请求回复失败时,队列或共享预算不足发起关闭 1013,回复编码或大小错误发起关闭 1011。 原始错误返回给调用方。取消、过期、跨会话或已结束的请求沿用自身的错误与关闭语义。 每个请求的回复次数由业务保证,库不维护 pending 表、响应去重或远端任务句柄。
身份与业务状态 ¶
ClientInfo 在认证后固定,Info 返回值副本;其中 Value 的深层对象必须保持不可变, 不能携带原始凭据、HTTP 请求、Gin Context 或可变共享状态。身份变化通过重新连接完成。 同用户/设备可有多个独立连接,ID 是连接 ID,不是 DeviceID,不自动踢出或覆盖。
NewState 为每个连接返回独立的业务对象,State 返回对象本身,不做深拷贝或自动加锁。 工厂仅做快速内存初始化;业务自定义结构负责并发安全,可在 OnClose 中释放资源。 OnOpen 与业务 Handler 同连接串行,Revalidate 基于固定身份并发执行。 OnClose 等待网络和在途回调完成,OnOpen 失败也会清理已发布的业务会话; 初始化前断开或 NewState panic 时可能不创建业务会话,因此不调用业务 OnClose。 清理 I/O 使用 OnClose 的 ctx,不使用已取消的 Session.Context;它独立于断连取消, 受 Transport.OnCloseTimeout(默认 5 秒)及 Server.Cancel 约束,Stop/Wait 超时不取消它。 回调必须配合取消,Session.Done/Server.Done 等待实际返回;停收期间清理失败计入 ErrDrainFailed。 每个业务回调使用自己的 ctx;不可同步等待自身或所属 Server.Done。
命名实例 ¶
应用可以通过 bootstrap.MustRegisterWebSocket(name, factory) 声明实例, MustBaseInit 在共享基础设施就绪后构造并登记。之后以 NewWebSocketService(name) 接管运行和关闭,业务通过 realtime.Named(name) 获取同一 Server:
server := realtime.Named("app")
result, err := server.PushToUser(ctx, userID, realtime.PushMessage{
Type: "home.notice",
Data: wrapperspb.String("updated"),
})
自行装配时也可通过 Register(name, server) 登记全新 Server,并显式管理其生命周期。 注册只保存引用,不启动任务、不关闭实例、不自动替换;名称未知时 Named panic。 需要处理实例未登记的情况时可使用 Lookup(name),检查返回的布尔值。 关闭后 Named 仍返回原对象,其投递方法返回关闭错误。初始化完成后只查询,不重新登记。
按端投递事件 ¶
持有 Server 的调用方可按用户、设备或整个端投递,只影响本实例上的连接。 例如业务消费者从外部事件取出 userID 后调用:
result, err := server.PushToUser(ctx, userID, realtime.PushMessage{
Type: "home.notice",
Data: wrapperspb.String("updated"),
})
同一服务的其他节点由调用方分别触发相同投递。Broadcast 和 PushToDevice 的消息参数相同。 PushMessage.Type 必填,Data 与 Error 必须且只能给一个,ExpiresAt 为绝对截止时间。 客户端接收 kind=3,携带 type 和 data 或 error。目标身份与有效期仅供投递端使用。 OnOpen 成功才加入索引,断开时摘除。多个标签页及设备各自占一个命中。 若 OnOpen 中发送建连通知,客户端可等本连接的首个业务请求收到响应后,再触发定向推送。 返回值统计 Matched、Enqueued 与 Failed。零命中返回零计数及 nil;部分失败时同时返回首个错误, 其他连接继续发送。调用方可据此观察过载,不应整批重试已部分入队的事件。 调用期间不得并发修改消息及其正文。Stop 封闭新投递,Wait 包含已接纳的投递及连接实际排空。
跨节点推送命令 ¶
发布端与订阅端使用同一份 UTF-8 JSON。命令上限 1 MiB,kind 为 broadcast 时须省略 id, user/device 则需要非空白 id。message.data 按 ProtoJSON 表示,也可改用 message.error。 expiresAt 可省略,填写时使用 RFC3339 绝对时间。示例在线上表示为:
{"kind":"user","id":"u42","message":{"type":"home.notice","data":"updated"}}
发布端可以将以下字节交给自己的消息系统:
data, err := realtime.MarshalPushCommand(realtime.PushCommand{
Kind: realtime.PushUser,
ID: "u42",
Message: realtime.PushMessage{
Type: "home.notice",
Data: wrapperspb.String("updated"),
},
})
消费端调用 UnmarshalPushCommand(data, newData)。newData 按业务 type 提供独立的可写 proto, 例如 home.notice 返回 new(wrapperspb.StringValue)。仅有 error 时无需提供构造函数。 已知事件类型的 data 会忽略本地 proto 未定义的字段,投递时只携带已识别的字段。 调用方按命令 Kind/ID 调用本端 Server 的投递方法,Server 再编码为各连接协商的 JSON/PB。
当前会话下发与容量 ¶
业务持有当前 Session 时可以直接下发事件,需导入 protocol;以下代码放在返回 error 的函数中:
return session.Send(ctx, realtime.Outgoing{
Kind: protocol.Kind_EVENT,
Type: "home.changed",
Data: wrapperspb.String("new value"),
})
Send 并发安全,调用内完成编码,返回后可复用输入;调用期间不得修改输入。 成功表示本地队列接纳,不代表网络写完或客户端收到;不自动重试。 队列满、共享预算不足、过期、关闭等错误保留 websocketx 的 errors.Is 语义。 ExpiresAt 可限制消息入队和实际写出的有效期,ctx 在入队后取消不会撤回消息。 Transport.ReadLimit/MaxSendBytes 零值各默认 64 KiB,按含信封的完整消息计量; 其他心跳、连接数、队列和共享 Budget 默认值沿用 websocketx。 Transport 的接纳门、认证、子协议与生命周期回调由 Server 管理,误填返回构造错误。 Observe/Logger 及容量参数可以注入。Server 不关闭借用的日志或业务共享资源。
Index ¶
- Constants
- Variables
- func MarshalPushCommand(cmd PushCommand) ([]byte, error)
- func Register(name string, server *Server) error
- type ClientInfo
- type Config
- type Error
- type Handler
- type Message
- type Outgoing
- type PushCommand
- type PushKind
- type PushMessage
- type PushResult
- type Router
- type Server
- func (s *Server) Broadcast(ctx context.Context, msg PushMessage) (PushResult, error)
- func (s *Server) Cancel()
- func (s *Server) Done() <-chan struct{}
- func (s *Server) PushToDevice(ctx context.Context, deviceID string, msg PushMessage) (PushResult, error)
- func (s *Server) PushToUser(ctx context.Context, userID string, msg PushMessage) (PushResult, error)
- func (s *Server) Ready() bool
- func (s *Server) Run() error
- func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)
- func (s *Server) Stop() error
- func (s *Server) Wait(ctx context.Context) error
- type Session
- func (s *Session) Close(code int, reason string) (bool, error)
- func (s *Session) Context() context.Context
- func (s *Session) Done() <-chan struct{}
- func (s *Session) ID() string
- func (s *Session) Info() ClientInfo
- func (s *Session) RemoteAddr() string
- func (s *Session) Reply(ctx context.Context, request *Message, messageType string, data proto.Message) error
- func (s *Session) ReplyError(ctx context.Context, request *Message, messageType, code, message string) error
- func (s *Session) Send(ctx context.Context, message Outgoing) error
- func (s *Session) State() any
- func (s *Session) Subprotocol() string
Constants ¶
const ( SubprotocolJSON = "pixiu.realtime.v1.json" SubprotocolProtobuf = "pixiu.realtime.v1.protobuf" )
Variables ¶
var ( ErrStarted = errors.New("realtime: already started") ErrInvalidMessage = errors.New("realtime: invalid message") )
Functions ¶
func MarshalPushCommand ¶
func MarshalPushCommand(cmd PushCommand) ([]byte, error)
MarshalPushCommand 将命令编码为最多 1 MiB 的 JSON。data 直接嵌入业务 ProtoJSON 值。
Types ¶
type ClientInfo ¶
type ClientInfo struct {
// UserID 标识业务用户,同时作为传输层的 Identity.Subject。
UserID string
// DeviceID 标识客户端设备。同一设备的多条连接各有独立的 Session.ID()。
DeviceID string
// Value 可保存业务定义的不可变信息,不得保存凭据、请求或可变共享状态。
Value any
}
ClientInfo 由 Authenticate 在握手时返回,身份在本次连接期间保持不变。 空标识不代表通配或已认证用户。
type Config ¶
type Config struct {
// Transport 提供容量、心跳、Observe、Logger 等参数。
// 子协议、接纳门、认证及生命周期回调由 Server 持有,不能在此重复配置。
// ReadLimit/MaxSendBytes 零值各为 64 KiB,其他零值沿用 websocketx。
Transport websocketx.Config
// Authenticate 必填;业务允许匿名时需显式返回空身份。
Authenticate func(context.Context, *http.Request) (ClientInfo, error)
// NewState 仅做快速内存初始化,每个连接返回独立的业务状态。
// State 中的并发访问和资源释放由业务负责,库不做深拷贝。
NewState func(ClientInfo) any
OnOpen func(context.Context, *Session) error
// RegisterHandlers 必填。NewServer 同步调用一次并封闭路由表,注册函数应返回 Handle 的错误。
RegisterHandlers func(*Router) error
// OnNotFound 可覆盖未知 scene/action 的处理。默认回复 request.error,错误码为 NOT_FOUND。
OnNotFound Handler
// OnClose 等待该连接在途回调结束后执行,可释放业务状态。
// ctx 独立于 Session.Context 的断连取消,受 Transport.OnCloseTimeout(默认 5 秒)及 Server.Cancel 约束。
// 回调须响应取消;实际返回前 Session.Done/Server.Done 不关闭。
OnClose func(context.Context, *Session)
// Revalidate 复核不可变身份,可与 OnOpen 及业务 Handler 并发。
Revalidate func(context.Context, ClientInfo) error
}
Config 通过代码注入业务行为,运行后不得修改所引用的共享对象。
type Message ¶
type Message struct {
// contains filtered or unexported fields
}
Message 保存已验证信封和未解码业务正文,仅在业务 Handler 执行期间有效。
type Outgoing ¶
type Outgoing struct {
Kind protocol.Kind
RequestID string
Type string
Data proto.Message
Error *Error
// ExpiresAt 为零时不过期,非零时在入队和实际写出前检查。
ExpiresAt time.Time
}
Outgoing 是下发消息,只允许 protocol.Kind_RESPONSE、protocol.Kind_EVENT。 Type 必须由业务显式指定,用于客户端分发和选择正文解码类型,不使用上行路由字段。 响应和事件的 Data/Error 必须且只能指定一个。 响应必须携带 RequestID,事件不携带。调用期间不得修改消息及其正文。
type PushCommand ¶
type PushCommand struct {
Kind PushKind
ID string
Message PushMessage
}
PushCommand 是跨节点事件。使用 MarshalPushCommand 和 UnmarshalPushCommand 读写总线 JSON。
func UnmarshalPushCommand ¶
UnmarshalPushCommand 解析总线 JSON。newData 根据业务 type 返回全新的可写 proto, 未知类型返回 nil。error 事件不调用 newData。截止时间保留给投递和写出时检查。 已知类型的 data 忽略本地 proto 未定义的字段,使滚动发布中的旧节点也能投递已知字段。
type PushMessage ¶
PushMessage 描述主动事件。Type 必填,Data 与 Error 二选一;ExpiresAt 为零表示不过期。
type PushResult ¶
PushResult 统计本节点匹配、已入队和失败的连接数。成功入队不等于终端已收到。
type Router ¶
type Router struct {
// contains filtered or unexported fields
}
Router 由 NewServer 提供给 Config.RegisterHandlers,按 scene 和 action 登记业务处理器。 每个服务拥有自己的路由表,构造成功后只读。首次使用后不可复制。
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server 组合业务 Session 和原始传输;HTTP 与本服务的生命周期均需由调用方管理。 bootstrap.NewWebSocketService 可将两者组合为独立端口服务;共用端口时分别登记。 构造后不得复制,Run 只执行一次,Stop/Wait/Cancel 可并发调用。
func (*Server) Broadcast ¶
func (s *Server) Broadcast(ctx context.Context, msg PushMessage) (PushResult, error)
Broadcast 向当前 Server 上所有已就绪的连接投递事件。零命中是正常结果。
func (*Server) PushToDevice ¶
func (s *Server) PushToDevice(ctx context.Context, deviceID string, msg PushMessage) (PushResult, error)
PushToDevice 向当前 Server 上该设备的全部已就绪连接投递。DeviceID 使用握手认证值。
func (*Server) PushToUser ¶
func (s *Server) PushToUser(ctx context.Context, userID string, msg PushMessage) (PushResult, error)
PushToUser 向当前 Server 上该用户的全部已就绪连接投递。UserID 使用握手认证值。
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session 代表单次连接;身份固定,发送并发安全,业务状态的并发安全由业务保证。 会话不是分布式寻址句柄,不跨重连复用。
func (*Session) Info ¶
func (s *Session) Info() ClientInfo
func (*Session) RemoteAddr ¶
func (*Session) Reply ¶
func (s *Session) Reply(ctx context.Context, request *Message, messageType string, data proto.Message) error
Reply 使用业务指定的消息类型关联当前连接的请求,只回显 requestId。 请求回调结束或超时后不能继续通过此方法回复。 活跃请求的回复因队列或预算不足而失败时发起关闭 1013,编码失败或超限时发起关闭 1011。 发送错误原样返回。
func (*Session) ReplyError ¶
func (s *Session) ReplyError(ctx context.Context, request *Message, messageType, code, message string) error
ReplyError 使用业务指定的消息类型下发公开错误,发送失败沿用 Reply 的关闭策略。
func (*Session) Send ¶
Send 同步编码并接纳到有界队列;成功不表示已写入网络或对端收到。 当前连接的主动事件用 Send。回复请求用 Reply 或 ReplyError,以关联原请求并处理发送失败。