一文详细解读 Dubbo 中的 Http 协议

太阳红彤彤,花儿五颜六色,各位读者朋友好,又来到了分享 Dubbo 知识点的时候了。说到 Dubbo 框架支持的协议,你的第一反应是什么?大概会有 Dubbo 默认支持的 dubbo 协议,以及老生常谈的由当当贡献给 Dubbo 的 rest 协议,或者是今天的主角 http。截止到目前,Dubbo 最新版本演进到了 2.7.3,已经支持了:dubbo,hessain,http,injvm,jsonrpc,memcached,native-thrift,thrift,redis,rest,rmi,webservice,xml 等协议,有些协议的使用方式还没有补全到官方文档中。原来 Dubbo 支持这么多协议,是不是有点出乎你的意料呢?

一文详细解读 Dubbo 中的 Http 协议

这么多 RPC 协议,可能有人会产生如下的疑问:rest,jsonrpc,webservice 不都是依靠 http 通信吗?为什么还单独有一个 http 协议?先不急着回答这个问题,而是引出今天的话题,先来介绍下 Dubbo 框架中所谓的 http 协议。

Dubbo 中的 http 协议

在 Dubbo 使用 http 协议和其他协议基本一样,只需要指定 protocol 即可。

  1. <dubbo:protocol name="http" port="8080" server="jetty" /> 

server 属性可选值:jetty,tomcat,servlet。

配置过后,当服务消费者向服务提供者发起调用,底层便会使用标准的 http 协议进行通信。可以直接在 https://github.com/apache/dubbo-samples 中找到官方示例,其中的子模块:dubbo-samples-http 构建了一个 http 协议调用的例子。

为避免大家误解,特在此声明:本文中,所有的 http 协议特指的是 dubbo 中的 http 协议,并非那个大家耳熟能详的通用的 http 协议。

http 协议的底层原理

从默认的 dubbo 协议改为 http 协议是非常简单的一件事,上面便是使用者视角所看到的全部的内容了,接下来我们将会探讨其底层实现原理。

翻看 Dubbo 的源码,找到 HttpProtocol 的实现,你可能会吃惊,基本就依靠 HttpProtocol 一个类,就实现了 http 协议

一文详细解读 Dubbo 中的 Http 协议

要知道实现自定义的 dubbo 协议,有近 30 个类!http 协议实现的如此简单,背后主要原因有两点:

  • remoting 层使用 http 通信,不需要自定义编解码
  • 借助了 Spring 提供的 HttpInvoker 封装了 refer 和 exporter 的逻辑

Spring 提供的 HttpInvoker 是何方神圣呢?的确是一个比较生僻的概念,但并不复杂,简单来说,就是使用 Java 序列化将对象转换成字节,通过 http 发送出去,在 server 端,Spring 能根据 Url 映射,找到容器中对应的 Bean 反射调用的过程,没见识过它也不要紧,可以通过下面的示例快速掌握这一概念。

Spring HttpInvoker

本节内容可参见 Spring 文档:https://docs.spring.io/spring/docs/4.3.24.RELEASE/spring-framework-reference/htmlsingle/#remoting-httpinvoker-server

下面的示例将会展示如何使用 Spring 原生的 HttpInvoker 实现远程调用。

