在socket网络程序中,TCP和UDP分别是面向连接和非面向连接的。
TCP的socket编程,收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。
对于UDP,不会使用块的合并优化算法,这样,实际上目前认为,是由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。
保护消息边界和流 ,那么什么是保护消息边界和流呢? 保护消息边界,就是指传输协议把数据当作一条独立的消息在网上 传输,接收端只能接收独立的消息.也就是说存在保护消息边界,接收 端一次只能接收发送端发出的一个数据包. 而面向流则是指无保护消息保护边界的,如果发送端连续发送数据, 接收端有可能在一次接收动作中,会接收两个或者更多的数据包. 我们举个例子来说,例如,我们连续发送三个数据包,大小分别是2k, 4k , 8k,这三个数据包,都已经到达了接收端的网络堆栈中,如果使 用UDP协议,不管我们使用多大的接收缓冲区去接收数据,我们必须有 三次接收动作,才能够把所有的数据包接收完.而使用TCP协议,我们 只要把接收的缓冲区大小设置在14k以上,我们就能够一次把所有的 数据包接收下来.只需要有一次接收动作. 这就是因为UDP协议的保护消息边界使得每一个消息都是独立的.而 流传输,却把数据当作一串数据流,他不认为数据是一个一个的消息. 所以有很多人在使用tcp协议通讯的时候,并不清楚tcp是基于流的 传输,当连续发送数据的时候,他们时常会认识tcp会丢包.其实不然, 因为当他们使用的缓冲区足够大时,他们有可能会一次接收到两个甚 至更多的数据包,而很多人往往会忽视这一点,只解析检查了第一个 数据包,而已经接收的其他数据包却被忽略了.所以大家如果要作这 类的网络编程的时候,必须要注意这一点. 结论: 根据以上所说,可以这样理解,TCP为了保证可靠传输,尽量减少额外 开销(每次发包都要验证),因此采用了流式传输,面向流的传输, 相对于面向消息的传输,可以减少发送包的数量。从而减少了额外开 销。但是,对于数据传输频繁的程序来讲,使用TCP可能会容易粘包。 当然,对接收端的程序来讲,如果机器负荷很重,也会在接收缓冲里 粘包。这样,就需要接收端额外拆包,增加了工作量。因此,这个特 别适合的是数据要求可靠传输,但是不需要太频繁传输的场合( 两次操作间隔100ms,具体是由TCP等待发送间隔决定的,取决于内核 中的socket的写法) 而UDP,由于面向的是消息传输,它把所有接收到的消息都挂接到缓冲 区的接受队列中,因此,它对于数据的提取分离就更加方便,但是, 它没有粘包机制,因此,当发送数据量较小的时候,就会发生数据包 有效载荷较小的情况,也会增加多次发送的系统发送开销(系统调用, 写硬件等)和接收开销。因此,应该最好设置一个比较合适的数据包 的包长,来进行UDP数据的发送。(UDP最大载荷为1472,因此最好能 每次传输接近这个数的数据量,这特别适合于视频,音频等大块数据 的发送,同时,通过减少握手来保证流媒体的实时性
UDP不存在粘包问题,是由于UDP发送的时候,没有经过Negal算法优化,不会将多个小包合并一次发送出去。另外,在UDP协议的接收端,采用了链式结构来记录每一个到达的UDP包,这样接收端应用程序一次recv只能从socket接收缓冲区中读出一个数据包。也就是说,发送端send了几次,接收端必须recv几次(无论recv时指定了多大的缓冲区)。
from: http://www.cnblogs.com/lancidie/archive/2013/10/28/3392428.html
TCP粘包处理-RingBuf方法
TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。粘包可能由发送方造成,也可能由接收方造成。TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据,造成多个数据包的粘连。如果接收进程不及时接收数据,已收到的数据就放在系统接收缓冲区,用户进程读取数据时就可能同时读到多个数据包。因为系统传输的数据是带结构的数据,需要做分包处理。
为了适应高速复杂网络条件,我们设计实现了粘包处理模块,由接收方通过预处理过程,对接收到的数据包进行预处理,将粘连的包分开。为了方便粘包处理,提高处理效率,在接收环节使用了环形缓冲区来存储接收到的数据。其结构如表1所示。
表1 环形缓冲结构
字段名 |
类型 |
含义 |
CS |
CRITICAL_SECTION |
保护环形缓冲的临界区 |
pRingBuf |
UINT8* |
缓冲区起始位置 |
pRead |
UINT8* |
当前未处理数据的起始位置 |
pWrite |
UINT8* |
当前未处理数据的结束位置 |
pLastWrite |
UINT8* |
当前缓冲区的结束位置 |
环形缓冲跟每个TCP套接字绑定。在每个TCP的SOCKET_OBJ创建时,同时创建一个PRINGBUFFER结构并初始化。这时候,pRingBuf指向环形缓冲区的内存首地址,pRead、pWrite指针也指向它。pLastWrite指针在这时候没有实际意义。初始化之后的结构如图1所示。
图1 初始化后的环形缓冲区
在每次投递一个TCP的接收操作时,从RINGBUFFER获取内存作接收缓冲区,一般规定一个最大值L1作为可以写入的最大数据量。这时把pWrite的值赋给BUFFER_OBJ的buf字段,把L1赋给bufLen字段。这样每次接收到的数据就从pWrite开始写入缓冲区,最多写入L1字节,如图 2。
图2 分配缓冲后的环形缓冲
如果某次分配过程中,pWrite到缓冲区结束的位置pEnd长度不够最小分配长度L1,为了提高接收效率,直接废弃最后一段内存,标记pLastWrite为pWrite。然后从pRingBuf开始分配内存,如图 3。
图 3 使用到结尾的环形缓冲
特殊情况下,如果处理包速度太慢,或者接收太快,可能导致未处理包占用大部分缓冲区,没有足够的缓冲区分配给新的接收操作,如图4。这时候直接报告错误即可。
图 4没有足够接收缓冲的环形缓冲
当收到一个长度为L数据包时,需要修改缓冲区的指针。这时候已经写入数据的位置变为(pWrite+L),如图 5。
图 5收到长度为L的数据的环形缓冲
分析上述环形缓冲的使用过程,收到数据后的情况可以简单归纳为两种:pWrite>pRead,接收但未处理的数据位于pRead到pWrite之间的缓冲区;pWrite
首先分析图6。此时,pRead是一个包的起始位置,如果L1足够一个包头长度,就获取该包的长度信息,记为L。假如L1>L,就说明一个数据包接收完成,根据包类型处理包,然后修改pRead指针,指向下一个包的起始位置(pRead+L)。这时候仍然类似于之前的状态,于是解包继续,直到L1不足一个包的长度,或者不足包头长度。这时退出解包过程,等待后续的数据到来。
图 6有未处理数据的环形缓冲(1)
图 7有未处理数据的环形缓冲(2)
图 8稍微复杂。首先按照上述过程处理L1部分。存在一种情况,经过若干个包处理之后,L1不足一个包,或者不足一个包头。如果这时(L1+L2)足够一个包的长度,就需要继续处理。另外申请一个最大包长度的内存区pTemp,把L1部分和L2的一部分复制到pTemp,然后执行解包过程。
经过上述解包之后,pRead就转向pRingBuf到pWrite之间的某个位置,从而回归情况图 6,继续按照图 6部分执行解包。