微服务架构已成为目前互联网架构的趋势,关于微服务的讨论,几乎占据了各种技术大会的绝大多数版面。国内使用最多的服务治理框架非阿里开源的 dubbo 莫属,千米网也选择了 dubbo 作为微服务治理框架。另一方面,和大多数互联网公司一样,千米的开发语言是多样的,大多数后端业务由 java 支撑,而每个业务线有各自开发语言的选择权,便出现了 nodejs,python,go 多语言调用的问题。
“专业、务实、高效、创新、把客户的事当成自己的事”是我们每一个人一直以来坚持追求的企业文化。 创新互联是您可以信赖的网站建设服务商、专业的互联网服务提供商! 专注于网站设计制作、网站制作、软件开发、设计服务业务。我们始终坚持以客户需求为导向,结合用户体验与视觉传达,提供有针对性的项目解决方案,提供专业性的建议,创新互联建站将不断地超越自我,追逐市场,引领市场!
跨语言调用是一个很大的话题,也是一个很有挑战的技术活,目前业界经常被提及的解决方案有如下几种,不妨拿出来老生常谈一番:
当我们再聊跨语言调用时我们在聊什么?纵观上述几个较为通用,成熟的解决方案,可以得出结论:解决跨语言调用的思路无非是两种:
如果一个新型的团队面临技术选型,我认为上述的方案都可以纳入参考,可考虑到遗留系统的兼容性问题
旧系统的迁移成本
这也关键的选型因素。我们做出的第一个尝试,便是在 RPC 协议上下功夫。
通用协议的跨语言支持
springmvc的美好时代
springmvc
springmvc
在没有实现真正的跨语言调用之前,想要实现“跨语言”大多数方案是使用 http 协议做一层转换,最常见的手段莫过于借助 springmvc 提供的 controller/restController,间接调用 dubbo provider。这种方案的优势和劣势显而易见
通用协议的支持
事实上,大多数服务治理框架都支持多种协议,dubbo 框架除默认的 dubbo 协议之外,还有当当网扩展的 rest协议和千米网扩展的 json-rpc 协议可供选择。这两者都是通用的跨语言协议。
rest 协议为满足 JAX-RS 2.0 标准规范,在开发过程中引入了 @Path,@POST,@GET 等注解,习惯于编写传统 rpc 接口的人可能不太习惯 rest 风格的 rpc 接口。一方面这样会影响开发体验,另一方面,独树一帜的接口风格使得它与其他协议不太兼容,旧接口的共生和迁移都无法实现。如果没有遗留系统,rest 协议无疑是跨语言方案最简易的实现,绝大多数语言支持 rest 协议。
和 rest 协议类似,json-rpc 的实现也是文本序列化http 协议。dubbox 在 restful 接口上已经做出了尝试,但是 rest 架构和 dubbo 原有的 rpc 架构是有区别的,rest 架构需要对资源(Resources)进行定义, 需要用到 http 协议的基本操作 GET、POST、PUT、DELETE。在我们看来,restful 更合适互联网系统之间的调用,而 rpc 更适合一个系统内的调用。使用 json-rpc 协议使得旧接口得以兼顾,开发习惯仍旧保留,同时获得了跨语言的能力。
千米网在早期实践中采用了 json-rpc 作为 dubbo 的跨语言协议实现,并开源了基于 json-rpc 协议下的 python 客户端 dubbo-client-py 和 node 客户端 dubbo-node-client,使用 python 和 nodejs 的小伙伴可以借助于它们直接调用 dubbo-provider-java 提供的 rpc 服务。系统中大多数 java 服务之间的互相调用还是以 dubbo 协议为主,考虑到新旧协议的适配,在不影响原有服务的基础上,我们配置了双协议。
dubbo 协议主要支持 java 间的相互调用,适配老接口;json-rpc 协议主要支持异构语言的调用。
定制协议的跨语言支持
微服务框架所谓的协议(protocol)可以简单理解为:报文格式和序列化方案。服务治理框架一般都提供了众多的协议配置项供使用者选择,除去上述两种通用协议,还存在一些定制化的协议,如 dubbo 框架的默认协议:dubbo 协议以及 motan 框架提供的跨语言协议:motan2。
motan2协议的跨语言支持
motan2
motan2
motan2 协议被设计用来满足跨语言的需求主要体现在两个细节中—MetaData 和 motan-go。在最初的 motan 协议中,协议报文仅由 Header+Body 组成,这样导致 path,param,group 等存储在 Body 中的数据需要反序列得到,这对异构语言来说是很不友好的,所以在 motan2 中修改了协议的组成;weibo 开源了 motan-go ,motan-php ,motan-openresty ,并借助于 motan-go 充当了 agent 这一翻译官的角色,使用 simple 序列化方案来序列化协议报文的 Body 部分(simple 序列化是一种较弱的序列化方案)。
agent
agent
仔细揣摩下可以发现这么做和双协议的配置区别并不是大,只不过这里的 agent 是隐式存在的,与主服务共生。明显的区别在于 agent 方案中异构语言并不直接交互。
dubbo协议的跨语言支持
dubbo 协议设计之初只考虑到了常规的 rpc 调用场景,它并不是为跨语言而设计,但跨语言支持从来不是只有支持、不支持两种选择,而是要按难易程度来划分。是的,dubbo 协议的跨语言调用可能并不好做,但并非无法实现。千米网便实现了这一点,nodejs 构建的前端业务是异构语言的主战场,最终实现了 dubbo2.js,打通了 nodejs 和原生 dubbo 协议。作为本文第二部分的核心内容,重点介绍下我们使用 dubbo2.js 干了什么事。
Dubbo协议报文格式
dubbo协议
dubbo协议
dubbo协议报文消息头详解:
magic:类似java字节码文件里的魔数,用来判断是不是 dubbo 协议的数据包。魔数是常量 0xdabb
flag:标志位, 一共8个地址位。低四位用来表示消息体数据用的序列化工具的类型(默认 hessian),高四位中,第一位为 1 表示是 request 请求,第二位为 1 表示双向传输(即有返回 response),第三位为 1 表示是心跳 ping 事件。
status:状态位, 设置请求响应状态,dubbo 定义了一些响应的类型。具体类型见com.alibaba.dubbo.remoting.exchange.Response
invoke id:消息 id, long 类型。每一个请求的唯一识别 id(由于采用异步通讯的方式,用来把请求 request 和返回的 response 对应上)
body length:消息体 body 长度, int 类型,即记录 Body Content 有多少个字节
body content:请求参数,响应参数的抽象序列化之后存储于此。
协议报文最终都会变成字节,使用 tcp 传输,任何语言只要支持网络模块,有类似 Socket 之类的封装,那么通信就不成问题。那,跨语言难在哪儿?以其他语言调用 java 来说,主要有两个难点:
ps:dubbo 协议通讯demo( )
Protocol 还有一个实现分支是 AbstractProxyProtocol,如下图所示:
从图中我们可以看到:gRPC、HTTP、WebService、Hessian、Thrift 等协议对应的 Protocol 实现,都是继承自 AbstractProxyProtocol 抽象类。
目前互联网的技术栈百花齐放,很多公司会使用 Node.js、Python、Rails、Go 等语言来开发 一些 Web 端应用,同时又有很多服务会使用 Java 技术栈实现,这就出现了大量的跨语言调用的需求。Dubbo 作为一个 RPC 框架,自然也希望能实现这种跨语言的调用,目前 Dubbo 中使用“HTTP 协议 + JSON-RPC”的方式来达到这一目的,其中 HTTP 协议和 JSON 都是天然跨语言的标准,在各种语言中都有成熟的类库。
下面就重点来分析 Dubbo 对 HTTP 协议的支持。首先,会介绍 JSON-RPC 的基础,并通过一个示例,快速入门,然后介绍 Dubbo 中 HttpProtocol 的具体实现,也就是如何将 HTTP 协议与 JSON-RPC 结合使用,实现跨语言调用的效果。
Dubbo 中支持的 HTTP 协议实际上使用的是 JSON-RPC 协议。
JSON-RPC 是基于 JSON 的跨语言远程调用协议。Dubbo 中的 dubbo-rpc-xml、dubbo-rpc-webservice 等模块支持的 XML-RPC、WebService 等协议与 JSON-RPC 一样,都是基于文本的协议,只不过 JSON 的格式比 XML、WebService 等格式更加简洁、紧凑。与 Dubbo 协议、Hessian 协议等二进制协议相比,JSON-RPC 更便于调试和实现,可见 JSON-RPC 协议还是一款非常优秀的远程调用协议。
在 Java 体系中,有很多成熟的 JSON-RPC 框架,例如 jsonrpc4j、jpoxy 等,其中,jsonrpc4j 本身体积小巧,使用方便,既可以独立使用,也可以与 Spring 无缝集合,非常适合基于 Spring 的项目。
下面先来看看 JSON-RPC 协议中请求的基本格式:
JSON-RPC请求中各个字段的含义如下:
在 JSON-RPC 的服务端收到调用请求之后,会查找到相应的方法并进行调用,然后将方法的返回值整理成如下格式,返回给客户端:
JSON-RPC响应中各个字段的含义如下:
Dubbo 使用 jsonrpc4j 库来实现 JSON-RPC 协议,下面使用 jsonrpc4j 编写一个简单的 JSON-RPC 服务端示例程序和客户端示例程序,并通过这两个示例程序说明 jsonrpc4j 最基本的使用方式。
首先,需要创建服务端和客户端都需要的 domain 类以及服务接口。先来创建一个 User 类,作为最基础的数据对象:
接下来创建一个 UserService 接口作为服务接口,其中定义了 5 个方法,分别用来创建 User、查询 User 以及相关信息、删除 User:
UserServiceImpl 是 UserService 接口的实现类,其中使用一个 ArrayList 集合管理 User 对象,具体实现如下:
整个用户管理业务的核心大致如此。下面我们来看服务端如何将 UserService 与 JSON-RPC 关联起来。
首先,创建 RpcServlet 类,它是 HttpServlet 的子类,并覆盖了 HttpServlet 的 service() 方法。我们知道,HttpServlet 在收到 GET 和 POST 请求的时候,最终会调用其 service() 方法进行处理;HttpServlet 还会将 HTTP 请求和响应封装成 HttpServletRequest 和 HttpServletResponse 传入 service() 方法之中。这里的 RpcServlet 实现之中会创建一个 JsonRpcServer,并在 service() 方法中将 HTTP 请求委托给 JsonRpcServer 进行处理:
最后,创建一个 JsonRpcServer 作为服务端的入口类,在其 main() 方法中会启动 Jetty 作为 Web 容器,具体实现如下:
这里使用到的 web.xml 配置文件如下:
完成服务端的编写之后,下面再继续编写 JSON-RPC 的客户端。在 JsonRpcClient 中会创建 JsonRpcHttpClient,并通过 JsonRpcHttpClient 请求服务端:
在 AbstractProxyProtocol 的 export() 方法中,首先会根据 URL 检查 exporterMap 缓存,如果查询失败,则会调用 ProxyFactory.getProxy() 方法将 Invoker 封装成业务接口的代理类,然后通过子类实现的 doExport() 方法启动底层的 ProxyProtocolServer,并初始化 serverMap 集合。具体实现如下:
在 HttpProtocol 的 doExport() 方法中,与前面介绍的 DubboProtocol 的实现类似,也要启动一个 RemotingServer。为了适配各种 HTTP 服务器,例如,Tomcat、Jetty 等,Dubbo 在 Transporter 层抽象出了一个 HttpServer 的接口。
dubbo-remoting-http 模块的入口是 HttpBinder 接口,它被 @SPI 注解修饰,是一个扩展接口,有三个扩展实现,默认使用的是 JettyHttpBinder 实现,如下图所示:
HttpBinder 接口中的 bind() 方法被 @Adaptive 注解修饰,会根据 URL 的 server 参数选择相应的 HttpBinder 扩展实现,不同 HttpBinder 实现返回相应的 HttpServer 实现。HttpServer 的继承关系如下图所示:
这里以 JettyHttpServer 为例简单介绍 HttpServer 的实现,在 JettyHttpServer 中会初始化 Jetty Server,其中会配置 Jetty Server 使用到的线程池以及处理请求 Handler:
可以看到 JettyHttpServer 收到的全部请求将委托给 DispatcherServlet 这个 HttpServlet 实现,而 DispatcherServlet 的 service() 方法会把请求委托给对应接端口的 HttpHandler 处理:
了解了 Dubbo 对 HttpServer 的抽象以及 JettyHttpServer 的核心之后,回到 HttpProtocol 中的 doExport() 方法继续分析。
在 HttpProtocol.doExport() 方法中会通过 HttpBinder 创建前面介绍的 HttpServer 对象,并记录到 serverMap 中用来接收 HTTP 请求。这里初始化 HttpServer 以及处理请求用到的 HttpHandler 是 HttpProtocol 中的内部类,在其他使用 HTTP 协议作为基础的 RPC 协议实现中也有类似的 HttpHandler 实现类,如下图所示:
在 HttpProtocol.InternalHandler 中的 handle() 实现中,会将请求委托给 skeletonMap 集合中记录的 JsonRpcServer 对象进行处理:
skeletonMap 集合中的 JsonRpcServer 是与 HttpServer 对象一同在 doExport() 方法中初始化的。最后,我们来看 HttpProtocol.doExport() 方法的实现:
介绍完 HttpProtocol 暴露服务的相关实现之后,下面再来看 HttpProtocol 中引用服务相关的方法实现,即 protocolBindinRefer() 方法实现。该方法首先通过 doRefer() 方法创建业务接口的代理,这里会使用到 jsonrpc4j 库中的 JsonProxyFactoryBean 与 Spring 进行集成,在其 afterPropertiesSet() 方法中会创建 JsonRpcHttpClient 对象:
下面来看 doRefer() 方法的具体实现:
在 AbstractProxyProtocol.protocolBindingRefer() 方法中,会通过 ProxyFactory.getInvoker() 方法将 doRefer() 方法返回的代理对象转换成 Invoker 对象,并记录到 Invokers 集合中,具体实现如下:
本文重点介绍了在 Dubbo 中如何通过“HTTP 协议 + JSON-RPC”的方案实现跨语言调用。首先介绍了 JSON-RPC 中请求和响应的基本格式,以及其实现库 jsonrpc4j 的基本使用;接下来我们还详细介绍了 Dubbo 中 AbstractProxyProtocol、HttpProtocol 等核心类,剖析了 Dubbo 中“HTTP 协议 + JSON-RPC”方案的落地实现。
如果我们手动写一个简单的RPC调用,一般需要把服务调用的信息传递给服务端,包括每次服务调用的一些共用信息包括服务调用接口、方法名、方法参数类型和方法参数值等,在传递方法参数值时需要先序列化对象并经过网络传输到服务端,在服务端接受后再按照客户端序列化的顺序再做一次反序列化,然后拼装成请求对象进行服务反射调用,最终将调用结果传给客户端。Dubbo的实现也基本是相同的原理,下图是Dubbo一次完整RPC调用中经过的步骤:
首先在客户端启动时,会从注册中心拉取和订阅对应的服务列表,Cluster会把拉取的服务列表聚合成一个Invoker,每次RPC调用前会通过Directory#list获取providers地址(已经生成好的Invoker地址),获取这些服务列表给后续路由和负载均衡使用。对应上图①中将多个服务提供者做聚合。在框架内部实现Directory接口的是RegistryDirectory类,它和接口名是一对一的关系(每一个接口都有一个RegistryDirectory实例),主要负责拉取和订阅服务提供者、动态配置和路由项。
在Dubbo发起服务调用时,所有路由和负载均衡都是在客户端实现的。客户端服务调用首先会触发路由操作,然后将路由结果得到的服务列表作为负载均衡参数,经过负载均衡后会选出一台机器进行RPC调用,这3个步骤一次对应图中②③④。客户端经过路由和负载均衡后,会将请求交给底层IO线程池(如Netty)进行处理,IO线程池主要处理读写、序列化和反序列化等逻辑,因此这里一定不能阻塞操作,Dubbo也提供参数控制(decode.in.io)参数,在处理反序列化对象时会在业务线程池中处理。在⑤中包含两种类似的线程池,一种是IO线程池(Netty),另一种是Dubbo业务线程池(承载业务方法调用)。
目前Dubbo将服务调用和Telnet调用做了端口复用,子啊编解码层面也做了适配。在Telnet调用时,会新建立一个TCP连接,传递接口、方法和json格式的参数进行服务调用,在编解码层面简单读取流中的字符串(因为不是Dubbo标准头报文),最终交给Telnet对应的Handler去解析方法调用。如果不是Telnet调用,则服务提供方会根据传递过来的接口、分组和版本信息查找Invoker对应的实例进行反射调用。在⑦中进行了端口复用,Telnet和正常RPC调用不一样的地方是序列化和反序列化使用的不是Hessian方式,而是直接使用fastjson进行处理。
讲解完主要调用原理,接下来开始探讨细节,比如Dubbo协议、编解码实现和线程模型等,本篇重点主要放在⑤⑥⑦。
Dubbo协议参考了现有的TCP/IP协议,每一次RPC调用包括协议头和协议体两部分。16字节长的报文头部主要包含魔数(0xdabb),以及当前请求报文是否是Request、Response、心跳和事件的信息,请求时也会携带当前报文体内序列化协议编号。除此之外,报文头还携带了请求状态,以及请求唯一标识和报文体长度。
在消息体中,客户端严格按照序列化顺序写入消息,服务端也会遵循相同的顺序读取消息,客户端发起的请求消息体一次依次保存下列内容:Dubbo版本号、服务接口名、服务接口版本、方法名、参数类型、方法参数值和请求额外参数(attachment)。
服务端返回的响应消息体主要包含回值状态标记和返回值,其中回值状态标记包含6中:
我们知道在网络通信中(TCP)需要解决网络粘包/解包的问题,常用的方法比如用回车、换行、固定长度和特殊分隔符进行处理,而Dubbo是使用特殊符号0xdabb魔法数来分割处理粘包问题。
在实际场景中,客户端会使用多线程并发调用服务,Dubbo如何做到正确响应调用线程呢?关键在于协议头的全局请求id标识,先看原理图:
当客户端多个线程并发请求时,框架内部会调用DefaultFuture对象的get方法进行等待。在请求发起时,框架内部会创建Request对象,这时候会被分配一个唯一id,DefaultFuture可以从Request中获取id,并将关联关系存储到静态HashMap中,就是上图中的Futures集合。当客户端收到响应时,会根据Response对象中的id,从Futures集合中查找对应DefaultFuture对象,最终会唤醒对应的线程并通知结果。客户端也会启动一个定时扫描线程去探测超时没有返回的请求。
先了解一下编解码器的类关系图:
如上,AbstractCodec主要提供基础能力,比如校验报文长度和查找具体编解码器等。TransportCodec主要抽象编解码实现,自动帮我们去调用序列化、反序列化实现和自动cleanup流。我们通过Dubbo编解码继承结构可以清晰看到,DubboCodec继承自ExchageCodec,它又再次继承了TelnetCodec实现。我们前面说过Telnet实现复用了Dubbo协议端口,其实就是在这层编解码做了通用处理。因为流中可能包含多个RPC请求,Dubbo框架尝试一次性读取更多完整报文编解码生成对象,也就是图中的DubboCountCodec,它的实现思想比较简单,依次调用DubboCodec去解码,如果能解码成完整报文,则加入消息列表,然后触发下一个Handler方法调用。
编码器的作用是将Java对象转成字节流,主要分两部分,构造报文头部,和对消息体进行序列化处理。所有编辑码层实现都应该继承自ExchangeCodec,当Dubbo协议编码请求对象时,会调用ExchangeCodec#encode方法。我们来看下这个方法是如何对请求对象进行编码的:
如上,是Dubbo将请求对象转成字节流的过程,其中encodeRequestData方法是对RpcInvocation调用对象的编码,主要是对接口、方法、方法参数类型、方法参数等进行编码,在DubboCodec#encodeRequestData中对此方法进行了重写:
如上,响应编码与请求编码的逻辑基本大同小异,在编码出现异常时,会将异常响应返回给客户端,防止客户端只能一直等到超时。为了防止报错对象无法在客户端反序列化,在服务端会将异常信息转成字符串处理。对于响应体的编码,在DubboCodec#encodeResponseData方法中实现:
注意不管什么样的响应,都会先写入1个字节的标识符,具体的值和含义前面已经讲过。
解码相对更复杂一些,分为2部分,第一部分是解码报文的头部,第二部分是解码报文体内容并将其转换成RpcInvocation对象。我们先看服务端接受到请求后的解码过程,具体解码实现在ExchangeCodec#decode方法:
可以看出,解码过程中需要解决粘包和半包问题。接下来我们看一下DubboCodec对消息题解码的实现:
如上,如果默认配置在IO线程解码,直接调用decode方法;否则不做解码,延迟到业务线程池中解码。这里没有提到的是心跳和事件的解码,其实很简单,心跳报文是没有消息体的,事件又消息体,在使用Hessian2协议的情况下默认会传递字符R,当优雅停机时会通过发送readonly事件来通知客户端当前服务端不可用。
接下来,我们分析一下如何把消息体转换成RpcInvocation对象,具体在DecodeableRpcInvocation#decode方法中:
解码请求时,严格按照客户端写数据的顺序处理。
解码响应和解码请求类似,调用的同样是DubboCodec#decodeBody,就是上面省略的部分,这里就不赘述了,重点看下响应体的解码,即DecodeableRpcResult#decode方法:
如果读者熟悉Netty,就很容易理解Dubbo内部使用的ChannelHandler组件的原理,Dubbo内部使用了大量的Handler组成类似链表,依次处理具体逻辑,包括编解码、心跳时间戳和方法调用Handler等。因为Nettty每次创建Handler都会经过ChannelPipeline,大量的事件经过很多Pipeline会有较多开销,因此Dubbo会将多个Handler聚合成一个Handler。(个人表示,这简直bullshit)
Dubbo的Channelhandler有5中状态:
Dubbo针对每个特性都会实现对应的ChannelHandler,在讲解Handler的指责之前,我们Dubbo有哪些常用的Handler:
Dubbo提供了大量的Handler去承载特性和扩展,这些Handler最终会和底层通信框架做关联,比如Netty等。一次完整的RPC调用贯穿了一系列的Handler,如果直接挂载到底层通信框架(Netty),因为整个链路比较长,则需要大量链式查找和事件,不仅低效,而且浪费资源。
下图展示了同时具有入站和出站ChannelHandler的布局,如果一个入站事件被触发,比如连接或数据读取,那么它会从ChannelPipeline头部一直传播到ChannelPipeline的尾端。出站的IO事件将从ChannelPipeline最右边开始,然后向左传播。当然ChannelPipeline传播时,会检测入站的是否实现了ChannelInboundHandler,出站会检测是否实现了ChannelOutboundHandler,如果没有实现,则自动跳过。Dubbo框架中实现这两个接口类主要是NettyServerHandler和NettyClientHandler。Dubbo通过装饰者模式包装Handler,从而不需要将每个Handler都追加到Pipeline中。因此NettyServer和NettyClient中最多有3个Handler,分别是编码、解码和NettyHandler。
讲完Handler的流转机制后,我们再来探讨RPC调用Provider方处理Handler的逻辑,在DubboProtocol中通过内部类继承自ExchangeHandlerAdapter,完成服务提供方Invoker实例的查找并进行服务的真实调用。
如上是触发业务方法调用的关键,在服务暴露时服务端已经按照特定规则(端口、接口名、接口版本和接口分组)把实例Invoker存储到HashMap中,客户端调用过来时必须携带相同信息构造的key,找到对应Exporter(里面持有Invoker)然后调用。
我们先跟踪getInvoker的实现,会发现服务端唯一标识的服务由4部分组成:端口、接口名、接口版本和接口分组。
如上,Dispatcher是线程池的派发器。这里需要注意的是,Dispatcher真实的职责是创建有线程派发能力的ChannelHandler,比如AllChannelHandler、MessageOnlyChannelHandler和ExecutionChannelHanlder,其本身并不具备线程派发能力。
Dispatcher属于Dubbo中的扩展点,这个扩展点用来动态产生Handler,以满足不同的场景,目前Dubbo支持一下6种策略调用:
具体需要按照使用场景不同启用不同的策略,建议使用默认策略,如果在TCP连接中需要做安全或校验,则可以使用ConnectionOrderedDispatcher策略。如果引入新的线程池,则不可避免的导致额外的线程切换,用户可在Dubbo配置中指定dispatcher属性让具体策略生效。
在Dubbo内部,所有方法调用都被抽象成Request/Response,每次调用都会创建一个Request,如果是方法调用则返回一个Response对象。HeaderExceptionExchangeHandler就是用了处理这种场景,主要负责4中事情:
(1) 更新发送和读取请求时间戳。
(2) 判断请求格式或编解码是否有错,并响应客户端失败的具体原因。
(3) 处理Request请求和Response正常响应。
(4) 支持Telnet调用。
我们先来看一下HeaderExchangeHandler#received实现: