当前位置:C++技术网 > 精选软件 > 云平台开发架构分析系列:5 数据包的处理逻辑分析2(数据包拆包实现思路)

云平台开发架构分析系列:5 数据包的处理逻辑分析2(数据包拆包实现思路)

更新时间:2017-06-21 08:56:15浏览次数:1+次

    在文章《云平台开发架构分析系列4:数据包的处理逻辑分析1》中,我已经将数据包处理的整体思路分析了。现在开始对数据包处理的关键细节实现思路进行分析。
    对于数据包处理最复杂的地方就是对数据包的拆包处理。我们要将粘包的多个包(连续完整的多个或者有被截断的包)正确的拆分出来,是一项稍微麻烦的事情。我们要准确拆分出来,还要将截断的数据包前后拼成完整的包。否则处理不好,就会出现数据包丢失的问题。
    至于验证数据包的合法性,也就是针对数据包对应的业务协议格式进行各个字段的检测而已。所以这里就没有什么好说的,每一个协议不一样,检验方法也不相同。不过有一点需要说的是,协议数据是一段字节流,解析协议也就是将一段字节流按照协议定义的格式,挨个的划分出各字段,然后再确定各字段的值和所代表的意义是否是有效的。比如一个字段的标志固定为0xFF,如果此字段的值不是这个,那么这个数据包就无效。如果一个字段表示数据包整个长度为100字节,而数据包却只有50字节或者120字节,那么这就需要进行额外的处理,缓存或丢弃。
    而我们的数据包的拆包,也是要基于协议的标志进行的。具体如何拆,不同的协议有所不同,我们也没有办法用实例的说明。我就说个思路,仅供参考。
    如果说要正确拆包,那么我们就需要正确识别包的开始和结束。所以我们至少要知道数据包的开始标志。这也是各种协议需要的开始标志字节序列的原因。如果不是这样,那么就是其他各种手段确保能够识别包头的。因为我们使用的是TCP协议传输字节流。我们很难字节包装每次接受的都是我们自己定义的一个完整的业务协议数据包,所以我们不能用某种办法控制数据包的发送,或者很难控制。而我们用起始字节序列作为开始,显然也就方便多了。
    那么我们识别包头,也就是从这个开始标志字节序列开始的。我们假设这个开头是0xAA0xFF,那么我们在接受到客户端发送过来的一段原始数据的时候,从头开始搜索开头字节序列,如果搜索到了,自然也就找到了开头,定位到了一个数据包的开始。
    那么向后我们如何确定数据包的结尾呢?这里我们要说到协议的第二个非常重要的字段,那就是数据包的长度。我们知道数据包的长度我们才更好的确定数据包的结束。如果没有数据包长度的字段,将会让解析数据包和校验数据包以及拆分数据包带来极大的困难。当然,如果使用结束标志呢?那就和数据包长度的功能差不多了。因为这两个都是来确定数据包的结尾的和长度的。不过很多时候,我们使用数据包长度显得更加方便。
    那么确定数据包结尾的方法大致有几种:
1.结尾标志
2.数据包长度
3.下一个开始标志
    对于前两种,我们很好理解。然而第三种,我们也是可以用到的。我们来分别说说这三种方式对应的拆包思路吧。
一、按结尾标志方式拆包
    如果协议中采用了结尾标志,才可以用这样的方法拆包。当我们在搜索到协议的开始标志的时候,紧接着继续往后搜索结尾标志。从而可以确定一个数据包的起始和结束位置,最终确定一段数据包的数据。如果接受的数据只有一个数据包,开头就是协议开始标志,结束就是协议的结尾标志。如果数据里刚好是有整数倍个数据包呢?如果数据包被截断,数据中有0.5或1.5个数据包呢?
    如果是整数倍个数据包,我们按照单个数据包来搜索开始和结束标志就可以搞定。无非就是循环搜索而已。这是数据包粘包(叠包)比较好处理的一种情况。如果有数据包从中截断,形成了0.5或0.5的倍数个数据包呢?直接的搜索还是不能完全应对的。因为我们不只是要拆包,而且还要确保被截断的数据包要能够正确的拼装到一起,然后再处理。所以在搜索的时候我们就要加入对这个情况的处理。
    我们还是一样的搜索开始标志,然后继续搜索结束标志。如果是整倍,我们就拆分出来完整的多个数据包。如果有不完整的,也就表示,在第一次接受的数据中,最后一个数据包是搜索不到结尾标志的。所以我们在搜索到开始标志,直到结束都没有结束标志,这就说明存在一个被截断的数据包。这也就说明,后续的数据,必然是开头就不是开始标志的数据包,而是第一次接受的被截断的最后一个数据包的后续数据。为什么这么说呢?因为TCP是可靠传输,底层会确保数据可靠的传输过来,数据是不会丢失的。有了开始就一定有结束。对于完整的数据包是这样的。
    那么我们在搜索数据包的开始标志时,我们就需要注意了。如果开始标志不是开头,或者搜索到开始标志的位置离数据开始有一段距离,那么从最开始位置到开始字节序列的位置的这一段数据,就是前面被截断的数据包的后续数据。所以我们要将这两端数据拼接起来。因此,在搜索结尾标志时,如果没有搜索到结尾标志而数据已经搜索完了,我们就需要将这个半段数据包缓存起来,然后再后续的拆包时,将后半段与之拼接,形成一条完整的数据包。然后继续往后拆分数据包即可。
    直接拆分出来的数据包和拼接起来的数据包都是一样的,都会进行数据包的检验。检验的规则就是业务协议的定义,分析各个字段的值是不是在合理的范围内。如果都是合理的,那么就继续进入数据包解析处理,然后再进行业务处理,最后回复客户端数据。
    当然,虽然对单个字段检测在合理范围内,也不能保证数据完全正确。所以我们一般会有校验和。那就是将数据包的数据和开头放在一起计算校验和,存入最后一个字节里(结尾标志字节序列的前面)。这样也是为了防止数据包被拦截篡改。
     那么这样处理之后,我们基本上可以确定数据包正确的被拆包和解析了。
