dubbo源码分析26 -- 网络编解码

在网络传输中只将数据看作是原始的字节序列。然则,我们的应用程序需要把这些字节序列组成有意义的信息。将应用程序的数据转换为网络格式,以及将网络格式转换为应用程序的数据的组件分别叫作编码器和解码器,同时具有这两种功能的单一组件叫作编解码器。

1、粘包 & 拆包

基于前面的分析我们知道 dubbo 的远程调用是基于 Netty 这个 Nio 框架进行基于 TCP/IP 的 Socket 通信。

TCP 是一个“流”协议,所谓流就是没有界限的一串数据。可以想像一个河里的流水是连成一片的,其间没有分界线。TCP 底层并不了解上层业务数据的具体含义,它会根据 TCP 缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被 TCP 拆分成多个包进行发送。也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的 TCP 粘包和拆包问题。

1.1 TCP 粘包 & 拆包问题说明

下面就通过以下的图来说明 TCP 粘包与拆包问题:

TCP 粘包 / 拆包问题.png

假设客户端分别发送了两个数据包 D1 和 D2 给服务端,由于服务端一次读取到的字节数据是不确定的,所以可能存在以下 4 种情况。

  • 服务端两次读取到了两个独立的数据包,分别是 D1 和 D2,没有粘包和拆包
  • 服务端一次接收到了两个数据包, D1 和 D2 粘合在一起,被称为 TCP 粘包
  • 服务端分两次读取到了两个数据包,第一次读取到了完整的 D1 包和 D2 包的部分内部,第二次读取到了 D2 包的剩余内部,这被称为 TCP 拆包
  • 服务端两次读取到了两个数据包,第一次读取到了 D1 包的部分内部 D1_1,第二次读取到了 D1 包的剩余内部 D1_2 和 D2 包的整包。

如果此时服务端 TCP 接收滑窗非常小,而数据包 D1 和 D2 比较大 ,很有可能会发生第五种可能,即服务端分多次才能将 D1 和 D2 包接收完全,期间发生多次拆包。

1.2 解决粘包 & 拆包

由于底层的 TCP 无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的。这个问题只能通过上层的应用协议栈设计来解决,主流的解决方案如下:

  • 消息定长,例如每个报文的大小为固定长度 200 字节,如果不够,空位被空格。
  • 在包尾增加回车换行符进行分割,例如 TFP 协议
  • 将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段。

netty 对于前 2 种都有自己的实现,而 dubbo 采用的是第 3 种来解决粘包与拆包问题的。

2、dubbo 自定义协议

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<NioSocketChannel>() {
                    @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_protocol_header.jpg

dubbo 使用长度为 16 的 byte 数组作为协议头。1 个 byte 对应 8 位。所以 dubbo 的协议头有 128 位 (也就是上图的从 0 到 127)。我们来看一下这 128 位协议头分别代表什么意思。

  • 0 ~ 7 : dubbo 魔数((short) 0xdabb) 高位,也就是 (short) 0xda。
  • 8 ~ 15: dubbo 魔数((short) 0xdabb) 低位,也就是 (short) 0xbb。
  • 16 ~ 20:序列化 id(Serialization id),也就是 dubbo 支持的序列化中的 contentTypeId,比如 Hessian2Serialization#ID 为 2
  • 21 :是否事件(event )
  • 22 : 是否 Two way 模式(Two way)。默认是 Two-way 模式,<dubbo:method> 标签的 return 属性配置为false,则是oneway模式
  • 23 :标记是请求对象还是响应对象(Req/res)
  • 24 ~ 31:response 的结果响应码 ,例如 OK=20
  • 32 ~ 95:id(long),异步变同步的全局唯一ID,用来做consumer和provider的来回通信标记。
  • 96 ~ 127: data length,请求或响应数据体的数据长度也就是消息头+请求数据的长度。用于处理 dubbo 通信的粘包与拆包问题。

我们就根据源码来分析一下 dubbo 是如何进行编解码的。

3、协议源码分析

dubbo 的编解码可以分为以下 4 个部分来分析:

  • consumer 请求编码
  • consumer响应结果解码
  • provider 请求解码
  • provider 响应结果编码

在 dubbo 进行服务暴露的时候是通过 NettyCodecAdapter 来获取到需要添加的编码器与解码器。在 NettyCodecAdapter 里面定义内部类 InternalEncoder (继承 netty 中的 MessageToByteEncoder)实现 dubbo 的自定义编码器,定义内部类 ByteToMessageDecoder (继承 netty 中的 ByteToMessageDecoder) 实现 dubbo 自定义解码器。不管是自定义的编码器还是解码器最终都会调用到 dubbo 的 SPI 接口 Codec2 默认使用 DubboCodec。下面就具体的分析一下 dubbo 这 4 个编解码过程。

3.1 consumer 请求编码

consumer 在请求 provider 的时候需要把 Request 对象转化成 byte 数组,所以它是一个需要编码的过程。

Consumer#request#encode.jpg

3.2 consumer响应结果解码

consumer 在接收 provider 响应的时候需要把 byte 数组转化成 Response 对象,所以它是一个需要解码的过程。

Consumer#response#decode.jpg

3.3 provider 请求解码

provider 在接收 consumer 请求的时候需要把 byte 数组转化成 Request 对象,所以它是一个需要解码的过程。

Provider#request#decode.jpg

3.4 响应结果编码

provider 在处理完成 consumer 请求需要响应结果的时候需要把 Response 对象转化成 byte 数组,所以它是一个需要编码的过程。

Provider#response#encode.jpg
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,039评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,223评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,916评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,009评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,030评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,011评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,934评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,754评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,202评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,433评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,590评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,321评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,917评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,568评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,738评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,583评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,482评论 2 352

推荐阅读更多精彩内容

  • 在前面一篇博客中分享了 dubbo 在网络通信当中的 consumer 的发送以及接收原理。通过集群容错最终选择一...
    carl_zhao阅读 946评论 0 2
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,650评论 18 139
  • 简书 占小狼转载请注明原创出处,谢谢! 本文由臧秀涛撰稿,经过R大润色,由占小狼倾情分享,这些分析总结道出了TLA...
    美团Java阅读 9,558评论 2 36
  • 看到老师发的倡议书已经好几天了,我迟迟未动笔。因为这几天期中考试,弄得我们心情都不好。虽然看书听课一年多了...
    ytt木子阅读 100评论 0 0