TCP流协议
TCP是一种流协议,这意着数据是以字节流的形式发送给接收者,没有固定的报文和报文边界的概念。接收端读取tcp数据是无法预知在这一次读操作中会返回多少字节。
假设主机A向主机B发送两条报文M1和M2,调用两次send发送两条独立的报文,但是数据在传输过程中并不会遵循这个方式。在发送端,send操作只是将数据复制到主机A的TCP/IP协议栈,由TCP决定怎么发送和每次发送多少。
这个决定的过程很复杂,如:发送窗口、拥塞窗口、路径上的最大传输单元等。也就是说数据的发送分为很多种情况:
- M1和M2分开发送数据。
- M1和M2数据一起传输。
- M1的先发送一部分,然后剩下的和M2一起发送。
- 先发送M1和M2的一部分,然后发送M2剩下的数据。
缓存发送
其实仔细看过TCP协议内容的人就可以发现,TCP协议允许发送端将几次发送的数据包缓存起来合成一个数据包发送到网络上去,因为这样可以获得更高的效率,这一行为通常是在操作系统提供的SOCKET中实现,所以在应用层对此毫无所觉。所以我们在程序中调用SOCKET的send发送了数据后操作系统有可能缓存了起来,等待后续的数据一起发送,而不是立即发送出去。send的文档中对此也有说明。
分包发送
网络传输的概念中有MTU的概念,也即是网络中一个数据包最大的长度。如果要发送超过这个长度的数据包,就需要分包发送。当调用SOCKET的send发送超过MTU的数据包时,操作系统提供的SOCKET实现会自动将这个数据包分割成几个不超过MTU的数据包发送。
粘包
当出现这些上面这些情况的时候,接收端就会发现接收到的数据和发送的数据的次数不一致。这个就是粘包现象。
当我们传输如文件这种数据时,流式的传输非常适合,但是当我们传输指令之类的数据结构时,流式模型就有一个问题:无法知道指令的结束。所以粘包必须问题是必须解决的。
定长结构
因为粘包问题的存在,接收端不能想当然的以为发送端一次发送了多少数据就能一次收到多少数据。如果发送端发送了一个固定长度的数据结构,接收端必须每次都严格判断接收到额数据的长度,当收到的数据长度不足时,需要再次接收数据,直到满足长度,当收到的数据多于固定长度时,需要截断数据,并将多余的数据缓存起来,视为长度不足需要再次接收处理。
不定长结构
定长的数据结构是一种理想的情况,真正的应用中通常使用的都是不定长的数据结构。
对于发送不定长的数据结构,简单的做法就是选一个固定的字符作为数据包结束标志,接收到这个字符就代表一个数据包传输完成了。
但是这只能应用于字符数据,因为二进制数据中很难确定结束字符到底是结束还是原本要传输的数据内容(使用字符来标识数据的边界在传输二进制数据时时可以实现的,只是实现比较复杂和低效。想了解可以参考以太网传输协议)。
目前最通用的做法是在每次发送的数据的固定偏移位置写入数据包的长度。
接收端只要一开始读取固定偏移的数据就可以知道这个数据包的长度,接下来的流程就和固定长度数据结构的处理流程类似。
所以对于处理粘包的关键在于提前获取到数据包的长度,无论这个长度是提前商定好的还是写在在数据包的开头。
因为在每次发送的数据的固定偏移位置写入数据包的长度的方法是最通用的一种方法,所以对这种方法实现中的一些容易出错误的地方在此特别说明。
- 通常我们使用2~4个字节来存放数据长度,多字节数据的网络传输需要注意字节序,所以要注意接受者和发送者要使用相同的字节序来解析数据长度。
- 每次新开始接收一段数据时不要急着直接去解析数据长度,先确保目前收到的数据已经足够解析出数据长度,例如数据开头的2个字节存储了数据长度,那么一定确保接收了2个字节以上的数据后才去解析数据长度。
- 如果没做到这一点的服务器代码,收到了一个字节就去解析数据长度的,结果得到的长度是内存中的随机值,结果必然是崩溃的。