您的位置:首页 > 理论基础 > 计算机网络

TCP和UDP的"保护消息边界" 机制

2014-06-10 22:19 260 查看
在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,因此最好能每次传输接近这个数的数据量,这特别适合于视频,音频等大块数据的发送,同时,通过减少握手来保证流媒体的实时性)

关于TCP的粘包 正是由于TCP是流式传送的 也就是连接建立后可以一直不停的发送 并没有明确的边界定义 而你用UDP发送的时候 是可以按照一个一个数据包去发送的 一个数据包就是一个明确的边界

而TCP并没有数据包的概念 是完全流式的 他会开辟一个缓冲区 发送端往其中写入数据 每过一段时间就发送出去 然后接收端接收到这些数据 但是并不是说我发送了一次数据就肯定发送出去了 数据会在缓冲区中 有可能后续发送的数据和之前发送的数据同时存在缓冲区中随后一起发送
这就是粘包的一种形式 接收端也有产生粘包的情况 如果应用程序没有及时处理缓冲区中的数据 那么后续到达的数据会继续存放到缓冲区中 也就是2次接收的数据同时存在缓冲区中 下次取缓冲区的时候就会取出2次粘包后的数据 这是粘包的另外一种形式 还有其他许多形式 比如填充缓冲区到一半缓冲区满了直接发送了 但是其实那个包还没填充完全 这个就是不完整的粘包了 剩余数据会在下次发送的时候补上

======附上另外一篇技术文章===

TCP无保护消息边界的解决

针对这个问题,一般有3种解决方案:

(1)发送固定长度的消息

(2)把消息的尺寸与消息一块发送

(3)使用特殊标记来区分消息间隔

下面我们主要分析下前两种方法:

1、发送固定长度的消息

这种方法的好处是他非常容易,而且只要指定好消息的长度,没有遗漏未未发的数据,我们重写了一个SendMessage方法。代码如下:

private static int SendMessage(Socket s, byte[] msg)

{

int offset = 0;

int size = msg.Length;

int dataleft = size;

while (dataleft > 0)

{

int sent = s.Send(msg, offset, SocketFlags.None);

offset += sent;

dataleft -= sent;

}

return offset;

}

简要分析一下这个函数:形参s是进行通信的套接字,msg即待发送的字节数组。该方法使用while循环检查是否还有数据未发送,尤其当发送一个很庞大的数据包,在不能一次性发完的情况下作用比较明显。特别的,用sent来记录实际发送的数据量,和recv是异曲同工的作用,最后返回发送的实际数据总数。

有sentMessage函数后,还要根据指定的消息长度来设计一个新的Recive方法。代码如下:

private byte[] ReciveMessage(Socket s, int size)

{

int offset = 0;

int recv;

int dataleft = size;

byte[] msg = new byte[size];

while (dataleft > 0)

{

//接收消息

recv = s.Receive(msg, offset, dataleft, 0);

if (recv == 0)

{

break;

}

offset += recv;

dataleft -= recv;

}

return msg;

}

以上这种做法比较适合于消息长度不是很长的情况。

2、消息长度与消息一同发送

我们可以这样做:通过使用消息的整形数值来表示消息的实际大小,所以要把整形数转换为字节类型。下面是发送变长消息的SendMessage方法。具体代码如下:

private static int SendMessage(Socket s, byte[] msg)

{

int offset = 0;

int sent;

int size = msg.Length;

int dataleft = size;

byte[] msgsize = new byte[2];

//将消息尺寸从整形转换成可以发送的字节型

msgsize = BitConverter.GetBytes(size);

//发送消息的长度信息

sent = s.Send(size);

while (dataleft > 0)

{

sent = s.Send(msg, offset, dataleft, SocketFlags.None);

//设置偏移量

offset += sent;

dataleft -= sent;

}

return offset;

}

下面是接收变长消息的ReciveVarMessage方法。代码如下:

private byte[] ReciveVarMessage(Socket s)

{

int offset = 0;

int recv;

byte[] msgsize = new byte[2];

//将字节数组的消息长度信息转换为整形

int size = BitConverter.ToInt16(msgsize);

int dataleft = size;

byte[] msg = new byte[size];

//接收2个字节大小的长度信息

recv = s.Receive(msgsize, 0, 2, 0);

while (dataleft > 0)

{

//接收数据

recv = s.Receive(msg, offset, dataleft, 0);

if (recv == 0)

{

break;

}

offset += recv;

dataleft -= recv;

}

return msg;

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: