从零手写一个 RPC 框架:用一条因果链推导出 Dubbo 的骨架

大部分 RPC 教程的开头是"Dubbo 由注册中心、提供者、消费者三部分组成"------这是结论,不是推导。

本文换一个姿势:只从一个愿景出发,每一步都因为上一步的必然缺陷而被迫存在

用三个版本(v1→v2→v3)逐步长出一个 mini-Dubbo。全部代码 JDK 零依赖,javac 直接跑。

零、因果链全景:每一层都是上一层的必然推论

起点不是技术,是愿景:让远程调用用起来像本地调用。

复制代码
0. 愿景:helloService.hello("阿黄") 代码不变,但实现在另一台机器
 │
 ├─ 因为:消费者手里只有接口,没有实现类,接口无法直接执行
 │   所以 ① 动态代理:运行期造一个"假实现",拦截每次方法调用
 │
 ├─ 因为:拦截到的是 JVM 内存对象,网络只认字节流
 │   所以 ② 序列化 + 应用层协议
 │
 ├─ 因为:字节流要跨过不可靠的网络
 │   所以 ③ 网络传输:TCP + IO 模型 + 粘包拆包
 │
 ├─ 因为:服务端要把字节流变回真正的代码执行
 │   所以 ④ 反序列化 → 本地注册表查实现 → 反射 invoke
 │
 ├─ 因为:生产环境提供者有多台,且会动态上下线
 │   所以 ⑤ 服务发现(注册中心)+ 负载均衡
 │
 └─ 因为:网络会超时、机器会挂、重试会雪崩
     所以 ⑥ 容错层:超时 → 重试 → 熔断降级

检验标准:拿掉任何一层,后面全部站不住。

注意一个反直觉的划分:①②③④ 才是 RPC 本体,⑤⑥ 是"多台提供者"这个事实长出来的治理层。Dubbo 三件套(注册/订阅/心跳)只是第 ⑤ 层的一种实现。本文 v1→v2 实现本体,v3 实现治理层。

一、v1 最小闭环:让一次调用先跑通

核心思想

消费者只有接口。JDK 动态代理在运行期造一个"假实现"------调用 hello() 不会执行任何业务逻辑,而是被拦进 invoke(),在那里完成:组装协议对象 → 序列化 → socket 发送 → 等待响应 → 反序列化返回。

协议五要素

一次远程调用要把"调谁"描述清楚,缺一不可:

字段 为什么必须存在
requestId TCP 长连接上并发多请求,响应靠它配对(多路复用)
interfaceName 服务端从本地注册表定位实现对象
methodName 调哪个方法
parameterTypes 方法重载时仅靠方法名无法区分,必须带参数类型签名
parameters 实参

调用时序

HelloServiceImpl 服务端处理器 TCP 连接 动态代理(假实现) 业务代码 HelloServiceImpl 服务端处理器 TCP 连接 动态代理(假实现) 业务代码 #mermaid-svg-qO717TmZaeA8RP2A{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qO717TmZaeA8RP2A .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qO717TmZaeA8RP2A .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qO717TmZaeA8RP2A .error-icon{fill:#552222;}#mermaid-svg-qO717TmZaeA8RP2A .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qO717TmZaeA8RP2A .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qO717TmZaeA8RP2A .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qO717TmZaeA8RP2A .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qO717TmZaeA8RP2A .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qO717TmZaeA8RP2A .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qO717TmZaeA8RP2A .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qO717TmZaeA8RP2A .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qO717TmZaeA8RP2A .marker.cross{stroke:#333333;}#mermaid-svg-qO717TmZaeA8RP2A svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qO717TmZaeA8RP2A p{margin:0;}#mermaid-svg-qO717TmZaeA8RP2A .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qO717TmZaeA8RP2A text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-qO717TmZaeA8RP2A .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-qO717TmZaeA8RP2A .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-qO717TmZaeA8RP2A .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-qO717TmZaeA8RP2A .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-qO717TmZaeA8RP2A #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-qO717TmZaeA8RP2A .sequenceNumber{fill:white;}#mermaid-svg-qO717TmZaeA8RP2A #sequencenumber{fill:#333;}#mermaid-svg-qO717TmZaeA8RP2A #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-qO717TmZaeA8RP2A .messageText{fill:#333;stroke:none;}#mermaid-svg-qO717TmZaeA8RP2A .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qO717TmZaeA8RP2A .labelText,#mermaid-svg-qO717TmZaeA8RP2A .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-qO717TmZaeA8RP2A .loopText,#mermaid-svg-qO717TmZaeA8RP2A .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-qO717TmZaeA8RP2A .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-qO717TmZaeA8RP2A .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-qO717TmZaeA8RP2A .noteText,#mermaid-svg-qO717TmZaeA8RP2A .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-qO717TmZaeA8RP2A .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qO717TmZaeA8RP2A .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qO717TmZaeA8RP2A .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qO717TmZaeA8RP2A .actorPopupMenu{position:absolute;}#mermaid-svg-qO717TmZaeA8RP2A .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-qO717TmZaeA8RP2A .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qO717TmZaeA8RP2A .actor-man circle,#mermaid-svg-qO717TmZaeA8RP2A line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-qO717TmZaeA8RP2A :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ①拦截:没有任何业务逻辑执行 helloService.hello("阿黄")②组装 RpcRequest(五要素) + 序列化③write 字节流④read 字节流反序列化 → Map查实现 → getMethod(方法名,参数类型)Method.invoke(实现, 实参)"你好, 阿黄!"序列化 RpcResponse 回写read 字节流反序列化 → return(和本地调用一模一样)

