Post

手写 RPC 框架踩坑记录:自定义 TCP 协议调试全过程

手写 RPC 框架踩坑记录:自定义 TCP 协议调试全过程

前言

最近在跟着编程导航鱼皮的教程手写 RPC 框架,第 7 章要求将原有的 HTTP 传输改为自定义 TCP 协议。改完之后消费者一直报错,折腾了不少时间,在此记录一下完整的排查和修复过程,希望能帮到同样踩坑的朋友。


一、背景介绍

本项目基于 Vert.x 构建 RPC 框架,第 7 章的目标是:

  • 将原有的 VertxHttpServer 替换为 VertxTcpServer
  • 自定义消息结构(17 字节消息头 + 消息体)
  • 实现 ProtocolMessageEncoderProtocolMessageDecoder
  • 实现 TcpServerHandler 处理请求并回写响应

二、坑一:Invalid magic number: 72

报错现象

消费者调用服务时,抛出异常:

1
RuntimeException: 消息 magic 非法 (Invalid magic number: 72)

排查过程

72 转成十六进制是 0x48,对应 ASCII 字符 'H'。联想到 VertxTcpServer 中有一个示例方法:

1
2
3
private byte[] handleRequest(byte[] requestData) {
    return "Hello, client!".getBytes(); // 'H' = 72
}

服务端返回的不是自定义协议格式的响应,而是这个示例字符串。消费者的解码器读取第一个字节时,拿到的是 'H' 而不是协议魔数 0x1,因此报 magic 非法。

根本原因

VertxTcpServerconnectHandler 里仍然在调用旧的 handleRequest() 示例逻辑,没有替换成真正的 TcpServerHandler

解决方案

VertxTcpServerconnectHandler 改为:

1
server.connectHandler(new TcpServerHandler());

三、坑二:消费者超时 TimeoutException,但 Provider 日志显示服务调用成功

报错现象

修复坑一之后,Provider 日志打印了正确的业务结果(例如”用户名:dangzitou”),说明服务调用成功。但消费者侧等待 5 秒后抛出:

1
java.util.concurrent.TimeoutException

排查过程

既然 Provider 业务逻辑执行成功了,那问题一定出在响应的编码和回写环节。仔细检查 TcpServerHandler 的代码,发现了如下问题:

1
2
3
4
5
6
7
8
9
10
11
12
// ❌ 错误代码
ProtocolMessage.Header header = protocolMessage.getHeader();
header.setType((byte) ProtocolMessageTypeEnum.RESPONSE.getKey());

ProtocolMessage<RpcResponse> responseMessage = new ProtocolMessage<>(); // body 和 header 都是 null!

try {
    Buffer encode = ProtocolMessageEncoder.encode(responseMessage);
    netSocket.write(encode);
} catch (Exception e) {
    throw new RuntimeException("Failed to encode protocol message");
}

responseMessage 是一个空对象,headerbody 都没有设置进去。

根本原因

编码器 ProtocolMessageEncoder.encode() 内部有如下判断:

1
2
3
if (protocolMessage == null || protocolMessage.getHeader() == null) {
    return Buffer.buffer(); // 直接返回空 Buffer!
}

由于 responseMessageheadernull,编码器直接返回了空 Buffer。netSocket.write(Buffer.buffer()) 向客户端写了一个空内容,消费者自然收不到任何有效数据,一直等到超时。

解决方案

构建 responseMessage 时,把 headerrpcResponse 都传入:

1
2
3
4
5
6
7
8
9
10
11
12
// ✅ 正确代码
ProtocolMessage.Header header = protocolMessage.getHeader();
header.setType((byte) ProtocolMessageTypeEnum.RESPONSE.getKey());

ProtocolMessage<RpcResponse> responseMessage = new ProtocolMessage<>(header, rpcResponse);

try {
    Buffer encode = ProtocolMessageEncoder.encode(responseMessage);
    netSocket.write(encode);
} catch (Exception e) {
    throw new RuntimeException("Failed to encode protocol message: " + e.getMessage(), e);
}

四、完整修复后的 TcpServerHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class TcpServerHandler implements Handler<NetSocket> {
    @Override
    public void handle(NetSocket netSocket) {
        netSocket.handler(buffer -> {
            // 解码请求
            ProtocolMessage<RpcRequest> protocolMessage;
            try {
                protocolMessage = (ProtocolMessage<RpcRequest>) ProtocolMessageDecoder.decode(buffer);
            } catch (Exception e) {
                throw new RuntimeException("Failed to decode protocol message: " + e.getMessage(), e);
            }
            RpcRequest rpcRequest = protocolMessage.getBody();

            // 反射调用服务
            RpcResponse rpcResponse = new RpcResponse();
            try {
                Class<?> implClass = LocalRegistry.getService(rpcRequest.getServiceName());
                Method method = implClass.getMethod(rpcRequest.getMethodName(), rpcRequest.getParamTypes());
                Object result = method.invoke(implClass.getDeclaredConstructor().newInstance(), rpcRequest.getParams());
                rpcResponse.setData(result);
                rpcResponse.setDataType(method.getReturnType());
                rpcResponse.setMessage("ok");
            } catch (Exception e) {
                e.printStackTrace();
                rpcResponse.setMessage(e.getMessage());
                rpcResponse.setException(e);
            }

            // 编码并回写响应(注意:header 和 body 都要设置!)
            ProtocolMessage.Header header = protocolMessage.getHeader();
            header.setType((byte) ProtocolMessageTypeEnum.RESPONSE.getKey());
            ProtocolMessage<RpcResponse> responseMessage = new ProtocolMessage<>(header, rpcResponse);
            try {
                Buffer encode = ProtocolMessageEncoder.encode(responseMessage);
                netSocket.write(encode);
            } catch (Exception e) {
                throw new RuntimeException("Failed to encode protocol message: " + e.getMessage(), e);
            }
        });
    }
}

五、经验总结

坑一的教训:教程中的示例代码(如 "Hello, client!" 这类占位逻辑)在推进到下一步时一定要及时替换,否则会产生非常迷惑的报错。72 这个魔数值如果不了解 ASCII 编码,很难第一时间联想到原因。

坑二的教训:在构建响应消息对象时,一定要检查所有字段是否都已正确赋值再传入编码器。编码器对 null 的防御性处理(返回空 Buffer)不会抛异常,这导致问题被静默掩盖,表现为消费者超时而非明显报错,排查起来非常隐蔽。


This post is licensed under CC BY 4.0 by the author.