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

       我们在上篇文章中讲述了序列化反序列化组成有意义的信息,然后这些信息需要经过网络进行传送,数据转成网络格式,再由网络格式转成应用程序的数据用的组件分别叫做编码器和解码器。

1.什么是粘包、拆包?

粘包

客户端发送多个请求给服务器,多个请求包粘成一个

发送方为了提高效率。往往会收集一定数量后一笔发送出去,这就粘到一个包

分析:

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

为什么会发生TCP粘包、拆包呢?

  1. 应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包。

  2. 应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包。

  3. 进行MSS(最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包。

  4. 接收方法不及时读取套接字缓冲区数据,这将发生粘包。

问题

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

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

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

解决粘包 & 拆包

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

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

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

2.dubbo 自定义协议

Netty 对于开发者而言,其实就是操作 ChannelHandler 这个组件。之前我们分析了 dubbo 网络请求的发送与接收是实现了 ChannelHandler 的 NettyServerHandler。针对于编解码同样也是实现 ChannelHandler 来进行的。

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 通信的粘包与拆包问题。

3、协议源码分析

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

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

在 dubbo 进行服务暴露的时候是通过 NettyCodecAdapter 来获取到需要添加的编码器与解码器。在 NettyCodecAdapter 里面定义内部类 InternalEncoder (继承 netty 中的 MessageToByteEncoder)实现 dubbo 的自定义编码器,定义内部类 ByteToMessageDecoder (继承 netty 中的 ByteToMessageDecoder) 实现 dubbo 自定义解码器。

Netty中处理粘包拆包

 这两个组件都是用来解码网络上过来的数据的。而他们的顺序一般是ByteToMessageDecoder位于head channel handler的后面,MessageToMessageDecoder位于ByteToMessageDecoder的后面。Netty中,涉及到粘包、拆包的逻辑主要在ByteToMessageDecoder及其实现中。

  1. ByteToMessageDecoder

  2. MessageToMessageDecoder

ByteToMessageDecoder

顾名思义、ByteToMessageDecoder是用来将从网络缓冲区读取的字节转换成有意义的消息对象

 看源码分析一波:

protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
    try {
        while (in.isReadable()) {
            int outSize = out.size();

            if (outSize > 0) {
                fireChannelRead(ctx, out, outSize);
                out.clear();
                
                if (ctx.isRemoved()) {
                    break;
                }
                outSize = 0;
            }

            int oldInputLength = in.readableBytes();
            decode(ctx, in, out);

            if (ctx.isRemoved()) {
                break;
            }

            if (outSize == out.size()) {
                if (oldInputLength == in.readableBytes()) {
                    break;
                } else {
                    continue;
                }
            }

            if (oldInputLength == in.readableBytes()) {
                throw new DecoderException(
                        StringUtil.simpleClassName(getClass()) +
                        ".decode() did not read anything but decoded a message.");
            }

            if (isSingleDecode()) {
                break;
            }
        }
    } catch (DecoderException e) {
        throw e;
    } catch (Throwable cause) {
        throw new DecoderException(cause);
    }
}

当上面一个channel handler传入的ByteBuf有数据的时候,这里我们可以把in参数看成网络流,这里有不断的数据流入,而我们要做的就是从这个byte流中分离出message,然后把message添加给out。

分步看代码:

            fireChannelRead(ctx, out, outSize); 

            if (ctx.isRemoved()) {                     break;                 }                 outSize = 0;

              if (ctx.isRemoved()) {                 break;             }

           if (outSize == out.size()) {                 if (oldInputLength == in.readableBytes()) {                     break;                 } else {                     continue;                 }             } 

 if(oldInputLength==in.readableBytes()){throw new DecoderException(                         StringUtil.simpleClassName(getClass()) +                         ".decode() did not read anything but decoded a message.");             }

  1. 当out中有Message的时候,直接将out中的内容交给后面的channel handler去处理。

  2. 当用户逻辑把当前channel handler移除的时候,立即停止对网络数据的处理。

  3. 记录当前in中可读字节数。int oldInputLength = in.readableBytes();

  4. decode是抽象方法,交给子类具体实现。decode(ctx, in, out)

  5. 同样判断当前channel handler移除的时候,立即停止对网络数据的处理。

  6. 如果子类实现没有分理出任何message的时候,且子类实现也没有动bytebuf中的数据的时候,这里直接跳出,等待后续有数据来了再进行处理 

  7. 如果子类实现没有分理出任何message的时候,且子类实现动了bytebuf中的数据,则继续循环,直到解析出message或者不在对bytebuf中数据进行处理为止。

  8. 如果子类实现解析出了message但是又没有动bytebuf中的数据,那么是有问题的,抛出异常。

  9. 如果标志位只解码一次,则退出。if (isSingleDecode()) {                 break;             }

对于粘包情况的decode需要实现的逻辑对应于将客户端发送的两条消息都解析出来分为两个message加入out,这样的话callDecode只需要调用一次decode即可。

对于拆包情况的decode需要实现的逻辑主要对应于处理第一个数据包的时候第一次调用decode的时候out的size不变,从continue跳出并且由于不满足继续可读而退出循环,处理第二个数据包的时候,对于decode的调用将会产生两个message放入out,其中两次进入callDecode上下文中的数据流将会合并为一个bytebuf和当前channel handler实例关联,两次处理完毕即清空这个bytebuf。

回来看dubbo自己是怎么进行粘包拆包

3.1 consumer 请求编码

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

3.2 consumer响应结果解码

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

3.3 provider 请求解码

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

3.4 响应结果编码

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

猜你喜欢

转载自blog.csdn.net/z15732621582/article/details/81085389