Dubbo 2 巩固教程之一

大纲

前言

学习资源

Dubbo 核心特性

通信协议

特别注意

  • Dubbo 支持多种通信协议,在 3.0 版本之前支持 Dubbo、gRPC、Hessian2、REST 等核心通信协议。
  • 从 Dubbo 3.0 开始,Dubbo 官方新引入了自研的 Triple 通信协议,不过也保留了 Dubbo 和 REST 通信协议的支持。
  • 从 Dubbo 3.2 开始,Dubbo 官方已经废弃原有的 gRPC 通信协议,使用 Triple 通信协议进行替代,Triple 通信协议完全兼容 gRPC 通信协议。
  • 从 Dubbo 3.3 开始,Dubbo 官方直接移除了 REST 通信协议的直接支持,使用 Triple 通信协议间接支持 REST 通信协议。
  • Dubbo 3 支持的通信协议
通信协议通信协议名 / 标识传输层核心特性适用场景是否为默认通信协议
Dubbo 通信协议dubboTCP 单一长连接、NIO 异步通信、高性能高并发小数据量场景(消费者数 >> 提供者)
Triple 通信协议triHTTP/2 兼容 gRPC 生态、支持流式通信、跨语言能力增强多语言交互、流式数据传输
REST 通信协议restHTTP 严格遵循 RESTful 规范、支持 JSON/XML 格式浏览器调用、跨平台集成
gRPC 通信协议grpcHTTP/2Google 开源 RPC 框架、高性能跨语言通信跨语言高性能服务调用
Thrift 通信协议thriftTCPApache 开源 RPC 框架、强类型接口定义多语言服务交互
Hessian 通信协议hessianHTTP 表单序列化、短连接、传输效率优于 WebService 文件 / 视频等大数据传输
HTTP 通信协议httpHTTP 通用同步传输、支持表单序列化混合数据大小场景
Redis 通信协议redis文本通信协议基于 Redis 的轻量级通信缓存操作、简单消息队列
MQTT 通信协议mqttTCP 发布 / 订阅模式、低带宽占用物联网设备通信
WebServicewebserviceHTTP 基于 SOAP 通信协议、XML 序列化传统企业系统集成
  • 通信协议的选择对比
维度 TripleDubboRESTgRPC
性能⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
跨语言⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
流式通信支持不支持不支持支持
浏览器兼容支持不支持直接支持需要依赖 Gateway(网关)
配置复杂度

Dubbo 通信协议的使用

更多关于 Dubbo 通信协议的介绍和使用案例可以看 这里

序列化协议

  • Dubbo 支持的序列化协议
序列化协议优点缺点适用场景
Hessian2(默认协议)- 跨语言支持好
- 二进制序列化,协议紧凑
- 使用简单,Dubbo 原生支持
- 性能中等,不是最高
- 不支持对象引用共享
默认通用场景,适合大多数 RPC 调用
FastJson(JSON 格式)- 可读性强,易于调试
- 跨语言方便,尤其适合前端交互
- 文本冗余,体积大
- 性能低于二进制序列化协议(如 Hessian、Protobuf)
需要与前端或非 Java 系统交互,或调试、日志记录场景
GSON(JSON 格式)- 可读性强,易于调试
- 跨语言方便,尤其适合前端交互
- 文本冗余,体积大
- 性能低于二进制序列化协议(如 Hessian、Protobuf)
需要与前端或非 Java 系统交互,或调试、日志记录场景
Kryo- 高性能,二进制序列化速度快
- 体积小
- 支持对象图
- 不支持跨语言
- 需要手动注册类才能获得最佳性能
内部 Java 服务之间的高性能 RPC 调用,大数据领域用得较多
FST(Fast-Serialization)- 二进制序列化速度快,性能接近 Kryo
- 使用更简单
- 不支持跨语言
- 对类版本变化敏感
内部 Java 服务之间的高性能 RPC 调用
Protobuf(Google Protocol Buffers)- 高性能,二进制序列化速度快
- 体积小
- 强类型定义
- 跨语言支持好
- 需要 .proto 文件定义
- 学习成本高
跨语言、高性能 RPC 或需要协议稳定性的场景
Protostuff(基于 Protobuf)- 高性能二进制序列化,速度快、体积小
- 无需 .proto 文件定义,直接支持 Java POJO
- 序列化 / 反序列化效率显著高于 Hessian2
- 仅适用于 Java 环境(不支持跨语言)
- 对象类结构变化后兼容性较差(需保持字段一致)
对性能要求高的 Java 内部服务调用,如高并发、高吞吐的微服务场景
Avro- Hadoop 的子项目
- 支持动态 Schema
- 跨语言支持好
- 二进制序列化
- 性能不如 Protobuf
- 使用复杂
大数据场景,如 Kafka、Hadoop,需要动态 Schema
Java 原生序列化 - Java 原生支持,无需额外依赖
- 保留完整对象结构
- 二进制序列化
- 体积大,性能低
- 安全性差(反序列化漏洞风险高)
- 不支持跨语言
仅用于内部 Java 服务调试或原型验证,不建议生产使用
  • Dubbo 序列化协议的选择建议
场景推荐的序列化协议说明
默认场景(通用 RPC 调用)Hessian2Dubbo 默认的序列化协议,跨语言支持好、易用,性能中等,适合大多数场景
Java 内部高性能 RPC 调用 Protostuff、Kryo、FST 二进制序列化速度快、体积小,适合 Java 内部服务之间的高性能调用
跨语言且追求极致性能 Protobuf 高性能、体积小、跨语言支持好,但需要 .proto 文件定义,学习成本较高
调试或日志场景 FastJson 可读性强,易于调试,适合调试、日志记录或与前端交互,但性能较低
不建议使用 Java 原生序列化性能低、体积大、安全性差(反序列化漏洞风险高),仅适合内部调试或原型验证

提示

  • 各种 Java 的序列化库的性能测试比较结果可以看 这里 或者 GitHub Wiki
  • 更多关于 Dubbo 序列化协议的介绍和使用案例可以看 这里

负载均衡策略

  • Dubbo 提供了五种负载均衡策略,如下所示:
负载均衡策略说明适用场景
RandomLoadBalance 随机(默认策略),支持按权重设置随机概率适用于请求量分散、服务性能相近的场景,能保证简单有效的负载均衡效果
RoundRobinLoadBalance 轮询,支持按公约后的权重设置轮询比率适用于服务节点性能相近、需要保证调用次数均匀的场景
LeastActiveLoadBalance 最少活跃调用数,支持相同活跃数的权重随机适用于服务性能差异较大或请求响应时间差异较大的场景,能提高请求分配效率
ConsistentHashLoadBalance 一致性哈希,相同参数的请求总是发送到同一个服务提供者适用于需要保证请求一致性的场景,例如:缓存服务或用户会话粘性
ShortestResponseLoadBalance 最短响应时间优先,选择响应时间最短的服务提供者适用于请求响应时间差异较大,且希望优先使用低延迟服务节点的场景

