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










