問題: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分別設置接收緩沖區和發送緩沖區大小。