Dubbo 负载均衡策略的使用

集群容错策略

  • Dubbo 提供了八种集群容错策略,如下所示:
集群容错策略策略名称策略说明适用场景
Failover 故障转移默认策略,当出现服务调用失败,重试调用其它服务器,默认重试 2 次,使用 retries 属性配置重试次数通常用于读操作(幂等操作),如查询数据时可以容忍偶尔失败并重试的场景
Failfast 快速失败消费者只发起一次调用,若失败则立即抛出异常,类似于 Failover 集群容错策略中重试次数设置为 0 的情况通常用于写操作(非幂等操作),如数据插入操作,失败时不应该重试
Failsafe 失败安全当消费者调用提供者出现异常时,只会打印异常,而不会抛出异常,即直接忽略本次消费操作,返回一个空结果适用于不重要的操作,如日志记录或监控信息上报,出现失败不影响整体业务逻辑
Failback 失败自动恢复在消费者调用失败后,返回一个空结果。Dubbo 会在内存中记录下(非持久化)该失败请求,并通过定时任务对失败的调用进行重试,且重试间隔默认是 5 秒(不可配置)通常用于消息通知操作,如异步通知,失败后可自动补偿
Forking 并行调用消费者对于同一服务会并行调用多个提供者服务器,只要有一个成功就调用结束,并返回结果通常用于实时性要求较高的读操作,但需要浪费更多的服务资源,可通过 forks = "2" 来设置最大并行数
Broadcast 广播调用通过广播调用所有提供者,逐个调用,任意一个提供者报错则报错适用于更新配置或通知所有服务提供者的场景,如缓存更新或者服务健康检查
ZoneAware 区域感知集群当部署了多个机房或区域(Zone)时,优先调用同一机房的服务提供者,以降低跨区调用延迟和网络风险适用于多区域部署场景,优先减少跨区域网络延迟的情况下实现高效负载均衡
Mergeable 结果合并集群调用多个服务提供者,并将多个服务提供者的调用结果进行合并,最终返回合并后的结果适用于需要聚合结果的场景,例如分布式查询、汇总统计等操作

Dubbo 集群容错策略的使用

  • FailbackCluster(失败自动恢复)的使用问题
    • 问题描述:
      • 调用失败的记录只存储在内存中,应用重启或进程崩溃后,重试记录将会丢失,导致重试机制失效。
      • 这种重试机制不适用于核心业务,比如支付交易、订单创建、库存扣减、用户积分更新等。
    • 问题解决:
      • 第一种方案:
        • Dubbo 调用改为消息驱动,使用 MQ 做调用的缓冲与重试。
          • (1) 生产者不直接调用 Dubbo 服务,而是先将消息写入 MQ 中;
          • (2) 由一个消息消费服务订阅 MQ 消息,然后执行 Dubbo RPC 调用(基于 Dubbo 原生 API 实现泛化调用);
          • (3) 如果 Dubbo RPC 调用失败,可以将消息写入到延迟队列中,直到 RPC 调用成功;
          • (4) 如果重试调用次数达到最大重试次数,则将消息写入死信队列。
          • (5) 为了避免重复调用,需要实现幂等性,比如使用数据库唯一约束(如 requestId 字段)、Redis 缓存(如 SETNX)等。
      • 第二种解决方案:
        • (1) 自定义扩展 FailbackCluster 的实现;
        • (2) 将失败记录写入外部存储(如 Redis、DB);
        • (3) 创建后台线程,每隔固定时间扫描一次外部存储,尝试重新调用 Dubbo 服务(基于 Dubbo 原生 API 实现泛化调用);
        • (4) 当重试调用成功返回时,将对应的失败记录从外部存储中删除,避免重复调用和数据堆积;
        • (5) 应用重启后重新加载未完成的失败记录,并继续由后台线程执行重试逻辑;
        • (6) 为了避免重复调用,需要实现幂等性,比如使用数据库唯一约束(如 requestId 字段)、Redis 缓存(如 SETNX)等。
    • 方案对比:
      对比维度 MQ(Kafka / RabbitMQ / RocketMQ)Redis / MySQL
      数据持久化内置持久化到 Broker 持久化到外部存储,需要自己实现
      应用重启恢复重启后仍可消费未完成消息重启后可读取失败记录,依赖自定义逻辑
      重试保障原生支持延迟重试和死信队列需定时任务扫描和手动实现延迟 / 死信机制
      吞吐能力高,支持大规模并发中等,定时扫描会成为性能瓶颈
      延迟控制原生支持延迟队列延迟不可控,依赖扫描频率
      实现复杂度中等,需要部署 MQ,但重试逻辑成熟低 - 中,不依赖额外中间件,但需自己实现重试和死信机制
      适用场景核心业务,如订单、支付、交易弱异步业务,如日志、通知、监控埋点等,可容忍一定延迟或失败
      维护成本低,MQ 自带可靠机制高,定时任务、重试逻辑和死信处理需自行维护

Dubbo 的泛化调用是什么

普通的 Dubbo 调用依赖接口类,会进行编译期类型检查,适合直接业务调用。而泛化调用则不依赖接口类,只通过接口名称和方法信息在运行时进行调用,参数类型通过字符串描述,适合消息化、动态调用、异步或重试场景。

Dubbo 深入理解

RMI 协议

Dubbo 支持使用 RMI 协议作为通信协议,但 RMI 的性能相对较低,不推荐用于高并发场景,更适合系统内部或旧系统集成场景。若需要高性能的通信,建议使用 Dubbo、Hessian、Thrift、gRPC 等通信协议。

RMI 协议的介绍

  • RMI 协议的简介

    • Dubbo 的 RMI 协议是基于 Java 原生 RMI 实现的,要求服务消费者与服务提供者均为 Java 应用
    • Dubbo 的 RMI 协议采用 JDK 标准的 java.rmi 实现,并采用阻塞式短连接和 JDK 序列化(二进制格式)
    • 如果 Dubbo 使用 RMI 协议提供服务给外部访问,且应用里依赖了旧版本的 common-collections 包的情况下,则可能会存在反序列化安全风险
  • RMI 协议的特性

    • 连接个数:多连接
    • 连接方式:短连接
    • 传输协议:TCP 协议
    • 传输方式:同步通信
    • 跨语言支持:不支持跨语言
    • 序列化协议:JDK 序列化(二进制格式)
    • 注意事项:
      • RMI 协议的性能相对较低,不推荐用于高并发场景
    • 适用场景:
      • 适用于针对 Java 的常规远程过程调用(RPC)
      • 适用于与原生 Java RMI 服务互通,便于系统间集成与迁移
      • 适用于系统内部或旧系统集成场景
    • 适用范围:
      • 适用于传入、传出参数数据量中等、消费者与提供者数量差不多的场景
      • 适用于 Java 对象和文件等可序列化数据的传输
  • RMI 协议的注意事项

    • 方法参数及返回值需要实现 Serializable 接口
    • Dubbo 配置中的超时时间(timeout)对 RMI 协议无效,需要使用 JVM 启动参数设置 RMI 协议的超时时间(单位毫秒):
      1
      2
      3
      -Dsun.rmi.transport.connectionTimeout=3000      # 建立连接的超时时间
      -Dsun.rmi.transport.tcp.responseTimeout=3000 # 响应超时时间(重点)
      -Dsun.rmi.transport.proxy.connectTimeout=3000 # 代理连接的超时时间

