问题:TCP与UDP收发的时候TCP有缓冲区还是UDP有缓冲区,使用它们时该注意什么?
(一)基础
1、TCP为可靠链接,分三次握手四次释放。
2、UDP为不可靠链接
(二)TCP与UDP的输出
每
个TCP套接口有一个发送缓冲区,可以用SO_SNDBUF套接口选项来改变这一缓冲区的大小。当应用进程调用write往套接口写数据时,内核从应用进
程缓冲区中拷贝所有数据到套接口的发送缓冲区,如果套接口发送缓冲区容不下应用程序的所有数据,或者是应用进程的缓冲区大于套接口的发送缓冲区,或者是套
接口的发送缓冲区中有别的数据,应用进程将被挂起。内核将不从write返回。直到应用进程缓冲区中的所有数据都拷贝到套接口发送缓冲区。所以,从写一个
TCP套接口的write调用成功返回仅仅表示我们可以重新使用应用进程缓冲区,它并不是告诉我们对方收到数据。TCP发给对方的数据,对方在收到数据时
必须给矛确认,只有在收到对方的确认时,本方TCP才会把TCP发送缓冲区中的数据删除。
UDP因为是不可靠连接,不必保存应用进程的数
据拷贝,应用进程中的数据在沿协议栈向下传递时,以某种形式拷贝到内核缓冲区,当数据链路层把数据传出后就把内核缓冲区中数据拷贝删除。因此它不需要一个
发送缓冲区。写UDP套接口的write返回表示应用程序的数据或数据分片已经进入链路层的输出队列,如果输出队列没有足够的空间存放数据,将返回错误
ENOBUFS.
(三)tcp socket的发送与接收缓冲区
应用程序可通过调用send(write, sendmsg等)利用tcp
socket向网络发送应用数据,而tcp/ip协议栈再通过网络设备接口把已经组织成struct
sk_buff的应用数据(tcp数据报)真正发送到网络上,由于应用程序调用send的速度跟网络介质发送数据的速度存在差异,所以,一部分应用数据被
组织成tcp数据报之后,会缓存在tcp
socket的发送缓存队列中,等待网络空闲时再发送出去。同时,tcp协议要求对端在收到tcp数据报后,要对其序号进行ACK,只有当收到一个tcp
数据报的ACK之后,才可以把这个tcp数据报(以一个struct sk_buff的形式存在)从socket的发送缓冲队列中清除。
tcp socket的发送缓冲区实际上是一个结构体struct sk_buff的队列,我们可以把它称为发送缓冲队列,由结构体struct
sock的成员sk_write_queue表示。sk_write_queue是一个结构体struct
sk_buff_head类型,这是一个struct sk_buff的双向链表,其定义如下:
struct sk_buff_head {
struct sk_buff *next; //后指针
struct sk_buff *prev; //前指针
__u32 qlen; //队列长度(即含有几个struct sk_buff)
spinlock_t lock; //链表锁
};
(1)
内核代码中,先在这个队列中创建足够存放数据的struct sk_buff,然后向队列存入应用数据。
结构体struct sock的成员sk_wmem_queued表示发送缓冲队列中已分配的字节数,一般来说,分配一个struct
sk_buff是用于存放一个tcp数据报,其分配字节数应该是MSS+协议首部长度。在我的实验环境中,MSS值是1448,协议首部取最大长度
MAX_TCP_HEADER,在我的实验环境中为224。经数据对齐处理后,最后struct
sk_buff的truesize为1956。也就是队列中每分配一个struct
sk_buff,成员sk_wmem_queue的值就增加1956。
struct sock的成员sk_forward_alloc是表示预分配长度。当我们第一次要为发送缓冲队列分配一个struct sk_buff时,我们并不是直接分配需要的内存大小,而是会以内存页为单位进行的预分配。
tcp协议分配struct
sk_buff的函数是sk_stream_alloc_pskb。它首先根据传入的参数指定的大小在内存中分配一个struct
sk_buff,如果成功,sk_forward_alloc取该大小值,并向上取整到页(4096字节)的整数倍。并累加到struct
sock的成员sk_prot,也即表示tcp协议的结构体mytcp_prot的成员memory_allocated中,该成员是一个指针,指向变量
tcp_memory_allocated,它表示的是当前整个TCP协议当前为缓冲区所分配的内存(包括读缓冲队列)
当把这个新分配成功的struct
sk_buff放入到缓冲队列sk_write_queue后,从sk_forward_alloc中减去该sk_buff的truesize值。第二次
分配struct
sk_buff时,只要再从sk_forward_alloc中减去新的sk_buff的truesize即可,如果sk_forward_alloc已
经小于当前的truesize,则将其再加上一个页的整数倍值,并累加入tcp_memory_allocated。
也就是说,通过sk_forward_alloc使全局变量tcp_memory_allocated保存当前tcp协议总的缓冲区分配内存的大小,并且该大小是页边界对齐的。
(2)
前面讲到struct
sock的成员sk_forward_alloc表示预分配内存大小,用于向全局变量mytcp_memory_allocated累加当前已分配的整个
TCP协议的缓冲区大小。之所以要累加这个值,是为了对tcp协议总的可用缓冲区大小作限制。表示TCP协议的结构体mytcp_prot还有几个成员与
缓冲区相关。
mysysctl_tcp_mem是一个数组,由mytcp_prot的成员sysctl_mem指向,数组共有三个元
素,mysysctl_tcp_mem[0]表示对缓冲区总的可用大小的最低限制,当前总共分配的缓冲区大小低于这个值,则没有问题,分配成功。
mysysctl_tcp_mem[2]表示对缓冲区可用大小的最高硬性限制,一旦总分配的缓冲区大小超出这个值,我们只好把tcp socket
的发送缓冲区的预设大小sk_sndbuf减小为已分配缓冲队列大小的一半,但不能小于SOCK_MIN_SNDBUF(2K),但保证这一次的分配成
功。mysysctl_tcp_mem[1]介于前面两个值的中间,这是一个警告值,一旦超出这个值,进入警告状态,这个状态下,根据调用参数来决定此次
分配是否成功。
这三个值的大小是根据所在系统的内存大小,在初始化时决定的,在我的实验环境中,内存大小为256M,这三个值分配是:96K,128K,192K。它们
可以通过/proc文件系统,在/proc/sys/net/ipv4/tcp_mem中进行修改。当然,除非特别需要,一般无需改动这些缺省值。
mysysctl_tcp_wmem也是一个同样结构的数组,表示发送缓冲区的大小限制,由mytcp_prot的成员sysctl_wmem指向,其缺
省值分别是4K,16K,128K。可以通过/proc文件系统,在/proc/sys/net/ipv4/tcp_wmem中进行修改。struct
sock的成员sk_sndbuf的值是真正的发送缓冲队列的预设大小,其初始值取中间一个16K。在tcp数据报的发送过程中,一旦
sk_wmem_queued超过sk_sndbuf的值,则发送停止,等待发送缓冲区可用。因为有可能一批已发送出去的数据还没有收到ACK,同时,缓
冲队列中的数据也可全部发出去,已达到清空缓冲队列的目的,所以,只要在网络不是很差的情况下(差到没有办法收到ACK),这个等待在一段时间后会成功
的。
全局变量mytcp_memory_pressure是一个标志,在tcp缓冲大小进入警告状态时,它置1,否则置0。
(3)
mytcp_sockets_allocated是到目前为止,整个tcp协议中创建的socket的个数,由
mytcp_prot的成员
sockets_allocated指向。可以在/proc/net/sockstat文件中查看,这只是一个供统计查看用的数据,没有任何实际的限制作
用。
mytcp_orphan_count表示整个tcp协议中待销毁的socket的个数(已无用的socket),由mytcp_prot的成员orphan_count指向,也可以在/proc/net/sockstat文件中查看。
mysysctl_tcp_rmem是跟mysysctl_tcp_wmem相同结构的数组,表示接收缓冲区的大小限制,由mytcp_prot的成员
sysctl_rmem指向,其缺省值分别是4096bytes,87380bytes,174760bytes。它们可以通过/proc文件系统,在
/proc/sys/net/ipv4/tcp_rmem中进行修改。struct
sock的成员sk_rcvbuf表示接收缓冲队列的大小,其初始值取mysysctl_tcp_rmem[1],成员sk_receive_queue
是接收缓冲队列,结构跟sk_write_queue相同。
tcp
socket的发送缓冲队列跟接收缓冲队列的大小既可以通过/proc文件系统进行修改,也可以通过TCP选项操作进行修改。套接字级别上的选项
SO_RCVBUF可用于获取和修改接收缓冲队列的大小(即strcut
sock->sk_rcvbuf的值),比如下列的代码可用于获取当前系统的接收缓冲队列大小:
int rcvbuf_len;
int len = sizeof(rcvbuf_len);
if( getsockopt( fd, SOL_SOCKET, SO_RCVBUF, (void *)&rcvbuf_len, &len ) < 0 ){
perror("getsockopt: ");
return -1;
}
printf("the recevice buf len: %d\n", rcvbuf_len );
而套接字级别上的选项SO_SNDBUF则用于获取和修改发送缓冲队列的大小(即struct sock->sk_sndbuf的值),代码同上,只需改SO_RCVBUF为SO_SNDBUF即可。
获取发送和接收缓冲区的大小相对简单一些,而设置的操作在内核中动作会稍微复杂一些,另外,在接口上也会有所差异,即由setsockopt传入的表示缓
冲区大小的参数是实际大小的1/2,即,如果想要设发送缓冲区的大小为20K,则需要这样调用setsockopt:
int rcvbuf_len = 10 * 1024; //实际缓冲区大小的一半。
int len = sizeof(rcvbuf_len);
if( setsockopt( fd, SOL_SOCKET, SO_SNDBUF, (void *)&rcvbuf_len, len ) < 0 ){
perror("getsockopt: ");
return -1;
}
在内核中,首先内核要判断新设置的值是否超过上限,若超过,则取上限为新值,发送和接收缓冲区大小的上限值分别为sysctl_wmem_max和
sysctl_rmem_max的2倍。这两个全局变量的值是相等的,都为(sizeof(struct sk_buff) + 256) *
256,大概为64K负载数据,由于struct
sk_buff的影响,实际发送和接收缓冲区的大小最大都可设到210K左右。它们的下限是2K,即缓冲区大小不能低于2K。
另外,SO_SNDBUF和SO_RCVBUF有一个特殊的版本:SO_SNDBUFFORCE和SO_RCVBUFFORCE,它们不受发送和接收缓冲区大小上限的限制,可设置不小于2K的任意缓冲区大小
此外还可以通过图例解释:
概念:
MTU:链路层上数据帧中数据的最大值,即IP数据报的整个值。详见TCP/IP第7页。数据进入协议栈的封装过程。
MSS:TCP报文段中数据的最大值---MSS选项只能出现在SYN报文中。
每个TCP套接口都有一个发送缓冲区,我们可以用SO_SNDBUF套接口选项来改变这个缓冲区的大小。当应用程序调用write时,内核从应用进程的缓冲区中拷贝所有数据到套接口的发送缓冲区。如 果 套接口发送缓冲区容不下应用程序所有的数据(或者应用进程的缓冲区大于套接口发送缓冲区,或者是套接口发送缓冲区还有其他数据),应用进程将被挂起,这里 假设write是阻塞的。内核将不从write系统调用返回,直到将应用进程缓冲区的所有数据都拷贝到套接口发送缓冲区。 因此从写一个TCP套接口的write调用成功返回仅仅代表我们重新使用应用进程的缓冲区。他并不告诉我们对端TCP或者应用进程已经接收到数据。
这一次我们展示的套接口发送缓冲区用虚框表示,因为它并不存在。UDP套接口有发送缓冲区大小(SO_SNDBUF修改),不过它仅仅是写到套接口的UDP数据报的大小上限。 如果应用程序写一个大于套接口发送缓冲区大小的数据报,内核将返回一个EMSGSIZE错误。 既然UDP不可靠,他不必保存应用进程的数据拷贝,因此无需真正的发送缓冲区(应用进程的数据在沿协议栈往下传递,以某种形式拷贝到内核缓冲区,然而数据链路层在送出数据之后将丢弃该拷贝)。
根据上图发现,UDP没有MSS的概念,如果某个UDP应用程序发送大数据,那么他比TCP应用程序更容易分片。从UDP套接口 write成功返回仅仅表示用户写入的数据报或者所有片段已经加入到数据链路层的输出队列。如果该队列没有足够的空间存放该数据报或者他的某个片段,内核 通常返回给应用进程一个ENOBUFS错误(也有的系统不会返回错误)。
TCP和UDP都拥有套接口接收缓冲区。TCP套接口接收缓冲区不可能溢出,因为TCP具有流量控制(窗口).然而对于TCP来说, 当接收到的数据报装不进套接口接收缓冲区时,该数据报就丢弃 。UDP是没有流量控制的:较快的发送端可以很容易淹没较慢的接收端,导致接收端的UDP丢弃数据报。
我们可以用程序来验证这一点:
#define NDG 2000
#define DGLEN 1400
client()
{
for(int i=0;i<NDG;i++)
sendto(sockfd,sendline,DGLEN,0,pservadd,servlen);
}
客户端快速发送大数据报,我们在一个慢速的主机(FreeBSD)上的接收端就发现很多丢包现象。UDP套接口接收缓冲区在 FreeBSD下面缺省是42080字节,也就是30*1400个字节的容纳空间。如果我们增大接收缓冲区,服务器就期望接收更多的数据报。 setsockopt(sockfd,SOL_SOCKET,SO_RECVBUF,&n,sizeof(n)),其中n=220*1024,这 个时候如果再次运行就会发现丢包有所改善(但并没实质解决)。SO_RCVBU和SO_SNDBUF分别设置接收缓冲区和发送缓冲区大小。