如何在 JSON-RPC 方法中获取 WebSocket 连接上下文信息

1次阅读

如何在 JSON-RPC 方法中获取 WebSocket 连接上下文信息

本文介绍在 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 生态中处理此类问题的经典范式。

text=ZqhQzanResources