核心代码

消费端发动机------动态代理(v1:对象流直连 socket):

java 复制代码
public class RpcClientProxy implements InvocationHandler {

    @SuppressWarnings("unchecked")
    public <T> T getProxy(Class<T> interfaceClass) {
        return (T) Proxy.newProxyInstance(
                interfaceClass.getClassLoader(),
                new Class<?>[]{interfaceClass}, this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        RpcRequest request = new RpcRequest();
        request.setRequestId(UUID.randomUUID().toString());  // v1:UUID 字符串,36 字节
        request.setInterfaceName(method.getDeclaringClass().getName());
        request.setMethodName(method.getName());
        request.setParameterTypes(method.getParameterTypes());
        request.setParameters(args);

        try (Socket socket = new Socket(host, port)) {
            // 必须先 OOS 后 OIS,原因见下文"必踩的坑"
            ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
            oos.writeObject(request);
            oos.flush();

            ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
            RpcResponse response = (RpcResponse) ois.readObject();

            if (response.hasError()) {
                throw response.getError();  // 远程异常就地重抛,感知同本地异常
            }
            return response.getResult();
        }
    }
}

服务端------一次连接只处理一个请求,处理完就关(v1 的临时形态):

java 复制代码
public void run() {
    try (Socket s = socket;
         // 和客户端镜像:先输入流后输出流(客户端先写 header,这里先读)
         ObjectInputStream ois = new ObjectInputStream(s.getInputStream());
         ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream())) {

        // 没有循环:readObject 一次、处理一次、回写一次,try 块结束连接就关闭
        RpcRequest request = (RpcRequest) ois.readObject();

        RpcResponse response = new RpcResponse();
        response.setRequestId(request.getRequestId());
        try {
            Object result = invoke(request);   // 查注册表 + 反射,见下
            response.setResult(result);
        } catch (Exception e) {
            response.setError(e.getCause() != null ? e.getCause() : e);
        }
        oos.writeObject(response);
        oos.flush();
    }
}

private Object invoke(RpcRequest request) throws Exception {
    Object service = serviceRegistry.get(request.getInterfaceName());  // 本地注册表:接口→实现
    if (service == null) {
        throw new IllegalStateException("服务未注册: " + request.getInterfaceName());
    }
    // 靠"方法名 + 参数类型签名"定位唯一方法(支持重载)
    Method method = service.getClass().getMethod(
            request.getMethodName(), request.getParameterTypes());
    return method.invoke(service, request.getParameters());
}

一个必踩的坑

v1 用 ObjectOutputStream/ObjectInputStream 直接包装 socket 流。OOS 构造时主动写出 stream header ,OIS 构造时阻塞等待对方 header------两端如果都先建输入流,互相等,死锁。所以客户端必须先 OOS 后 OIS,服务端镜像。