二、按数据包长度方式拆包
    按照数据包长度方式拆包,其实和结尾标志拆包差不多,无非就是确定数据包结尾方式有点差别。和“确定字符串结尾是以空字符来确定结尾和以字符数来确定结尾”差不多。其他的处理基本上差不多。
    一般来讲,一个数据包的长度是固定的。我们可以根据数据包的长度字段来确定数据包的结尾。我们在搜索到开始字节序列后,然后解析数据包的头,如果头部的字段信息都是合法的,包括长度字段,那么就继续往后解析。其中包括的长度字段值的检测。我们至少可以先检测长度值是否超越正常的水平。如果是固定的值,我们就按照固定的值来检测,如果有多种数据包,那么我们按照最长数据包的长度来限定。这是避免长度字段值出错后形成很大的值,结果一个劲的往后读取内存,会引发内存违规访问而崩溃。当然,我们也可以对长度字段值最小值进行校验,以确保长度字段值一定是正确的范围,然后再继续向后分析。
    解析协议是一环扣一环的,如果其中一环出错,那么后续的全部会出错。为了保证质量,我们得做好每一步的解析和验证,让后续的解析都是建立在前面可靠的基础之上的。一旦不符合要求,我们就做丢弃数据包的处理。当然,还得看具体出错的情况来定。我们也应该尽量提高解析的准确度。就算其中一个字段错了,我们也应该用冗余的检测手段来让后续的数据包正确解析出来,千万不能因为前面的残废数据包而丢弃了后面大量的数据包。假如数据包长度字段值都错了,那么怎么知道这个数据包的结尾呢?如何挽救后续的数据包不被丢弃呢?这就是第三种方式了。
三、按照下一个开始标志方式拆包
    在第二种方式中,如果数据字段长度本身出错,依靠长度来确定数据包结尾的方式就出现了问题,似乎必须丢弃本次接收到的所有数据,可能会造成大量数据包丢弃的情况。然而不必沮丧,我们还有方法来应对,那就是检测下一个数据包的开始标志字节序列。
    当数据字段长度失效的时候,我们向后搜索下一个数据包的开始标志字节序列,如果搜索到了,也就表明下一个开始的前一个位置就是本数据包的结束位置。我们然后将本数据包丢弃,然后继续处理下一个数据包。这样丢弃的仅仅为一个出错的数据包,没有滥杀无辜。
    当然,这个方式不仅仅可以用来作为第二种方式的补充,甚至可以直接用来拆包。你可以直接根据下一个数据包的开始来确定上一个数据包的结束。但是如果只有一个数据包,你还是无法确定数据包的结束。
    所以我们一般是将第二和第三结合在一起用,相辅相成。甚至会将这三种方式全部用上。这叫做冗余设计。这样即使一个方式失效,依然可以通过其他方式来进行补充,最终确保效果是可靠的。
    那么第二种方式中,如果长度和固定的值匹配,就表明得到一个完整的数据包。如果长度和接收到的数据长度不匹配,则可能是粘包也可能是数据包被截断。如果接收到的数据比数据包应有的长度还要长,那就是粘包了。如果短,就是被截断了。而第三种方式中,如果连续搜索到下一个数据包,说明粘包,如果没有搜索到,那么就要是截断或者正常结束。此时我们要通过其他方式来补充确定是正常结束还是被截断了。前面说的是二和三的结合。我们这里就可以通过数据字段长度来辅助确定。当然都不限于此,我们还可以通过其他的字段的含义来确定,这个要视具体情况而定了。

    那么以上三种方式,基本是涵盖了协议拆包的常见方式。不过我们要明白的是,不管是哪种方式,无非就是确定开头、确定结尾,然后校验,然后处理。而具体实现方法,还要看具体的应用了。我们这里仅仅是举例说明了这几种方法,请灵活运用。