
本文介绍在 go 语言中基于 .net/rpc/jsonrpc` 和 websocket 实现 rpc 时,如何安全、规范地将 websocket 连接(*websocket.conn)信息透传至具体 rpc 方法内部,解决“方法内无法访问调用连接”这一常见需求。
在标准 net/rpc/jsonrpc 框架中,RPC 方法签名是严格隔离的:参数和返回值均需序列化/反序列化,而底层网络连接(如 *websocket.Conn)被抽象层完全隐藏——这虽保障了协议一致性,却也导致业务逻辑无法获取调用来源的连接元信息(如客户端 IP、握手头、连接 ID 或自定义会话状态)。直接修改 jsonrpc.ServeConn 行为不可行,因其不暴露连接上下文;因此,需通过扩展 RPC 编解码器(ServerCodec)+ 上下文注入的方式实现解耦且类型安全的传递。
✅ 推荐方案:基于 context.Context 的连接信息注入
go 官方 context 包天然适合作为跨层传递请求级数据的载体。我们可构建一个自定义 ServerCodec,在反序列化请求参数后,自动将封装了 WebSocket 连接的 context.Context 注入到参数结构体的 Context 字段中。
步骤一:定义支持上下文的参数结构
import ( "context" "net/http" "net/rpc" "net/rpc/jsonrpc" "code.google.com/p/go.net/websocket" ) type Args struct { A int B int Context context.Context // 新增字段,用于接收注入的上下文 }
步骤二:实现自定义 ServerCodec
以下是一个轻量级 websocketJSONCodec 示例,它包装原始 jsonrpc.ServerCodec,并在 ReadRequestBody 中完成上下文注入:
type websocketJSONCodec struct { *jsonrpc.ServerCodec ws *websocket.Conn } func (c *websocketJSONCodec) ReadRequestBody(x interface{}) error { // 先执行原逻辑反序列化 if err := c.ServerCodec.ReadRequestBody(x); err != nil { return err } // 反射注入 context.Context(仅当 x 是指针且目标结构含 Context 字段) v := reflect.ValueOf(x) if v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct { elem := v.Elem() ctxField := elem.FieldByName("Context") if ctxField.IsValid() && ctxField.CanSet() && ctxField.Type() == reflect.TypeOf((*context.Context)(nil)).Elem().Elem() { // 构建包含 WebSocket 连接的 context(可扩展更多元信息) ctx := context.WithValue(context.Background(), "websocket.conn", c.ws) ctxField.Set(reflect.ValueOf(ctx)) } } return nil }
步骤三:改造 serve 函数,使用自定义编解码器
func serve(ws *websocket.Conn) { // 创建自定义 codec 并绑定当前 ws 连接 codec := &websocketJSONCodec{ ServerCodec: jsonrpc.NewServerCodec(ws), ws: ws, } rpc.ServeRequest(codec) }
步骤四:在 RPC 方法中安全使用连接信息
func (t *Arith) Multiply(args *Args, reply *int) error { *reply = args.A * args.B // ✅ 安全获取 WebSocket 连接 if conn, ok := args.Context.Value("websocket.conn").(*websocket.Conn); ok { // 获取客户端地址(示例) remoteAddr := conn.RemoteAddr().String() // 获取 HTTP 头(需在握手时保存,因 ws.Conn 不直接暴露 headers) // 建议:在 serve 函数中提前解析并存入 context httpReq := conn.Request() userAgent := httpReq.Header.Get("User-Agent") // 业务逻辑可基于连接做鉴权、限流、日志追踪等 log.Printf("Multiply called from %s (UA: %s)", remoteAddr, userAgent) } return nil }
⚠️ 注意事项与最佳实践
- 避免全局状态:不要通过包级变量或 goroutine local storage 传递连接,易引发竞态和内存泄漏。
- Context 生命周期管理:context.Context 应随连接生命周期存在,无需手动取消(websocket.Conn.Close() 后 context 自然失效)。
- Header 访问限制:websocket.Conn.Request() 仅在握手阶段有效;若需完整 HTTP 头,建议在 serve 中提取并存入 context(如 context.WithValue(ctx, “headers”, req.Header))。
- 兼容性考量:此方案要求所有需连接信息的 RPC 参数结构必须显式声明 Context context.Context 字段,否则注入失败——这是明确契约,优于隐式依赖。
- 替代方案对比:若项目已升级至 Go 1.21+,可考虑改用更现代的 gRPC-Web 或 jsonrpc2 库,其原生支持上下文;但对现有 net/rpc 迁移成本低的场景,本方案最务实。
通过以上设计,既尊重了 RPC 的抽象边界,又以最小侵入方式赋予业务方法感知网络层的能力,是 Go 生态中处理此类问题的经典范式。