本文首先简单回顾了沾包拆包问题的本质,然后从odl目前应用最广泛的三个南向协议包括openflow、netconf和ovsdb,从代码实现出发,分析了odl如何来解决三个不同类型协议的沾包粘包问题,给出具体思路!
文章目录
1 tcp沾包粘包问题的本质
1.1 tcp报文发送过程
首先回顾一下tcp沾包拆包问题发生的过程:
- 当socket连接建立后,内核会为每个socket通道新建一个发送缓冲区和接收缓冲区.
- 发送数据时候,应用程序将数据写入发送缓冲区,这里有三种情况:
- 数据包太长:单个tcp报文无法将数据发送完,这里则需要进行分包,将一个应用层报文分成两个或多个包发送。实际上这里通常有三个原因:
write写入数据大于发送缓冲区大小
。- 缓冲区足够大,但是MSS/MTU的限制不得不进行拆包;
- 滑动窗口大小的限制;
- 数据包太短:当数据报文内容比较小的时候,发送缓冲区是采取的
限制时间内攒一波发送
的策略。此时就涉及到了同一个数据包中可能包含多个数据包的情况。
- 数据包太长:单个tcp报文无法将数据发送完,这里则需要进行分包,将一个应用层报文分成两个或多个包发送。实际上这里通常有三个原因:
- 接收数据时,应用程序从接收缓冲区取数据,由于发送的数据存在沾包的问题,因此接收端应用程序,必须将缓冲区进行必要的处理,进行识别,否则就会出现应用报文解析错误的问题!
1.2 沾包的四种现象
- 服务端分别收取了两个数据包,没有沾包和拆包;
- 服务端一次收到了两个数据包,数据包D1和D2 沾合到一起了;被称为
tcp沾包
; - 服务到两次读取到了两个数据包,第一次读取到了完成的D1包和D2包的部分内容,第二次读取到了D2包的部分内容;被称为
tcp拆包
; - 幅度段端两次读取到了两个数据包,第一次读取到了D1的部分内容,第二次读取到了D1的剩余内容和D2完整内容。被称为
tcp拆包
;
1.3 沾包拆包问题的本质
沾包拆包问题的本质由于tcp协议是一个基于流的协议,并没有界定上层应用层数据的界限!
一个完整的应用层数据包,可能会被tcp拆分成多个包发送,也可能是多个小数据包组装成一个大的数据包发送,这便是所谓的tcp沾包拆包问题。
1.4 tcp沾包拆包问题解决思路
常见的沾包拆包问题解决思路可以总结为两种情况:
- 将应用数据报文定长–固定数据包长度,应用层则可以很简单的进行数据内容的识别
- 变长情况:该情况稍微复杂,通常有两种做法:
- 为应用层报文添加指定的的分隔符,比如\n\r ]]}]]
- 使用tlv结构,在应用层报文头部,明确消息类型和具体的长度!
本文讲解的odl的三个南向协议都涉及到了这个解决思路!
2 netty的沾包拆包之道
在解决tcp沾包拆包导致的半包读写问题上,netty提供了多种编码器用于解决半包问题,现将相关编码器总结如下:
LineBaseFrameDecoder
:LineBaseFrameDecoder的工作原理是依次遍历ByteBuf的可读字节,如果有\n或者\n\r,就以此位置为结束。StringDecoder
:StringDecoder的功能是将接受对象转换成字符串,继续调用后续的handler;DelimiterBasedFrameDecoder
:DelimiterBasedFrameDecoder是一种分隔符加码器,可以自动完成对指定分隔符结束的消息的解码;FixedLengthFrameDecoder
:FixedLengthFrameDecoder可以自动完成对于定长消息的解码;
根据上面分析可知,netty框架对于简单的终止符分割、定长消息解码给出了自己的解码器,方便用户进行解码,但是对于复杂的tlv结构没有很好办法,但是给出了相应接口,支持用户自行的扩展,本文则是以此为基础重点分析以下opendaylight的三个重要协议如何解决这个问题!
2 netconf 协议沾包粘包问题解决
下图所示是netconf协议子通道初始化的位置,这里有两个比较有引人注意的handler添加到了pipline。
首先我们看看NetconfEOMAggregator,可以看到它便是我们寻找的终止符解码器,继承了DelimiterBasedFrameDecoder,然后为其传入了一个netconf协议规定的终止符]]>]]>
,
需要明确的是DelimiterBasedFrameDecoder是一个入方向接收解码器
其次我们来看看报文的出方向FramingMechanismHandlerFactory,工厂传入的参数为EOM,这是由netconf协议相关的,参考rfc6242#section-4.3,在协议框架上采用了终止符机制,因此每次发送数据也有必要通过一个编码器加上netconf固定的后缀]]>]]>
,这便是EOMFramingMechanismEncoder的工作,由继承关系可知,它是一个输出方向的编码器–ChannelOutboundHandlerAdapter。
3 openflow协议的沾包粘包问题解决
openflow协议是一个tlv结构的报文,其具有非常明确的报文协议头部,包含了消息类型、长度以及子tlv结构等内容,详细细节可查看of组织定义的标准openflow协议rfc文档说明!
为了找到openflow协议channel初始化的地方,我们先聚焦到tcpHandler的初始化位置,这里可以找到我们所关注的tcp通道初始化器
如下图所示为TcpChannelInitializer的初始化通道代码,由于openflow消息可能会被tls协议加密,所以最初加入的handler是一个tls的hanler,专用于tls协议的解析!
下图是所示,完成tls解析后加入的第一个handler,便是openflow消息框架的handler!
我们期待的沾包拆包功能便是由openflow自己重写的这个OFFrameDecoder
完成!
这个解码器的功能就是识别出openflow的报文头部,进行消息的完整性检查,具体逻辑如下:
- 取出openflow报文头部,如果读取字节小于报文头长度则返回;
- 取把报文长度位,若可读字节长度低于报文长度,则返回,暂时不进行后续处理!
- 若长度够,则按长度取出数据,并偏移相关ByteBuf
由上文图文分析可知,openflow的沾包处理是自己定义了一个OFFrameDecoder框架解码器,通过tlv报文长度入手,缓冲区达到长度则处理,加入对象列表,否则直接返回,等待下一次触发处理!
4 ovsdb协议的沾包粘包问题解决
ovsdb的协议采用json rpc格式进行协议消息传递,具体协议细节,有兴趣的读者同样可以参看rfc文档,查看相应细节!
按照openflow协议的细节,我们很快找到相应的协议处理类,便是这里的JsonRpcDecoder
和
StringEncoder
,一个负责解码,一个负责编码!
json rpc消息的框架解码器,在这里做的工作便是识别一个完整的json消息,如下图方框便是做的这个工作,但是存在有一种可能,就是可能一个handler的处理的时候不能形成json的那种开合处理!也就是我们所说的半包,ovsdb模块的处理逻辑是使用了一个临时变量lastRecordBytes
,假如每次处理的是一个整包,这个值会归零,但是一旦处理不完全,变不会想相关消息内容加入list,并且会记录住这里的lastRecordBytes
,下一次处理的时候把它在加入缓冲字节一并处理!
5 小结说明
本节分析了tcp粘包拆包问题的具体原因和本质,同时针对netty并结合opendaylight这个开源网络控制器,针对其开发的三个rfc标准协议openflow 、netconf以及ovsdb给出了相关沾包拆包的实现细节!可以看到随着rfc协议的复杂化,沾包拆包问题,更倾向于使用特定分隔符以及用户自定义tlv编码解码器完成相关处理!
这里给出的三个协议的实现,更是一种参考!值得记录下来回味一番,确实netty在tcp沾包拆包的处理上面提供了很大的灵活度和扩展性,这种设计思路值得借鉴!可能有些读者对于netty的handler的读写回调还有些问题,本文之后下一节将会专门给出说明!