v1 埋下的伏笔(每处临时方案都是演进的种子)

  1. 对象流直接包在 socket 上------序列化算法和消息定界焊死在一起
  2. requestId 用 UUID 字符串(36 字节)
  3. 服务端一次连接只处理一个请求
  4. 点对点硬编码 host:port

二、v2 自定义协议:对象流下线的连锁反应

因果链

对象流(OOS/OIS)其实是一套私有协议:stream header(0xACED)+ TC_OBJECT 等类型标记,消息边界由它内部维护 ------v1"恰好"没遇到粘包,是它的功劳。但它体积大(带类元数据)、只限 Java、有反序列化安全漏洞 → 换掉它 → 免费的消息边界被收回 → TCP 是字节流,write 三次和 read 三次没有任何对应关系(Nagle 合并小包=粘包、MSS 拆分大包=半包)→ 应用层必须自己定界。

定界三法:固定长度(浪费)/ 分隔符(body 含分隔符要转义)/ 长度字段(主流,Dubbo、HTTP Content-Length 都是它)

16 字节协议头设计

每个字段都回答一个"为什么":

复制代码
magic(2B) + type(1B) + serializer(1B) + requestId(8B) + bodyLength(4B) | body(变长)
  • magic:识别非法流量,误连端口直接断开(Dubbo 用 0xdabb),这个不防恶意,只防随机的
  • type:一套协议承载多种消息:请求/响应/心跳(v3 会回收这个伏笔)
  • serializer:头体分离------头部固定编码,body 用什么算法必须声明,序列化才可插拔
  • requestId :UUID(36B) → long(8B),协议头要抠字节
  • bodyLength:定界 + 先校验上限再分配缓冲(防内存攻击)

对照 Dubbo:magic(2) + flag(1,位压缩) + status(1) + requestId(8) + dataLength(4) = 16B。

核心代码

解码逻辑------粘包半包就是在这里被消化的:

java 复制代码
public static DecodedMessage decode(DataInputStream in) throws IOException {
    // ① 先读满 16 字节头(readFully 循环读,半包会等数据到齐)
    short magic = in.readShort();
    if (magic != ProtocolConstants.MAGIC) {
        throw new IllegalStateException("非法流量,魔数不匹配");
    }
    byte type = in.readByte();
    byte serializerCode = in.readByte();
    long requestId = in.readLong();
    int bodyLength = in.readInt();

    // ② 先校验长度再分配缓冲------防恶意 length 撑爆内存
    if (bodyLength < 0 || bodyLength > ProtocolConstants.MAX_BODY_LENGTH) {
        throw new IllegalStateException("非法 bodyLength: " + bodyLength);
    }

    // ③ 按头里声明的长度读满 body------粘包/半包在这一步被消化
    byte[] body = new byte[bodyLength];
    in.readFully(body);

    // ④ 头体分离:按 serializer 字段选算法(可插拔)
    Serializer serializer = Serializers.get(serializerCode);
    return new DecodedMessage(type, serializer.deserialize(body, Object.class));
}

一句话总结:readFully 循环读到满 = 半包处理;每条消息精确消费 16+bodyLength 字节 = 粘包拆分。

证伪测试:粘包演示

不只证明"能跑",要证明"定界有效"------把两条消息合并成一个 824 字节的数组一次 write

java 复制代码
byte[] msg1 = MessageCodec.encodeRequest(req1, serializer);  // 380B
byte[] msg2 = MessageCodec.encodeRequest(req2, serializer);  // 444B
merged.write(msg1);
merged.write(msg2);
out.write(merged.toByteArray());  // 824B 一次写出,服务端一次 read 可能拿到全部

结果:服务端一个连接正确拆出两个请求(requestId=1001/1002),各自正确响应。如果 bodyLength 定界逻辑有错,这步必炸。

迭代副产物

  • 死锁坑消失:对象流作用域缩到内存字节数组(Serializer 实现),不再有流 header 协商
  • Serializer 接口化:换 Hessian/Protobuf 只是新增一个实现类,协议头不动
  • 服务端循环 decode:一个连接可处理多条消息(长连接雏形)

