使用net/rpc/jsonrpc通过Unix套接字发送大数组/切片为何阻塞?
Go net/rpc jsonrpc 大数组响应导致客户端阻塞问题排查
问题现象
使用Go内置net/rpc配合net/rpc/jsonrpc编解码器时,将数组作为RPC响应发送,当响应数据大小约为48字节时,客户端会在client.Call处阻塞;若将数组长度常量N改为5(响应数据更小),程序则能正常运行,且该问题仅在数组/切片大小超过49字节时触发。
问题根源
net/rpc/jsonrpc依赖JSON编码后的数据流完成通信,其默认实现未处理好TCP分包/粘包场景:
- 当响应数据较小时,TCP会将整个JSON响应封装为单个数据包发送,客户端能一次性读取完整的JSON对象,正常解析响应。
- 当响应数据达到一定大小,TCP会将数据拆分为多个数据包发送,而
jsonrpc客户端的默认读取逻辑无法正确识别响应的结束边界,会一直等待剩余数据,最终导致阻塞。
解决方案
方案1:改用Go原生gob编解码器
gob是Go专属的序列化格式,对复杂数据类型、大数据的处理更稳定,不存在JSON的边界识别问题,是最快捷的解决方式:
- 服务端:直接使用
rpc.Register注册服务,通过rpc.Accept处理连接,无需指定jsonrpc。 - 客户端:使用
rpc.Dial建立连接,替代jsonrpc.Dial。
方案2:自定义jsonrpc边界处理逻辑
若必须使用JSON编码,可通过长度前缀的方式明确响应边界,让客户端先读取数据长度,再读取对应字节数的JSON内容:
服务端修改示例
func serveCustomJSONRPC(conn net.Conn) { defer conn.Close() codec := jsonrpc.NewServerCodec(conn) for { reqHeader, err := codec.ReadRequestHeader() if err != nil { break } resp := &rpc.Response{ServiceMethod: reqHeader.ServiceMethod, Seq: reqHeader.Seq} // 处理RPC请求 err = rpc.ServeRequest(codec) if err != nil { resp.Error = err.Error() } // 序列化响应并添加长度前缀 respBytes, _ := json.Marshal(resp) lengthBuf := binary.BigEndian.AppendUint32(nil, uint32(len(respBytes))) // 先发送长度,再发送响应内容 conn.Write(lengthBuf) conn.Write(respBytes) } }
客户端修改示例
func customJSONRPCCall(client *rpc.Client, method string, args, reply interface{}) error { // 获取底层连接 conn, ok := client.Codec.(*jsonrpc.ClientCodec).Conn.(net.Conn) if !ok { return fmt.Errorf("invalid jsonrpc codec") } // 先读取响应长度 var respLength uint32 if err := binary.Read(conn, binary.BigEndian, &respLength); err != nil { return err } // 读取对应长度的响应内容 respBytes := make([]byte, respLength) if _, err := io.ReadFull(conn, respBytes); err != nil { return err } // 解析响应 var resp rpc.Response if err := json.Unmarshal(respBytes, &resp); err != nil { return err } if resp.Error != "" { return errors.New(resp.Error) } return json.Unmarshal([]byte(resp.Result), reply) }
方案3:确保响应流正确关闭
检查服务端代码,确保发送完响应后正确刷新连接写入缓冲区,或在必要时关闭连接写入端,避免客户端等待额外数据。
内容的提问来源于stack exchange,提问作者qciccoretti
相关产品推荐
相关产品推荐