RMI 协议的使用

下载 Dubbo 使用 RMI 协议的案例代码

  • Dubbo 使用 RMI 协议的完整案例代码可以直接从 GitHub 下载对应章节 dubbo-lesson-13。
  • RMI 服务接口的定义

    • 如果服务接口继承了 java.rmi.Remote 接口,可以和 Java 原生 RMI 服务相互操作
      • 服务提供者使用 Dubbo 的 RMI 协议暴露服务,服务消费者直接使用标准 RMI 接口调用服务;
      • 或者,服务提供者使用标准 RMI 接口暴露服务,服务消费者使用 Dubbo 的 RMI 协议调用服务。
    • 如果服务接口没有继承 java.rmi.Remote 接口
      • Dubbo 默认会自动生成一个 com.xxx.XxxServiceRemote 接口,并继承 java.rmi.Remote 接口,并以该接口暴露服务;
      • 但是,如果设置了 <dubbo:protocol name="rmi" codec="spring"/>,Dubbo 将不会自动生成 Remote 接口,而是使用 Spring 提供的 RmiInvocationHandler 接口来暴露服务,可以和 Spring 兼容。
    • 由于 Dubbo 的 RMI 协议是基于 Java 原生 RMI 实现的,并且使用 JDK 序列化(二进制格式)机制,因此 Dubbo 应用(基于 Java 开发)无需引入第三方包就可以直接使用 RMI 协议
  • RMI 协议的的配置示例

    • 服务提供者的配置
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <!-- 应用信息 -->
      <dubbo:application name="rmi-provider"/>

      <!-- 注册中心 -->
      <dubbo:registry address="zookeeper://127.0.0.1:2181"/>

      <!-- 使用 RMI 协议 -->
      <dubbo:protocol name="rmi" port="1099"/>

      <!-- 服务暴露 -->
      <dubbo:service interface="com.example.api.HelloService" ref="helloServiceImpl"/>

      <!-- 服务实现 -->
      <bean id="helloServiceImpl" class="com.example.provider.HelloServiceImpl"/>
    • 服务消费者的配置
      1
      2
      3
      4
      5
      6
      7
      8
      <!-- 应用信息 -->
      <dubbo:application name="rmi-consumer"/>

      <!-- 注册中心 -->
      <dubbo:registry address="zookeeper://127.0.0.1:2181"/>

      <!-- 服务引用 -->
      <dubbo:reference id="helloService" interface="com.example.api.HelloService"/>

HTTP 协议

HTTP 协议的介绍

  • HTTP 协议的简介

    • 基于 HTTP 的远程过程调用协议,采用 Spring 的 HttpIvoker 实现。
    • 使用 HTTP 协议可以让服务通过 HTTP 端口暴露,便于浏览器或者非 Dubbo 客户端调用。
    • Dubbo 通过 Protocol SPI 扩展支持 HTTP 协议,但功能有限。如果需要支持浏览器 JSON/HTTP 调用,通常需要配合 Jsonrpc4j 等一起使用。
  • HTTP 协议的特性

    • 连接个数:多连接
    • 连接方式:短连接
    • 传输协议:HTTP 协议
    • 传输方式:同步通信
    • 序列化:JSON 序列化
    • 跨语言支持:支持跨语言
    • 适用范围:
      • 服务提供者数量通常多于服务消费者数量
      • 可通过浏览器直接访问和调试服务
      • 参数可以通过 URL 或表单方式传递
      • 暂不适合传输大文件
    • 适用场景:
      • 适合需要同时被应用程序和浏览器端 JavaScript 调用的服务
  • HTTP 协议的注意事项

    • Dubbo 的 HTTP 协议并不是标准 REST/HTTP,而是基于 JSON-RPC 的 RPC 调用,强烈建议 Dubbo 使用 REST 协议来替代 HTTP 协议。
    • Dubbo 的 HTTP 协议要求:
      • 必须是 POST 请求
      • HTTP 请求体必须严格符合的 JSON-RPC 格式
    • Dubbo 使用 HTTP 协议后,可以通过 curl 等网络工具直接调用 Provider 对外提供的 JSON-RPC 服务,URL 格式:http://ip:port/<接口全限定名>,比如:
      1
      2
      3
      4
      # curl 调用命令
      curl -X POST http://127.0.0.1:8030/com.clay.dubbo.service.DemoService \
      -H "Content-Type: application/json" \
      -d '{"jsonrpc":"2.0", "method":"sayHello","params":["Jim"],"id":1}'
      1
      2
      # 返回结果
      {"jsonrpc":"2.0","id":1,"result":"Hello Jim"}

HTTP 协议的使用

