在网络传输中只将数据看作是原始的字节序列。然则,我们的应用程序需要把这些字节序列组成有意义的信息。将应用程序的数据转换为网络格式,以及将网络格式转换为应用程序的数据的组件分别叫作编码器和解码器,同时具有这两种功能的单一组件叫作编解码器。
基于前面的分析我们知道 dubbo 的远程调用是基于 Netty 这个 Nio 框架进行基于 TCP/IP 的 Socket 通信。
TCP 是一个“流”协议,所谓流就是没有界限的一串数据。可以想像一个河里的流水是连成一片的,其间没有分界线。TCP 底层并不了解上层业务数据的具体含义,它会根据 TCP 缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被 TCP 拆分成多个包进行发送。也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的 TCP 粘包和拆包问题。
下面就通过以下的图来说明 TCP 粘包与拆包问题:
假设客户端分别发送了两个数据包 D1 和 D2 给服务端,由于服务端一次读取到的字节数据是不确定的,所以可能存在以下 4 种情况。
如果此时服务端 TCP 接收滑窗非常小,而数据包 D1 和 D2 比较大 ,很有可能会发生第五种可能,即服务端分多次才能将 D1 和 D2 包接收完全,期间发生多次拆包。
由于底层的 TCP 无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的。这个问题只能通过上层的应用协议栈设计来解决,主流的解决方案如下:
netty 对于前 2 种都有自己的实现,而 dubbo 采用的是第 3 种来解决粘包与拆包问题的。
Netty 对于开发者而言,其实就是操作 ChannelHandler 这个组件。之前我们分析了 dubbo 网络请求的发送与接收是实现了 ChannelHandler 的 NettyServerHandler。针对于编解码同样也是实现 ChannelHandler 来进行的。
NettyServer#doOpen
protected void doOpen() throws Throwable {
bootstrap = new ServerBootstrap();
bossGroup = new NioEventLoopGroup(1, new DefaultThreadFactory("NettyServerBoss", true));
workerGroup = new NioEventLoopGroup(getUrl().getPositiveParameter(Constants.IO_THREADS_KEY, Constants.DEFAULT_IO_THREADS),
new DefaultThreadFactory("NettyServerWorker", true));
final NettyServerHandler nettyServerHandler = new NettyServerHandler(getUrl(), this);
channels = nettyServerHandler.getChannels();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childOption(ChannelOption.TCP_NODELAY, Boolean.TRUE)
.childOption(ChannelOption.SO_REUSEADDR, Boolean.TRUE)
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyServer.this);
ch.pipeline()//.addLast("logging",new LoggingHandler(LogLevel.INFO))//for debug
.addLast("decoder", adapter.getDecoder())
.addLast("encoder", adapter.getEncoder())
.addLast("handler", nettyServerHandler);
}
});
// bind
ChannelFuture channelFuture = bootstrap.bind(getBindAddress());
channelFuture.syncUninterruptibly();
channel = channelFuture.channel();
}
在进行服务暴露的时候,在初始化的时候通过 ChannelPipeline 添加编码、解码与处理请求响应的具体 ChannelHandler 实现类。dubbo 编解码的具体都是通过 NettyCodecAdapter 来处理的。
下面我们来看一下 dubbo 的协议头约定:
dubbo 使用长度为 16 的 byte 数组作为协议头。1 个 byte 对应 8 位。所以 dubbo 的协议头有 128 位 (也就是上图的从 0 到 127)。我们来看一下这 128 位协议头分别代表什么意思。
(short) 0xdabb
) 高位,也就是 (short) 0xda。(short) 0xdabb
) 低位,也就是 (short) 0xbb。contentTypeId
,比如 Hessian2Serialization#ID 为 2
标签的 return 属性配置为false,则是oneway模式我们就根据源码来分析一下 dubbo 是如何进行编解码的。
dubbo 的编解码可以分为以下 4 个部分来分析:
在 dubbo 进行服务暴露的时候是通过 NettyCodecAdapter 来获取到需要添加的编码器与解码器。在 NettyCodecAdapter 里面定义内部类 InternalEncoder (继承 netty 中的 MessageToByteEncoder)实现 dubbo 的自定义编码器,定义内部类 ByteToMessageDecoder (继承 netty 中的 ByteToMessageDecoder) 实现 dubbo 自定义解码器。不管是自定义的编码器还是解码器最终都会调用到 dubbo 的 SPI 接口 Codec2 默认使用 DubboCodec。下面就具体的分析一下 dubbo 这 4 个编解码过程。
consumer 在请求 provider 的时候需要把 Request 对象转化成 byte 数组,所以它是一个需要编码的过程。
consumer 在接收 provider 响应的时候需要把 byte 数组转化成 Response 对象,所以它是一个需要解码的过程。
provider 在接收 consumer 请求的时候需要把 byte 数组转化成 Request 对象,所以它是一个需要解码的过程。
provider 在处理完成 consumer 请求需要响应结果的时候需要把 Response 对象转化成 byte 数组,所以它是一个需要编码的过程。