TCP通信粘包问题分析和解决

在socket网络程序中,TCP和UDP分别是面向连接和非面向连接的。因此TCP的socket编程,收发两端(客户端和服务器端)都要有成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小、数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。

对于UDP,不会使用块的合并优化算法,这样,实际上目前认为,是由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。所以UDP不会出现粘包问题。


TCP协议简介

作为一个面向连接的传输层协议,TCP的目标是为用户提供可靠的端到端连接,保证信息有序无误的传输。它除了提供基本的数据传输功能外,还为保证可靠性采用了数据编号、校验和计算、数据确认等一系列措施。

它对传送的每个数据字节都进行编号,并请求接收方回传确认信息(ACK)。发送方如果在规定的时间内没有收到数据确认,就重传该数据。

(1) 数据编号使接收方能够处理数据的失序和重复问题。

(2) 数据误码问题通过在每个传输的数据段中增加校验和予以解决,接收方在接收到数据后检查校验和,若校验和有误,则丢弃该有误码的数据段,并要求发送方重传。

(3) 流量控制也是保证可靠性的一个重要措施,若无流控,可能会因接收缓冲区溢出而丢失大量数据,导致许多重传,造成网络拥塞恶性循环。

(4) TCP采用可变窗口进行流量控制,由接收方控制发送方发送的数据量。

TCP为用户提供了高可靠性的网络传输服务,但可靠性保障措施也影响了传输效率。因此,在实际工程应用中,只有关键数据的传输才采用TCP,而普通数据的传输一般采用高效率的UDP。


保护消息边界和流

那么什么是保护消息边界和流呢?

保护消息边界,就是指传输协议把数据当作一条独立的消息在网上传输,接收端只能接收独立的消息。也就是说存在保护消息边界,接收端一次只能接收发送端发出的一个数据包。

而面向流则是指无保护消息保护边界的,如果发送端连续发送数据,接收端有可能在一次接收动作中,会接收两个或者更多的数据包。

例如,我们连续发送三个数据包,大小分别是2k,4k ,8k,这三个数据包,都已经到达了接收端的网络堆栈中,如果使用UDP协议,不管我们使用多大的接收缓冲区去接收数据,我们必须有三次接收动作,才能够把所有的数据包接收完.

而使用TCP协议,我们只要把接收的缓冲区大小设置在14k以上,我们就能够一次把所有的数据包接收下来,只需要有一次接收动作。


注意:

这就是因为UDP协议的保护消息边界使得每一个消息都是独立的。而流传输却把数据当作一串数据流,他不认为数据是一个一个的消息。所以有很多人在使用tcp协议通讯的时候,并不清楚tcp是基于流的传输,当连续发送数据的时候,他们时常会认识tcp会丢包。其实不然,因为当他们使用的缓冲区足够大时,他们有可能会一次接收到两个甚至更多的数据包,而很多人往往会忽视这一点,只解析检查了第一个数据包,而已经接收的其他数据包却被忽略了。所以大家如果要作这类的网络编程的时候,必须要注意这一点。


结论:

(1)TCP为了保证可靠传输,尽量减少额外开销(每次发包都要验证),因此采用了流式传输,面向流的传输,相对于面向消息的传输,可以减少发送包的数量,从而减少了额外开销。但是,对于数据传输频繁的程序来讲,使用TCP可能会容易粘包。当然,对接收端的程序来讲,如果机器负荷很重,也会在接收缓冲里粘包。这样,就需要接收端额外拆包,增加了工作量。因此,这个特别适合的是数据要求可靠传输,但是不需要太频繁传输的场合(两次操作间隔100ms,具体是由TCP等待发送间隔决定的,取决于内核中的socket的写法)

(2)UDP,由于面向的是消息传输,它把所有接收到的消息都挂接到缓冲区的接受队列中,因此,它对于数据的提取分离就更加方便,但是,它没有粘包机制,因此,当发送数据量较小的时候,就会发生数据包有效载荷较小的情况,也会增加多次发送的系统发送开销(系统调用,写硬件等)和接收开销。因此,应该最好设置一个比较合适的数据包的包长,来进行UDP数据的发送。(UDP最大载荷为1472,因此最好能每次传输接近这个数的数据量,这特别适合于视频,音频等大块数据的发送,同时,通过减少握手来保证流媒体的实时性

====================================================================


另:对于UDP来说就不存在拆包的问题,因为UDP是个"数据包"协议,也就是两段数据间是有界限的,在接收端要么接收不到数据要么就是接收一个完整的一段数据,不会少接收也不会多接收。