创建服务提供者

  1. public class AccountServiceImpl implements AccountService { 
  2.  @Override 
  3.  public Account findById(int id) { 
  4.  Account account = new Account(id, new Date().toString()); 
  5.  return account; 
  6.  } 
  7. @Bean 
  8. AccountService accountService(){ 
  9.  return new AccountServiceImpl(); 
  10. @Bean("/AccountService"
  11. public HttpInvokerServiceExporter accountServiceExporter(AccountService accountService){ 
  12.  HttpInvokerServiceExporter exporter = new HttpInvokerServiceExporter(); 
  13.  exporter.setService(accountService); 
  14.  exporter.setServiceInterface(AccountService.class); 
  15.  return exporter; 

暴露服务的代码相当简单,需要注意两点:

  1. org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter 是 Spring 封装的一个服务暴露器,它会以 serviceInterface 为公共接口,以 service 为实现类向外提供服务。
  2. @Bean("/AccountService") 不仅仅指定了 IOC 容器中 bean 的名字,还充当了路径映射的功能,如果本地服务器暴露在 8080 端口,则示例服务的访问路径为http://localhost:8080/AccountService

创建服务消费者

  1. @Configuration 
  2. public class HttpProxyConfig { 
  3.  @Bean("accountServiceProxy"
  4.  public HttpInvokerProxyFactoryBean accountServiceProxy(){ 
  5.  HttpInvokerProxyFactoryBean accountService = new HttpInvokerProxyFactoryBean(); 
  6.  accountService.setServiceInterface(AccountService.class); 
  7.  accountService.setServiceUrl("http://localhost:8080/AccountService"); 
  8.  return accountService; 
  9.  } 
  10. @SpringBootApplication 
  11. public class HttpClientApp { 
  12.  public static void main(String[] args) { 
  13.  ConfigurableApplicationContext applicationContext = SpringApplication.run(HttpClientApp.class, args); 
  14.  AccountService accountService = applicationContext.getBean(AccountService.class); 
  15.  System.out.println(accountService.findById(10086)); 
  16.  } 

消费者端引用服务同样有两个注意点:

  1. org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean 是 Spring 封装的一个服务引用器,serviceInterface 指定了生成代理的接口,serviceUrl 指定了服务所在的地址,与之前配置的服务暴露者的路径需要对应。
  2. HttpInvokerProxyFactoryBean 注册到容器之中时,会同时生成一个 AccountService 接口的代理类,由 Spring 封装远程调用的逻辑。

调用细节分析

对于 Spring HttpInvoker 的底层实现,就没必要深究了,但大家肯定还是会好奇一些细节:dubbo 中的 http 报文体是怎么组织的?如何序列化对象的?

我们使用 Wireshark 可以抓取到客户端发送的请求以及服务端响应的报文。

一文详细解读 Dubbo 中的 Http 协议

追踪报文流,可以看到详细的请求和响应内容

一文详细解读 Dubbo 中的 Http 协议

从 ContentType:application/x-java-serialized-object 和报文 Body 部分的 ASCII 码可以看出,使用的是 Java Serialize 序列化。我们将 Body 部分导出成文件,使用 Java Serialize 反序列化响应,来验证一下它的庐山真面目:

一文详细解读 Dubbo 中的 Http 协议

使用 Java Serialize 可以正常反序列化报文,得到结果是 Spring 内置的包装类 RemoteInvocationResult,里面装饰着实际的业务返回结果。

http 协议的意义

Dubbo 提供的众多协议有各自适用的场景,例如

  • dubbo://,dubbo 协议是默认的协议,自定义二进制协议;单个长连接节省资源;基于 tcp,架构于 netty 之上,性能还算可以;协议设计上没有足够的前瞻性,不适合做 service-mesh 谈不上多么优雅,但是好歹风风雨雨用了这么多年,周边也有不少配套组件例如 dubbo2.js, dubbo-go, dubbo-cpp,一定程度解决了多语言的问题。
  • webservice://,hession://,thrift:// 等协议,基本是为了适配已有协议的服务端/客户端,借助于 dubbo 框架的 api,可以使用其功能特性,意义不是特别大。
  • redis://,memcached:// 等协议,并非是暴露给用户配置的协议,一般是 dubbo 自用,在注册中心模块中会使用到相应的扩展

所有协议的具体使用场景和其特性,我可能会单独写文章来分析,而如今我们要思考的是 Dubbo 提供 http 协议到底解决什么问题,什么场景下用户会考虑使用 Dubbo 的 http 协议。

我个人认为 Dubbo 现如今的 http 协议比较鸡肋,原生 http 通信的优势在于其通用性,基本所有语言都有配套的 http 客户端和服务端支持,但是 Dubbo 的 http 协议却使用了 application/x-java-serialized-object 的格式来做为默认的 payload,使得其丧失了跨语言的优势。可能有读者会反驳:HttpInvoker 支持配置序列化格式,不能这么草率的诟病它。但其实我们所关注的恰恰是默认实现,正如 dubbo:// 协议也可以配置 fastjson 作为序列化方案,但是我们同样不认为 dubbo:// 协议是一个优秀的跨语言方案,理由是一样的。当然,评价一个应用层协议是否是优秀的,是否适合做 mesh 等等,需要多种方向去分析,这些我不在本文去分析。

说到底,本文花了一定的篇幅向大家介绍了 Dubbo 的 http 协议,到头来却是想告诉你:这是一个比较鸡肋的协议,是不是有些失望呢?不要失望,dubbo 可能在 2.7.4 版本废弃现有的 http 协议,转而使用 jsonrpc 协议替代,其实也就是将 jsonrpc 协议换了个名字而已,而关于 jsonrpc 的细节,我将会在下一篇文章中介绍,届时,我也会分析,为什么 jsonrpc 比现有的 http 协议更适合戴上 http 协议的帽子,至于现有的 http 协议,我更倾向于称之为:spring-httpinvoker 协议。

相关推荐