下载 Dubbo 使用 HTTP 协议的案例代码

  • Dubbo 使用 HTTP 协议的完整案例代码可以直接从 GitHub 下载对应章节 dubbo-lesson-15。
  • HTTP 协议支持的 Server

    • 目前在 Dubbo 2 中,HTTP 协议可以跑在以下几种不同的 Server 上,分别是:
      • Jetty:基于嵌入式 Jetty 的 HTTP Server,通过 <dubbo:protocol name="rest" port="8080" server="jetty"/> 来配置。
      • Tomcat:基于嵌入式 Tomcat 的 HTTP Server,通过 <dubbo:protocol name="rest" port="8080" server="tomcat"/> 来配置。
      • Servlet:采用外部应用服务器的 Servlet 容器(如外部 Tomcat)来做 HTTP Server,通过 <dubbo:protocol name="rest" server="servlet"/> 来配置,这里不需要配置端口,另外还需要在 web.xml 中做额外的其他配置。
  • HTTP 协议的配置示例

    • 服务提供者中的配置:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      <!-- Dubbo 核心包 -->
      <dependency>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo-spring-boot-starter</artifactId>
      <version>2.7.23</version>
      </dependency>
      <!-- Dubbo 的 HTTP 协议扩展 -->
      <dependency>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo-rpc-http</artifactId>
      <version>2.7.23</version>
      </dependency>
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <!-- 应用信息 -->
      <dubbo:application name="http-provider"/>

      <!-- 注册中心 -->
      <dubbo:registry address="zookeeper://127.0.0.1:2181"/>

      <!-- 使用 HTTP 协议 -->
      <dubbo:protocol name="http" port="8030" server="tomcat"/>

      <!-- 服务暴露 -->
      <dubbo:service interface="com.example.api.HelloService" ref="helloServiceImpl"/>

      <!-- 服务实现 -->
      <bean id="helloServiceImpl" class="com.example.provider.HelloServiceImpl"/>
    • 服务消费者中的配置:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      <!-- Dubbo 核心包 -->
      <dependency>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo-spring-boot-starter</artifactId>
      <version>2.7.23</version>
      </dependency>
      <!-- Dubbo 的 HTTP 协议扩展 -->
      <dependency>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo-rpc-http</artifactId>
      <version>2.7.23</version>
      </dependency>
      1
      2
      3
      4
      5
      6
      7
      8
      <!-- 应用信息 -->
      <dubbo:application name="http-consumer"/>

      <!-- 注册中心 -->
      <dubbo:registry address="zookeeper://127.0.0.1:2181"/>

      <!-- 服务引用 -->
      <dubbo:reference id="helloService" interface="com.example.api.HelloService"/>
    • 当 Dubbo 成功使用 HTTP 协议作为通信协议后,可以通过 curl 等网络工具直接调用 Provider 对外提供的 JSON-RPC 服务,这样服务就可以同时被应用程序和浏览器端 JavaScript 直接调用
      1
      2
      3
      4
      # curl 调用命令
      curl -X POST http://127.0.0.1:8030/com.clay.dubbo.service.DemoService \
      -H "Content-Type: application/json" \
      -d '{"jsonrpc":"2.0", "method":"sayHello","params":["Jim"],"id":1}'
      1
      2
      # 返回结果
      {"jsonrpc":"2.0","id":1,"result":"Hello Jim"}

Thrift 协议

Thrift 协议的介绍

  • Thrift 协议的简介

    • Apache Thrift 协议最初是由 Facebook 开源的高性能、跨语言的远程过程调用(RPC)协议,它通过接口定义语言(IDL)定义服务,并使用二进制编码格式进行高效的数据序列化和通信。
    • 当前 Dubbo 的 Thrift 协议是对 Apache Thrift 原生协议的扩展,即在 Apache Thrift 原生协议的基础上添加了一些额外的头信息,比如 service namemagic number 等。
    • 使用 Dubbo 的 Thrift 协议时,需要先使用 Apache Thrift 的 IDL 编译器根据 .thrift 文件编译生成相应的 Java 接口代码(使用流程类似 gRPC),后续版本中会在这方面做一些增强。
    • Dubbo 的 Thrift 协议与 Apache Thrift 原生协议不兼容,因此无法直接实现跨语言互相调用,仅用在 Dubbo 内部服务之间的通信。
  • Thrift 协议的特性

    • 连接个数:多连接(基于 TCP,支持并发连接)
    • 连接方式:长连接(保持会话连接,减少 TCP 握手开销)
    • 传输协议:TCP 协议
    • 传输方式:同步通信
    • 跨语言支持:不支持跨语言
    • 序列化:Apache Thrift 原生协议自带的二进制序列化
    • 适用范围:
      • 服务接口通过 .thrift 文件定义,需提前生成服务接口的代码
      • 适合高性能、低延迟的 RPC 场景
    • 适用场景:
      • 适合后端微服务间高频通信(如金融、游戏、即时通讯等领域)
      • 适合对性能要求较高、接口定义稳定的 Dubbo 内部服务
  • Thrift 协议的使用步骤

    • (1) 从 Thrift 官网 下载 Thrift
      • Windows 平台:可以直接下载 Thrift 的二进制可执行文件,然后配置 Thrift 的环境变量
      • Linux 平台:可以下载 Thrift 的源码包,然后手动编译安装 Thrift
    • (2) 定义 .thrift 文件(IDL)
    • (3) 根据 .thrift 文件,通过 thrift 命令自动生成 Java 接口文件
      • 比如:thrift -r -gen java HelloThrift.thrift
    • (4) 根据自动生成的 Java 接口文件,实现相应的 Java 接口
    • 特别注意thrift 命令生成的 Iface 类是一个 Java 内部类,在 XML 中通过 Spring Bean 定义引用时,不可以用 . 符号连接,而是需要用 $ 符号连接,比如:
      • 错误写法:<dubbo:service interface="com.clay.dubbo.thrift.HelloThrift.Iface" ref="helloThrift" />
      • 正确写法:<dubbo:service interface="com.clay.dubbo.thrift.HelloThrift$Iface" ref="helloThrift" />
  • Thrift 协议的注意事项

    • Thrift 协议不支持 null 值,也就是不能在 Thrift 协议中传递 null 值数据。
    • Dubbo 的 Thrift 协议与 Apache Thrift 原生协议不兼容,因此无法直接实现跨语言互相调用,仅用在 Dubbo 内部服务之间的通信。
      • Dubbo 的 Thrift 协议是基于 Apache Thrift 原生协议实现的扩展版本,用于 Dubbo 框架内部的 Thrift 序列化和网络传输支持,而非与 Apache Thrift 原生服务端或客户端直接交互;
      • 如果需要通过 Dubbo 实现跨语言的 RPC 调用,建议使用 Dubbo Triple 协议(与 gRPC 完全兼容)。
    • Dubbo 并未直接使用 Apache Thrift 原生协议的标准服务端(TServer / TProcessor)实现,而是在其协议层之上做了二次封装。
      • Dubbo 的 Thrift 协议虽然使用了 Apache Thrift 原生协议的序列化与网络传输机制,但仍保留了 Dubbo 自身的:
        • 服务导出机制
        • 请求头与协议帧格式
        • Service Name 识别方式
        • Dubbo 的线程池模型与 Invoker 调用机制
      • 由于这些封装,Dubbo 的 Thrift 协议在底层 Wire Protocol(报文格式 / 字节流格式) 上与 Apache Thrift 原生协议不同,即使接口定义一致,也无法互通。
    • Thrift 协议在 Dubbo 里的地位相当于 gRPC 协议 —— 它同时定义通信格式和数据序列化格式。
      • Dubbo 的 Thrift 协议属于通信协议,但它内部包含了 Apache Thrift 原生协议自带的序列化机制;
      • 在 Dubbo 的 Thrift 协议中,它并不使用 Dubbo 自己的序列化机制,而是直接使用 Apache Thrift 原生协议内置的编码 / 解码机制(即序列化机制);
      • 在 Dubbo 的 Thrift 协议中,通信格式由 Apache Thrift 原生协议定义,因此它是一个带自有序列化机制的 Dubbo 通信协议,序列化机制由 Apache Thrift 原生协议提供。

Thrift 协议的使用

Thrift 的安装
  • Thrift 编译安装
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
# 下载源码压缩包
wget http://archive.apache.org/dist/thrift/0.12.0/thrift-0.12.0.tar.gz

# 解压源码压缩包
tar -xvf thrift-0.12.0.tar.gz

# 进入解压目录
cd thrift-0.12.0

# 生成构建系统
./bootstrap.sh

# 生成构建文件(Makefile),建议禁用所有语言库,只生成 Thrift 编译器,避免不必要的依赖和编译
./configure --without-cpp \
--without-c_glib \
--without-python \
--without-py3 \
--without-php \
--without-ruby \
--without-nodejs \
--without-perl \
--without-go \
--without-lua \
--without-haxe \
--without-dart \
--without-erlang \
--without-rust \
--without-swift \
--without-csharp \
--without-kotlin \
--without-d \
--without-java-script \
--without-java

# 编译源代码
make -j4

# 安装到系统路径(包括可执行文件、头文件和库)
sudo make install
  • Thrift 验证安装
1
2
3
4
5
6
7
8
9
10
11
# 查看 Thrift 的版本
thrift --version

# 创建 Thrift 文件
echo 'namespace java com.clay.dubbo.thrift service HelloThrift { string sayHello(1:string name) }' > HelloThrift.thrift

# 编译 Thrift 文件,生成 Java 接口文件
thrift -r -gen java HelloThrift.thrift

# 查看生成的 Java 接口文件内容(如下所示)
cat gen-java/com/dubbo/study/thrift/HelloThrift.java
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
/**
* Autogenerated by Thrift Compiler (0.12.0)
*
* DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
* @generated
*/
package com.clay.dubbo.thrift;

@SuppressWarnings({"cast", "rawtypes", "serial", "unchecked", "unused"})
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.12.0)", date = "2019-09-11")
public class DemoThrift {

public interface Iface {

public java.lang.String sayHello(java.lang.String name) throws org.apache.thrift.TException;

}

public interface AsyncIface {

public void sayHello(java.lang.String name, org.apache.thrift.async.AsyncMethodCallback<java.lang.String> resultHandler) throws org.apache.thrift.TException;

}

......(省略)

}
  • 若希望卸载 Thrift(比如想安装其他版本),可以执行以下命令
1
2
3
4
5
6
7
8
9
10
11
# 卸载可执行文件
sudo rm -f /usr/local/bin/thrift*

# 卸载库文件
sudo rm -f /usr/local/lib/libthrift*

# 卸载头文件
sudo rm -rf /usr/local/include/thrift

# 卸载文档资源
sudo rm -rf /usr/local/share/doc/thrift
Thrift 的使用

下载 Dubbo 使用 Thrift 协议的案例代码

本节将演示 Dubbo 如何使用 Thrift 作为通信协议,完整的案例代码可以直接从 GitHub 下载对应章节 dubbo-lesson-14。