三、v3 注册中心:治理层是"多台提供者"长出来的

因果链

复制代码
v2 现状:点对点硬编码 host:port
 │  因为:提供者有多台且动态上下线,硬编码既没法分散压力,机器挂了也不知道
 │  所以 注册中心:第三方进程存"接口 → 地址列表"
 │       (注册/查找本身就是远程调用 → 复用 16B 协议,type 字段伏笔回收)
 ├─ 因为:查找返回一个列表,选哪台?
 │   所以 负载均衡:轮询 / 随机
 ├─ 因为:提供者宕机不会主动注销(被 kill 时没机会发消息)
 │   所以 心跳续约 + 租约过期剔除(对应 ZK 临时节点,同一个因果)
 └─ 因为:注册中心自己也可能挂
     所以 消费者本地缓存列表 + 定时刷新------已有缓存照常调用,注册中心不是单点

全流程时序

消费者 注册中心 :8888 提供者B :9998 提供者A :9999 消费者 注册中心 :8888 提供者B :9998 提供者A :9999 #mermaid-svg-MJaVKHQdIi4azXzw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MJaVKHQdIi4azXzw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MJaVKHQdIi4azXzw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MJaVKHQdIi4azXzw .error-icon{fill:#552222;}#mermaid-svg-MJaVKHQdIi4azXzw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MJaVKHQdIi4azXzw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MJaVKHQdIi4azXzw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MJaVKHQdIi4azXzw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MJaVKHQdIi4azXzw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MJaVKHQdIi4azXzw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MJaVKHQdIi4azXzw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MJaVKHQdIi4azXzw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MJaVKHQdIi4azXzw .marker.cross{stroke:#333333;}#mermaid-svg-MJaVKHQdIi4azXzw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MJaVKHQdIi4azXzw p{margin:0;}#mermaid-svg-MJaVKHQdIi4azXzw .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-MJaVKHQdIi4azXzw text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-MJaVKHQdIi4azXzw .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-MJaVKHQdIi4azXzw .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-MJaVKHQdIi4azXzw .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-MJaVKHQdIi4azXzw .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-MJaVKHQdIi4azXzw #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-MJaVKHQdIi4azXzw .sequenceNumber{fill:white;}#mermaid-svg-MJaVKHQdIi4azXzw #sequencenumber{fill:#333;}#mermaid-svg-MJaVKHQdIi4azXzw #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-MJaVKHQdIi4azXzw .messageText{fill:#333;stroke:none;}#mermaid-svg-MJaVKHQdIi4azXzw .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-MJaVKHQdIi4azXzw .labelText,#mermaid-svg-MJaVKHQdIi4azXzw .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-MJaVKHQdIi4azXzw .loopText,#mermaid-svg-MJaVKHQdIi4azXzw .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-MJaVKHQdIi4azXzw .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-MJaVKHQdIi4azXzw .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-MJaVKHQdIi4azXzw .noteText,#mermaid-svg-MJaVKHQdIi4azXzw .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-MJaVKHQdIi4azXzw .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-MJaVKHQdIi4azXzw .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-MJaVKHQdIi4azXzw .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-MJaVKHQdIi4azXzw .actorPopupMenu{position:absolute;}#mermaid-svg-MJaVKHQdIi4azXzw .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-MJaVKHQdIi4azXzw .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-MJaVKHQdIi4azXzw .actor-man circle,#mermaid-svg-MJaVKHQdIi4azXzw line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-MJaVKHQdIi4azXzw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} loop每 2 秒续约 宕机 kill -9(没机会注销) REGISTER(HelloService, 9999)REGISTER(HelloService, 9998)HEARTBEATHEARTBEATLOOKUP(HelloService)9999, 9998 → 本地缓存,每 2s 刷新轮询选中 → RPC 调用(type=1)响应(type=2)下一次轮询到 B响应心跳中断后台扫描:超 6s 未续约 → 剔除 9998定时刷新 LOOKUP9999后续调用全部切到 9999,零报错

注意:注册中心只参与"选哪台",不参与"怎么调"------业务流量始终点对点,它不在调用链路上,所以不是性能瓶颈。

核心代码

注册中心的下线感知------租约过期剔除:

