如何设计一个应用层协议

如何设计一个应用层协议

前言

看完这几个协议的报文,发现有一种共性在其中,于是就引发了一个思考:如何设计一个协议?

协议的特点是上层协议依赖下层协议的传输,而TCP协议、IP协议已经比较成熟了,所以在这里加一个限定:应用层协议。

在以往的经历中,也接触了一些协议相关的内容,比如最早的USB协议、后来的前置服务通信的几个规约(376、698等),这些协议可以算作是比较成熟的应用层协议了,最近在做消防机器人项目时,设备与服务之间的通信是通过json来做的,就比较粗糙了。这里借这个机会对这个协议设计进行一个探索。

报文

协议的设计应该有特定的应用场景,然后在这个场景中会出现哪些问题,再通过算法来解决这些问题,从而设计出一个完整的协议。报文中的一些字段与这个算法或者解决方案是密切相关的,另外一些字段应该算了共性的。这里我们先来看看共性的字段。

我们通过列举一下这几个协议的报文,从这几个报文中挑拣一下共性字段。

报文

  • ARP

    ARP报文
  • IP

    IP报文
  • ICMP

    IP报文-2
  • TCP

    tcp报文

整理

字段以太网ARPIPICMPTCP
源地址
目的地址
帧类型
子类型
版本号
首部长度
总长度
序号
检验和
字段说明
源地址与目的地址ICMP相当于一种特殊的IP报文,故它也是有这2个地址的
帧类型与子类型这里唯一没有的是IP,IP解决的是选路问题,所有报文都要传输。在有不同种类帧(命令、操作)等协议中,都需要有这两个字段
版本号这里只IP有,考虑升级、兼容的环境下应该有该字段
首部长度、总长度部首长度应该是有可变长度的部首时,才需要有的,对于固定的长度可以不要;数据报文的总长度可能考虑了IP包的拆分,所以采有。HTTP协议中也有长度,可能与粘包有关
序号IP与TCP都有,主要能解决一个帧的乱序问题,TCP可能也与IP包的拆分有关
检验和长距离的都有

视角

  • 一种视角

    协议的共性字段,总结一下可以简称为:我是谁(源地址),我找谁(目的地址),我要干嘛(帧类型、子类型),以及一些兼容与保障措施(长度、序号、检验和),这有点像打电话(不是打手机)

  • 另外一个视角

    帧类型、子类型可以看做是操作码,而携带的数据可以看做是操作数,这样看,协议远程就像是在执行某个指令。

  • 请求与应答

    这里有个小细节,协议的请求与应答,有些协议如:ARP、TCP,请求与应答都用一种报文,而IP协议是用过ICMP协议做了应答。

  • 关于地址

    TCP+IP已经从机器的角度解决了地址的问题,在高层的协议中可以直接使用它们,如:起初,Http协议中只关注了目的地址,后来根据认证,从业务上补偿了源地址。

编解码器

除了报文之外,重要的就是提供编解码器了,这里我更侧重一下解码器。在它之前,要注意TCP中的的粘包与拆包问题。

粘包与拆包

  • 问题

    本来2个独立的TCP包,因为网络的各种原因进行了拆分、重组、合并的现象。如下:第一个图是正常包,第二个是粘包,第三、四个是拆包加粘包。

    拆包与粘包问题
  • 原因

    粘包与拆包是网络的正常现象,消息太大自然要分成一段一段的发送,消息太小自然合并发送更高效且对网络友好。其实这里的粘包与拆包问题是针对TCP而言的,在IP层上,这个问题已经被解决过了。IP报文中报文总长度、唯一标示字段(往往是自增的)、还有片偏移,这样能够应对IP包的重复、先后顺序、拆包等复杂的问题。那在TCP中还存在IP,就不是IP层引入的问题了。

    网上有很多分析各种原因的,我觉得靠谱的原因应该有2个:

    1. Nagle为了减少碎片化的网络环境,而主动引入的合并碎片算法。

      Nagle
    2. 应用层读取TCP接收缓存区时所引入的问题。

      缓冲区造成的粘包问题

    这两个都不是网络层引入的,像其他MSS/MTU限制这都不应该算是TCP粘包的原因。什么只有粘包问题,而没有乱序问题,因为乱序问题已经在IP层解决了,而在TCP层并没有再次引入。

    PS:每个TCP连接都有自己独立的缓冲区,不用单心与其他连接共享。

    报文简介
  • 解决方案

    解决方案其实再照抄IP层的解决方案就行,加消息总长度是最省力的。还有加首尾标志的,这也可以,还有加上奇偶校验和的,我觉得用处不大,因为IP、TCP协议都有奇偶校验和了。

C做法

参考3

粘包问题解决方案

Java做法

参考2,Netty提供了比较方便的编解码器,在解码器方面,提供了2个抽象基类,ByteToMessageDecoderMessageToMessageDecoder

netty解码器
  • ByteToMessageDecoder 用于将接收到的二进制数据(Byte)解码,得到完整的请求报文(Message)。

    ByteToMessageDecoder提供的一些常见的实现类,这些就可以用来解决粘包问题了。

    • **FixedLengthFrameDecoder:**定长协议解码器,我们可以指定固定的字节数算一个完整的报文
    • **LineBasedFrameDecoder:**行分隔符解码器,遇到\n或者\r\n,则认为是一个完整的报文
    • **DelimiterBasedFrameDecoder:**分隔符解码器,与LineBasedFrameDecoder类似,只不过分隔符可以自己指定
    • **LengthFieldBasedFrameDecoder:**长度编码解码器,将报文划分为报文头/报文体,根据报文头中的Length字段确定报文体的长度,因此报文提的长度是可变的
    • **JsonObjectDecoder:**json格式解码器,当检测到匹配数量的"{" 、”}”或”[””]”时,则认为是一个完整的json对象或者json数组。
  • MessageToMessageDecoder则是将一个本身就包含完整报文信息的对象转换成另一个Java对象。

    Netty提供的MessageToMessageDecoder实现类较少,常见的是StringDecoder

扩展阅读

123

# tcp/ip 

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×