API 模块的代码与配置

  • (1) 在 API 模块中,创建 Thrift 的 IDL 文件(比如 DemoThrift.thrift
1
2
3
4
5
namespace java com.clay.dubbo.thrift

service DemoThrift {
string sayHello(1:string name)
}
  • (2) 在 API 模块中,通过 thrift 命令生成 Java 接口文件,并拷贝到 src/main/java 目录下
1
2
# 编译 Thrift 文件,生成 Java 接口文件
thrift -r -gen java DemoThrift.thrift
  • (3) 在 API 模块中,引入 Dubbo 的 Thrift 依赖
1
2
3
4
5
6
<!-- Dubbo 的 Thrift 协议扩展 -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-rpc-thrift</artifactId>
<version>2.7.23</version>
</dependency>

特别注意

  • 这个 dubbo-rpc-thrift 模块实现了 Dubbo Protocol SPI 扩展,支持 Thrift RPC 调用;内部依赖了 Apache Thrift 库(libthrift),但大多数 Maven 坐标会自动拉取。
  • Thrift 安装的二进制版本(即 thrift 命令的版本),强烈建议跟 Apache Thrift 库(libthrift)的版本保证一致,否则通过 thrift 命令生成的 Java 代码可能无法正常编译。

Privoder 模块的代码与配置

  • (1) 在 Provider 模块中,引用 API 模块与 Dubbo 的依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- API 模块 -->
<dependency>
<groupId>com.clay.dubbo</groupId>
<artifactId>dubbo-lesson-14-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- Dubbo 核心包 -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.23</version>
</dependency>

  • (2) 在 Provider 模块中,实现 Thrift 生成的 Java 接口,并暴露 Dubbo 服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.clay.dubbo.thrift.DemoThrift;
import org.apache.dubbo.config.annotation.DubboService;
import org.apache.thrift.TException;

/**
* 暴露服务
*/
@DubboService
public class DemoServiceImpl implements DemoThrift.Iface {

@Override
public String sayHello(String name) throws TException {
return null;
}

}
  • (3) 在 Provider 模块中,添加对应的 Dubbo 配置内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server:
port: 9090

spring:
application:
name: dubbo-provider-application

dubbo:
# 服务信息
application:
name: ${spring.application.name}
# 注册中心地址
registry:
address: zookeeper://192.168.2.235:2181
# 服务提供者的协议
protocol:
name: thrift # 指定使用 Thrift 协议
port: 3030
# 扫描 Dubbo 相关的注解
scan:
base-packages: com.clay.dubbo.provider

Consumer 模块的代码与配置

  • (1) 在 Consumer 模块中,引用 API 模块与 Dubbo 的依赖
1
2
3
4
5
6
7
8
9
10
11
12
<!-- API 模块 -->
<dependency>
<groupId>com.clay.dubbo</groupId>
<artifactId>dubbo-lesson-14-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- Dubbo 核心包 -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.23</version>
</dependency>
  • (2) 在 Consumer 模块中,通过 Thrift 生成的 Java 接口引用 Dubbo 服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.apache.dubbo.config.annotation.DubboReference;
import com.clay.dubbo.thrift.DemoThrift;
import org.apache.thrift.TException;

@RestController
public class DemoController {

/**
* 引用 Dubbo 服务
*/
@DubboReference
private DemoThrift.Iface demoService;

@GetMapping("/sayHello/{name}")
public String sayHello(@PathVariable("name") String name) throws TException {
String result = demoService.sayHello(name);
log.info("===> " + result);
return result;
}

}
  • (3) 在 Consumer 模块中,添加对应的 Dubbo 配置内容
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
server:
port: 9091

spring:
application:
name: dubbo-consumer-application

dubbo:
# 服务信息
application:
name: ${spring.application.name}
# 注册中心地址
registry:
address: zookeeper://192.168.2.235:2181
# 扫描 Dubbo 相关的注解
scan:
base-packages: com.clay.dubbo.consumer
# 消费行为配置
consumer:
# 关闭了启动检查,这样消费者启动时,不会到注册中心里面检查服务提供者是否存在
check: false
# 建议统一配置为不重试请求,对于查询等幂等操作来说可以在代码中单独配置重试次数
retries: 0
# 默认情况下限制请求必须在 1000 毫秒内完成,对于具体服务可以在代码中单独配置
timeout: 1000

Consumer 调用 RPC 服务失败

  • Dubbo 配置使用 Thrift 作为通信协议后,如果在 Consumer 调用 Provider 提供的服务时,出现超时现象,则可能是缺少 slf4j-log4j12 包导致,详细说明请看 这里,但亲测可能还有其他原因会导致 RPC 调用超时。

Dubbo 协议

协议的简单介绍

Dubbo 协议是官方默认协议,其构成、特性、约束如下:

  • Dubbo 协议的构成

    • 网络传输框架:Mina、Netty、Grizzy
    • 序列化机制:Hessian2、FastJson、GSON、Protobuf、Kryo、FST、Avro、Java 等
    • 线程分发策略:alldirectmessageexecutionconnection
    • 线程池类型:fixedcachedlimitedeager
  • Dubbo 协议的特性

    • 连接个数:单连接(基于 TCP,支持多路复用)
    • 连接方式:长连接(保持会话连接,减少 TCP 握手开销)
    • 传输协议:TCP 协议
    • 传输方式:NIO 异步通信
    • 跨语言支持:不支持跨语言
    • 序列化协议:Hessian2 二进制序列化
    • 适用场景:
      • 常规的远程过程调用(RPC)
    • 适用范围:
      • 服务消费者比服务提供者个数多
      • 单一服务消费者无法压满服务提供者
      • 传输的数据包较小(建议小于 100K)
      • 尽量不要用 Dubbo 协议传输大文件(如音频、视频)或超大字符串
  • Dubbo 协议的注意事项

    • 方法参数及返回值需要实现 Serializable 接口
    • 方法参数及返回值不能自定义实现 ListMapNumberDateCalendar 等接口,只能用 JDK 自带的实现(如 HashMap),因为 Hessian2 序列化会做特殊处理,自定义实现类中的属性值都会丢失
    • 对于 Hessian2 序列化,只会传成员属性值和值的类型,不会传方法或者静态变量,兼容情况如下表所示:

协议的注意事项

  • 为什么 Dubbo 协议不适合传输大数据、大文件的场景?

    • 核心原因:
      • 因为 Dubbo 协议基于长连接和内存缓冲的 RPC 框架,它会将请求数据整体序列化、加载到内存再发送;
      • 传输大文件会占用大量内存、阻塞网络 I/O 线程、导致 TCP 连接阻塞或超时;
      • 大文件(如音频、视频)传输这类场景,更适合使用 HTTP、FTP、OSS 等流式传输协议。
    • 举个例子:
      • 前提条件:单一长连接、网络为千兆网卡(1024Mbit = 128MByte);
      • 假设每个请求的数据包大小为 500KByte
      • 假设每条连接最大只能压满 7MByte(不同环境可能不一样,仅供参考);
      • 单个服务提供者的最大 TPS:128MByte / 500KByte = 262
      • 单个服务消费者调用单个服务提供者的最大 TPS:7MByte / 500KByte = 14
      • 如果能接受相应的 TPS,可以考虑使用 Dubbo 协议,否则网络将成为性能瓶颈。
  • 为什么 Dubbo 协议适合消费者比提供者个数多的场景?

    • 核心原因:
      • Dubbo 协议基于长连接和 NIO 异步通信,服务消费者与服务提供者之间会建立少量长连接,多个请求会复用这些长连接;
      • 当服务消费者的数量远多于服务提供者时,连接可复用、资源占用低、性能更高;
      • 反之,若服务提供者更多,会造成连接过多、内存和句柄开销大,效率反而下降。
    • 举个例子:
      • 前提条件:单一长连接、网络为千兆网卡(1024Mbit = 128MByte);
      • 根据测试经验数据,每条连接最多只能压满 7MByte(不同环境可能不一样,仅供参考);
      • 理论上 1 个服务提供者需要 18(128MByte / 7MByte = 18)个服务消费者才能压满网卡。
  • 为什么 Dubbo 协议采用单一长连接和 NIO 异步通信?

    • 核心原因:
      • 因为在大多数场景下,服务提供者数量远少于服务消费者,例如某个核心服务只有几台提供者机器,却有成百上千个消费者同时调用;
      • 如果使用传统的短连接(如 Hessian 服务),频繁地建立与销毁连接会导致服务端资源耗尽、性能下降;
      • Dubbo 通过单一长连接维持服务消费者与服务提供者的通信,可以避免过多连接压垮服务端;
      • 同时采用 NIO 异步通信和线程池复用,提升并发处理能力,有效避免 C10K 问题。

协议的详细配置

Dubbo 协议的配置示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
dubbo:
protocol:
# 协议名称与端口号
name: dubbo
port: 20880
# 服务端、客户端的底层通讯实现
server: netty
client: netty
# 编解码、序列化
codec: dubbo
serialization: hessian2
charset: UTF-8
# 线程、队列配置
dispatcher: all
threadpool: fixed
threads: 100
queues: 0
lothreads: 9
# 缓存区大小、接受的最大连接数、请求与响应的数据包大小
buffer: 8192
accepts: 1000
payload: 8388608

协议的报文格式

  • Dubbo 协议的报文结构图

  • Dubbo 协议的消息头(固定 16 个字节)
字段长度作用
Magic Number2 字节魔数(通信协议标识),Dubbo 协议是固定值 0xdabb
Flag1 字节请求 / 响应标识
Status1 字节响应状态
Request ID8 字节请求的唯一标识
Body Length4 字节请求体的长度
  • Dubbo 协议的消息体(具体的业务操作内容)
字段名说明
RPC 版本 Dubbo RPC 协议的版本号(比如 2.0.2
服务接口路径服务接口的全限定名(比如 com.example.UserService
服务版本号服务的版本信息(比如 1.0.0
服务方法名调用的具体方法名称(比如 getUserById
参数描述符方法参数类型描述字符串(比如 Ljava/lang/String; 表示 String 类型参数)
参数值序列化参数值的序列化内容(根据序列化协议如 Hessian2、JSON 等序列化后的字节流)
Dubbo 内置参数 Dubbo 扩展的键值对参数(比如 pathtimeoutgrouploadbalance 等)

协议的多路复用机制

Dubbo 协议(官方默认协议)支持单连接并发请求,因为它在 TCP 协议之上通过 Netty 和自定义通信协议实现了同一条 TCP 连接上的多路复用机制。每个请求都有唯一的 Request ID,响应则根据 Request ID 匹配到对应的调用线程,从而突破了 TCP 串行阻塞的限制。这就是 Dubbo 协议的多路复用机制,也是其高性能的主要原因。虽然 TCP 本质上是有序字节流,但 Dubbo 协议允许多个 RPC 请求在同一连接上交错传输,不必等待 “一个请求发送完、响应回来后才处理下一个请求”,实现了真正的并发请求。如果单连接仍存在性能瓶颈,可以通过调高 connections 参数,让 Consumer 为每个 Provider 建立多条物理 TCP 连接来分担流量。

特别注意

  • Dubbo 协议的的多路复用机制,本质上复用的是 TCP 连接,也就是在同一条物理 TCP 连接上同时承载多个 RPC 请求和响应,从而减少连接建立的开销,并显著提升吞吐量。
  • 这跟常说的 I/O 多路复用机制(比如 pollepoll)完全不同,因为 epoll 等 I/O 多路复用机制本质上是在操作系统层面复用线程,两者概念完全不同。
  • 基础认知:TCP 是有序字节流

    • TCP 连接本身提供的只是可靠的、有序的字节流,并不关心应用层消息的边界。
    • 如果应用层不做任何设计,那么它只能按顺序处理请求:
      • (1) 客户端发出请求 A
      • (2) 客户端等待服务端响应 A
      • (3) 客户端才能发送请求 B
    • 这就像一条 “管道”,只能一个一个地走,非常低效。
    • 典型例子:HTTP/1.0 早期,每次请求必须等上一次响应返回后才能发起下一次,这就是队头阻塞(Head-of-Line Blocking)。
  • Dubbo 协议的报文结构

    • Dubbo 协议的消息头固定 16 字节,包含关键信息:
    字段长度作用
    Magic Number2 字节魔数(通信协议标识),Dubbo 协议是固定值 0xdabb
    Flag1 字节请求 / 响应标识
    Status1 字节响应状态
    Request ID8 字节请求的唯一标识
    Body Length4 字节请求体的长度
    • Dubbo 协议的报文格式:
      • | Header(16 bytes) | Body(variable) |
    • Dubbo 处理报文的流程:
      • Consumer 发起 RPC 调用时,生成一个全局唯一的 Request ID
      • Provider 处理完请求后,响应中带回相同的 Request ID
      • Consumer 根据 Request ID 将响应结果分发到正确的调用线程
  • Dubbo 协议的多路复用机制

    • 假设 Consumer 同时发起 3 个 RPC 请求(A、B、C),请求体会被打包,如下:

      1
      Consumer 的 TCP 发送缓冲区:|Header(ID=1)|BodyA|Header(ID=2)|BodyB|Header(ID=3)|BodyC|
    • Provider 可以并发处理这 3 个请求,并按处理完成的先后顺序返回结果,如下:

      1
      Consumer 的 TCP 接收缓冲区:|Header(ID=2)|RespB|Header(ID=1)|RespA|Header(ID=3)|RespC|
    • Consumer 接收到响应结果后:

      • 解析消息头,取出 Request ID
      • 将 RespB 分发给发起 B 请求的线程
      • 将 RespA 分发给 A 请求线程
      • 将 RespC 分发给 C 请求线程
    • 这并行和串行的区别:

      • 在 Dubbo 中,即使只有一条 TCP 连接,也能同时跑成百上千的并发请求。

        特性串行请求(无 Request ID)Dubbo 并发请求(带 Request ID)
        TCP 数据组织每次只能传一条完整消息多条消息交错在同一流里
        发送端必须等待上一个响应回来才能发送下一个请求可以连续发送多个请求
        响应顺序响应必须严格按请求顺序返回响应顺序与请求顺序无关
        并发性能低,队头阻塞严重高,类似 HTTP/2 多路复用
  • Dubbo 协议多路复用机制的工作流程

    • (1) 客户端发起调用
      • Consumer 应用线程调用代理对象
      • Dubbo 在客户端生成一个唯一的 Request ID 并将调用参数序列化
    • (2) 写入 Netty Channel
      • 所有调用请求都会被写入同一条 Netty Channel
      • 发送数据时不会阻塞,只要 TCP 发送缓冲区有空间,就可以写入
    • (3) 服务端并发处理
      • Provider 使用线程池同时处理多个请求
      • 不要求按请求顺序处理,哪个先处理完就先返回
    • (4) 返回结果匹配
      • Consumer 维护一个 ConcurrentHashMap<Long, Future>
      • Key = Request IDValue = 该请求对应的 Future
      • Consumer 收到响应时,根据响应里的 Request ID 找到对应 Future,并唤醒等待线程(调用线程)
  • Dubbo 协议为什么还会出现 “单连接阻塞” 问题

    • 虽然 Dubbo 协议支持单连接并发请求,但底层 TCP 仍然是单通道,存在物理限制,主要体现在:
      • TCP 层的队头阻塞(Head-of-Line Blocking)
        • TCP 是有序字节流,一旦某个包丢失,后续所有数据都必须等待该包重传。
        • 如果网络丢包率高,即使 Dubbo 应用层并行,仍会被 TCP 阻塞。
      • Provider 中的线程池已满
        • 如果 Provider 的处理线程不足,Consumer 的请求虽然发出去了,但在服务端被排队。
      • 单连接带宽不足
        • 多个请求争夺同一条 TCP 通道的带宽时,整体性能受限。
  • Dubbo 多路复用机制的类比理解

    • 可以将 Dubbo 多路复用机制类比为高速公路:
    场景类比
    串行模式高速公路只有 1 条车道,车必须按顺序行驶,前车没到终点,后车不能走
    Dubbo 多路复用高速公路有多条虚拟车道(Request ID),虽然实际物理只有 1 条道路,但每辆车都有自己的标记,不会互相等待
  • Dubbo 不同通信协议对单连接并发请求的支持情况

通信协议类型是否支持单连接并发请求(多路复用机制)说明
Dubbo 协议(默认)✅ 支持通过自定义协议头 + Request ID 实现多路复用,这是 Dubbo 高性能的核心。
Triple 协议(gRPC)✅ 支持基于 HTTP/2,本身支持多路复用,天然具备并行能力。
RMI、Hessian、WebService 等传统协议❌ 不支持基于同步调用或无多路复用设计,一个请求未返回响应前,后续请求无法在同一连接并行发送。

实战手写 Dubbo 客户端

学习目标与代码下载

  • 这里将基于 Dubbo 协议的报文格式 + TCP 协议手写一个 Dubbo 客户端,目的是熟悉 Dubbo 协议的报文格式。
  • 为了方便演示,服务提供者(Provider)使用的序列化协议为 FastJSON,而通信协议为 Dubbo 协议,注册中心使用 ZooKeeper。
  • 由于篇幅有限,下面只给出服务消费者(Consumer)的核心代码,而服务提供者(Provider)的代码不再累述,完整的案例代码可以直接从 GitHub 下载对应章节 dubbo-lesson-09

案例代码

  • ZooKeeper 中的数据存储结构
1
2
3
4
5
6
7
8
9
10
11
12
13
/dubbo
└── com.clay.dubbo.service.DemoService
├── providers
│ └── dubbo%3A%2F%2F192.168.233.1%3A20880%2Fcom.clay.dubbo.service.DemoService%3Fanyhost%3Dtrue%26application%3Ddubbo-provider-application%26deprecated%3Dfalse%26dubbo%3D2.0.2%26dynamic%3Dtrue%26generic%3Dfalse%26interface%3Dcom.clay.dubbo.service.DemoService%26metadata-type%3Dremote%26methods%3DsayHello%26pid%3D47362%26release%3D2.7.23%26serialization%3Dfastjson%26service.name%3DServiceBean%3A%2Fcom.clay.dubbo.service.DemoService%26side%3Dprovider%26timestamp%3D1758794536318

├── consumers
│ └── empty

├── configurators
│ └── empty

└── routers
└── empty
  • 核心依赖(服务消费者端)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>2.7.23</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.5.0</version>
</dependency>
<dependency>
<groupId>com.clay.dubbo</groupId>
<artifactId>dubbo-lesson-09-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
  • 核心代码(服务消费者端)
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import com.alibaba.fastjson.JSON;
import com.clay.dubbo.service.DemoService;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.io.Bytes;

import java.net.InetSocketAddress;
import java.net.URLDecoder;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Random;

public class ConsumerApplication {

// ZooKeeper 连接地址(多个集群节点使用逗号分割)
private static final String ADDRESS = "127.0.0.1:2181";

// ZooKeeper 会话超时时间
private static final int SESSION_TIMEOUT = 2000;

// 根节点的路径
private static final String ROOT_PATH = "/dubbo";

// 服务节点的路径
private static final String SERVICE_PATH = ROOT_PATH + "/" + DemoService.class.getName() + "/providers";

/**
* 获取 ZK 客户端
*/
public CuratorFramework getCuratorFramework() {
// 重试策略
ExponentialBackoffRetry backoffRetry = new ExponentialBackoffRetry(3000, 3);

// 创建 ZK 客户端
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString(ADDRESS)
.connectionTimeoutMs(SESSION_TIMEOUT)
.retryPolicy(backoffRetry)
.build();

// 启动 ZK 客户端
client.start();

return client;
}

public void run() throws Exception {
// 获取服务地址列表
CuratorFramework zkClient = getCuratorFramework();
List<String> providerUrls = zkClient.getChildren().forPath(SERVICE_PATH);

if (providerUrls.size() == 0) {
System.out.println("服务地址列表为空,客户端退出");
System.exit(0);
}

// 随机选取一个服务地址
int index = new Random().nextInt(providerUrls.size());
String providerUrl = providerUrls.get(index);
System.out.println("未解码的 URL: " + providerUrl);

// 对 Provider URL 进行解码
String decodeUrl = URLDecoder.decode(providerUrl, StandardCharsets.UTF_8.name());
System.out.println("解码后的 URL: " + decodeUrl);

// 获取 Provider 的信息
URL url = URL.valueOf(decodeUrl);
String protocol = url.getProtocol(); // dubbo
String host = url.getHost(); // 192.168.233.1
int port = url.getPort(); // 20880
String path = url.getPath(); // com.clay.dubbo.service.DemoService

// 创建 TCP 客户端
SocketChannel dubboClient = SocketChannel.open();
dubboClient.connect(new InetSocketAddress(host, port));

// 发送的数据(消息体),基于 FastJSON 序列化协议,以下内容必须按顺序组织
StringBuffer bodyString = new StringBuffer();
// Dubbo 协议的版本
bodyString.append(JSON.toJSONString("2.0.2")).append("\r\n");
// 接口名称
bodyString.append(JSON.toJSONString(path)).append("\r\n");
// 接口版本
bodyString.append(JSON.toJSONString("0.0.0")).append("\r\n");
// 方法名称
bodyString.append(JSON.toJSONString("sayHello")).append("\r\n");
// 参数描述
bodyString.append(JSON.toJSONString("Ljava/lang/String;")).append("\r\n");
// 参数值(经过序列化)
bodyString.append(JSON.toJSONString("Jim")).append("\r\n");
// 附加参数(用于扩展 Dubbo 功能)
bodyString.append("{}").append("\r\n");
byte[] body = bodyString.toString().getBytes();

// 发送的数据(消息头),固定 16 个字节
byte[] header = new byte[16];
// 魔数,协议标识(2 个字节)
byte[] magicArray = Bytes.short2bytes((short) 0xdabb);
System.arraycopy(magicArray, 0, header, 0, 2);
// 请求 / 响应标识(1 个字节)
header[2] = (byte) 0xc6;
// 响应状态(1 个字节)
header[3] = 0x00;
// 请求 ID(8 个字节)
byte[] requestId = Bytes.long2bytes(1);
System.arraycopy(requestId, 0, header, 4, 8);
// 消息体的长度(4 个字节)
byte[] bodyLength = Bytes.int2bytes(body.length);
System.arraycopy(bodyLength, 0, header, 12, 4);

// 封装请求报文
byte[] request = new byte[header.length + body.length];
System.arraycopy(header, 0, request, 0, header.length);
System.arraycopy(body, 0, request, header.length, body.length);

// 发送 RPC 请求
dubboClient.write(ByteBuffer.wrap(request));

// 获取响应结果
ByteBuffer response = ByteBuffer.allocate(1024);
dubboClient.read(response);

// 准备读取响应结果
response.flip();

// 打印响应结果
byte[] data = new byte[response.remaining()];
response.get(data);
System.out.println("RPC 调用结果:" + new String(data));
}

public static void main(String[] args) throws Exception {
ConsumerApplication application = new ConsumerApplication();
application.run();
}

}
  • 测试结果(服务消费者端),打印 RPC 调用结果时可能会出现部分内容乱码,但不影响整体的测试效果
1
2
3
4
5
6
7
未解码的 URL: dubbo%3A%2F%2F192.168.233.1%3A20880%2Fcom.clay.dubbo.service.DemoService%3Fanyhost%3Dtrue%26application%3Ddubbo-provider-application%26deprecated%3Dfalse%26dubbo%3D2.0.2%26dynamic%3Dtrue%26generic%3Dfalse%26interface%3Dcom.clay.dubbo.service.DemoService%26metadata-type%3Dremote%26methods%3DsayHello%26pid%3D47362%26release%3D2.7.23%26serialization%3Dfastjson%26service.name%3DServiceBean%3A%2Fcom.clay.dubbo.service.DemoService%26side%3Dprovider%26timestamp%3D1758794536318

解码后的 URL: dubbo://192.168.233.1:20880/com.clay.dubbo.service.DemoService?anyhost=true&application=dubbo-provider-application&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.clay.dubbo.service.DemoService&metadata-type=remote&methods=sayHello&pid=47362&release=2.7.23&serialization=fastjson&service.name=ServiceBean:/com.clay.dubbo.service.DemoService&side=provider&timestamp=1758794536318

RPC 调用结果:ڻ  4
"Hello Jim"
{"dubbo":"2.0.2"}

参考资料