java 复制代码
/** 心跳超时时长:超过它没续约就剔除(提供者每 2s 续约一次) */
private static final long EXPIRE_MS = 6_000;

private void expireDeadInstances() {
    long now = System.currentTimeMillis();
    services.forEach((interfaceName, instances) -> {
        synchronized (instances) {
            instances.removeIf(ins -> {
                boolean dead = now - ins.lastHeartbeat > EXPIRE_MS;
                if (dead) {
                    System.out.println("[Registry] 心跳超时剔除: "
                            + interfaceName + " -> " + ins.address);
                }
                return dead;
            });
        }
    });
}

消费者的本地缓存------注册中心挂了也能调:

java 复制代码
public List<String> discover(String interfaceName) {
    return cache.computeIfAbsent(interfaceName, this::lookupFromRegistry);
}

private void refreshAll() {
    cache.keySet().forEach(interfaceName -> {
        try {
            cache.put(interfaceName,
                    new CopyOnWriteArrayList<>(lookupFromRegistry(interfaceName)));
        } catch (Exception e) {
            // 刷新失败保留旧缓存------注册中心短暂不可用不影响调用
        }
    });
}

证伪测试:kill 一个实例

  1. 双实例(9999/9998)+ 轮询 → 6 次调用交替命中 ✓
  2. kill -9 掉 9998(模拟宕机)→ 注册中心日志打出 心跳超时剔除 → 9 秒后再调 4 次全部切到 9999,零报错

如果心跳、剔除、缓存刷新任何一环失效,这一步必然 Connection refused。

四、三版对照:每个改动都被一个具体问题驱动

维度 v1 v2 v3
序列化 对象流直连 socket Serializer 接口 + 内存字节数组 同 v2
消息定界 对象流内部标记(免费) 协议头 bodyLength(自己维护) 同 v2
requestId UUID 36B long 8B + AtomicLong 同 v2
寻址 ------ 硬编码 host:port 注册中心查找 + 本地缓存
选机器 ------ 无(只有一台) 轮询 / 随机负载均衡
下线感知 ------ 心跳续约 + 租约过期剔除
协议 type 1/2 1/2 1/2/3/4/5 全部启用

五、结语:和 Dubbo 对照着看

手写一遍之后再回头看 Dubbo,每个设计都能对号入座:

  • 我们的 16B 协议头 ↔ Dubbo 协议头(0xdabb 魔数 + 位压缩 flag)
  • 我们的 decode ↔ Netty 的 LengthFieldBasedFrameDecoder
  • 我们的心跳剔除 ↔ ZK 临时节点(会话断开自动消失,同一个因果)
  • 我们的本地缓存 ↔ Dubbo 的缓存文件兜底
  • 我们的 ServiceDiscovery 定时拉取 ↔ Dubbo 的订阅推送

框架不是被设计出来的,是被问题逼出来的。 当你能说出每一层"因为什么而存在",八股文就变成了你自己的技术见解。

相关推荐
AI备忘录3 小时前
(十一)DHCP 配置命令五厂商对照:华为 华三 锐捷 迈普 思科
服务器·网络·网络协议·网络安全·华为
游戏开发爱好者814 小时前
WebSocket 抓包怎么查看内容?从握手到消息帧完整解读
网络协议·计算机网络·网络安全·ios·adb·https·udp
尘世壹俗人16 小时前
如何生成SSL要用的自签证书
网络协议
新手unity自用笔记19 小时前
unity基于Socket的网络学习
网络·网络协议·学习·unity·c#·游戏引擎
2401_8685347820 小时前
在企业环境中快速安装配置 FreeBSD Unix 服务器操作系
linux·网络协议
XUEYUAN521220 小时前
Cloudflare 防护机制深度剖析与跨境数据采集工程化实践
服务器·网络·网络协议·tcp/ip·http
青瓦梦滋20 小时前
IP/MAC帧/ARP协议
运维·服务器·网络·网络协议·tcp/ip
TlSfoward20 小时前
TLS指纹库的数据质量与误判治理 TLSFOWARD TLS指纹库
爬虫·网络协议·http
Shell运维手记1 天前
Linux (挂载|rpm|yum|源码编译)
linux·运维·笔记·网络协议·bash