从本文开始,我们会进入dubbo网络通信层的设计分析。本篇主要讲解一下Dubbo的协议以及对应的编解码器。
Dubbo协议
Dubbo协议的设计参考了TCP/IP协议,其中包含了协议头和协议体两个部分。采用了定长协议头+变长协议体(payload),16个字节的报文头包含了魔数(0xdabb)和请求响应标志、心跳、序列化标志等。其具体结构如下所示:
- Magic - Magic High & Magic Low (16 bits)
标识协议版本号,Dubbo 协议:0xdabb
- Req/Res (1 bit)
标识是请求或响应。请求: 1; 响应: 0。
- 2 Way (1 bit)
仅在 Req/Res 为1(请求)时才有用,标记是否期望从服务器返回值。如果需要来自服务器的返回值,则设置为1。
- Event (1 bit)
标识是否是事件消息,例如,心跳事件。如果这是一个事件,则设置为1。
- Serialization ID (5 bit)
标识序列化类型:比如 fastjson 的值为6。
- Status (8 bits)
仅在 Req/Res 为0(响应)时有用,用于标识响应的状态。
20 - OK
30 - CLIENT_TIMEOUT
31 - SERVER_TIMEOUT
40 - BAD_REQUEST
50 - BAD_RESPONSE
60 - SERVICE_NOT_FOUND
70 - SERVICE_ERROR
80 - SERVER_ERROR
90 - CLIENT_ERROR
100 - SERVER_THREADPOOL_EXHAUSTED_ERROR
- Request ID (64 bits)
标识唯一请求。类型为long。
- Data Length (32 bits)
序列化后的内容长度(可变部分),按字节计数。int类型。
- Variable Part
被特定的序列化类型(由序列化 ID 标识)序列化后,每个部分都是一个 byte [] 或者 byte
⇒ 如果是请求包 ( Req/Res = 1),则每个部分依次为:
Dubbo version
Service name
Service version
Method name
Method parameter types
Method arguments
Attachments
⇒ 如果是响应包(Req/Res = 0),则每个部分依次为:
返回值类型(byte),标识从服务器端返回的值类型:
返回空值:RESPONSE_NULL_VALUE 2
正常响应值: RESPONSE_VALUE 1
异常:RESPONSE_WITH_EXCEPTION 0
返回值:从服务端返回的响应bytes
Dubbo编解码器
对于Dubbo的编解码,我们先看下2.0.7版本是如何实现的。如下图所示:
从上面类图关系可以看出,Dubbo编解码的核心类文件就6个。其中我们首先看下顶层的2个抽象,Codec和AbstractCodec。
Codec
从上面的Codec顶层接口可以看出,只有编解码的两个方法。但是细心的读者可以看到,通常编解码只需要Object 和 二进制数据之间的相互转化,为什么上面的接口入参还有Channel 这个类型的参数?原因是因为URL是始终围绕着Dubbo的一个东西,而Channel 里面可以获取到 URL。通过URL可以获取到序列化类型,寻找SPI实现等。
我们可以看到还有一个常量NEED_MORE_INPUT,这个是给TelnetCodec定义使用的。意思是当解码输入的二进制时,当不满足命令的结束标志时,会通过这个对象提示调用方需要继续数据相关命令字符。
AbstractCodec
从上面的AbstractCodec可以看出,其并没有什么抽象的模板方法,而是两个工具方法。其中一个是获取序列化类型的,另外一个是检查消息体是否过载的。
TransportCodec
TransferCode这个类的编解码也相对比较简单,也就是将Object和二进制之间进行转换,其实并没有对Dubbo协议本身进行编解码,也即是直接使用了序列化类进行编解码。
TelnetCodec
TelnetCodec是针对于命令行输入的编解码,主要处理命令客户端相关的编解码。上面的代码笔者只是摘要了一部分,从成员变量可以看出,会有一些键盘上的回车/向上翻页/向下翻页等键盘符的静态变量。另外编码是对String的编码,然后其他的类型会调用TransportCodec的编码。对于解码而言,则是会根据用户在客户端命令行的输入,决定是否解码完成。如:命令结束符回车,表示本次解码可以完成,然后再由上层判断解码出来的字符是否符合命令规范。
ExchangeCodec & DubboCodec
对于ExchangeCodec而言,其本身对Dubbo协议头的解析,而对于协议体的解析是放到DubboCodec中去处理的,也就是交换层只关心协议头。上面的代码只是一部分,从成员变量可以看出,它包含了Dubbo协议头的相关信息。下方的encode()和decode() 方法,也是直接重写了父类的方法。
对于编码而言,只处理了Request和Responsel2种对象的编码,其余的对象调用的是父类的编码方法。(TelnetCodec只处理String类型的编码,其余的交给了TransportCodec处理)下面我们先看下对请求的编码:
上面的代码也比较简单,就是按照Dubbo协议头去拼装二进制数据。不过这里有一点需要注意的是,如果是Dubbo协议,encodeRequestData() 方法会被DubboCodec重写,此时会对请求体进一步的进行编码。其代码实现在DubboCodec中,如下所示:
对于上述的代码,也相对比较简单,就是开篇说到的Dubbo协议的payload组装。同理对于Response的编码也是类似的,这里就不再赘述。
对于ExchangeCodec的解码,其也是只解码了消息头,解码消息体(payload)是在DubboCodec中实现的。我们首先看下ExchangeCodec对消息头的解码过程:
从从上面代码可以看出,解码也是按照Request/Response的维度进行的。此时当消息头解析完成后,如果是响应消息,会调用DubboCodec的decodeResponseData()方法进入消息体的解码过程。
参考:《深入理解Apache Dubbo 与实战》、Dubbo 2.6.0 源代码、Dubbo 2.0.7 源代码