1. 程式人生 > 其它 >TCP之send & recv,一篇就夠了

TCP之send & recv,一篇就夠了

接觸過網路開發的人,大抵都知道,上層應用使用send函式傳送資料,使用recv來接收資料,而send和recv的實現原理又是怎樣的呢?

在前面的幾篇文章中,我們有提過,TCP是個可靠的、全雙工協議。其流量控制或者擁塞控制依賴於滑動視窗和擁塞視窗的滑動來實現,而這兩個視窗的滑動實現則是依賴於TCP中的兩個buffer,這兩個buffer則是TCP socket在核心中的傳送緩衝區(send buffer)和接收緩衝區(recv buffer)。

在本文中,我們首先會簡單介紹下TCP中傳送緩衝區和接收緩衝區的作用(對於後面理解send和recv非常重要),然後講解Linux系統下,TCP傳送和接收資料是如何實現的。

緩衝區

緩衝區,可以理解為是一個臨時快取。

對於傳送端來說,socket將資料拷貝到傳送臨時緩衝區,就立即返回到應用層去做其他的事情,而剩下的將臨時緩衝區的資料通過核心傳送到對端,這就是tcp的事。

對於接收端來說,核心將網路中的資料拷貝到緩衝區,等待上層應用讀取。

傳送緩衝區

上面有講,程序在呼叫send()傳送的資料的時候,最簡單情況(也是一般情況), 將資料拷貝進入socket的核心傳送緩衝區之中,然後send便會立即返回。

換句話說,在應用層呼叫send()返回之時,資料不一定會發送到對端去(和write寫檔案有點類似),send()僅僅是把應用層buffer的資料拷貝進socket的核心傳送buffer中。

TCP socket有兩種模式,即阻塞模式和非阻塞模式。

  • 在阻塞模式下, send函式的過程是將應用程式請求傳送的資料拷貝到傳送快取中傳送並得到確認後再返回.但由於傳送快取的存在,表現為:如果傳送快取大小比請求傳送的大小要大,那麼send函式立即返回,同時向網路中傳送資料;否則,send向網路傳送快取中不能容納的那部分資料,並等待對端確認後再返回(接收端只要將資料收到接收快取中,就會確認,並不一定要等待應用程式呼叫recv)

  • 在非阻塞模式下,send函式的過程僅僅是將資料拷貝到協議棧的快取區而已,如果快取區可用空間不夠,則盡能力的拷貝,返回成功拷貝的大小;如快取區可用空間為0,則返回-1,同時設定errno為EAGAIN.

在Linux核心中,有兩種方式可以檢視tcp緩衝區buffer大小。

1、通過檢視/etc/sysctl.ronf下的net.ipv4.tcp_wmem值

2、 通過命令'cat /proc/sys/net/ipv4/tcp_wmem'

cat/proc/sys/net/ipv4/tcp_wmem
4096163844194304

從上面可以看出,在筆者所在的伺服器上,tcp send緩衝區buffer有3個值,分別是4096 16384 4194304。

  • 第一個值是socket的傳送快取區分配的最少位元組數,
  • 第二個值是預設值(該值會被net.core.wmem_default覆蓋),快取區在系統負載不重的情況下可以增長到這個值
  • 第三個值是傳送快取區空間的最大位元組數(該值會被net.core.wmem_max覆蓋)

我們可以通過程式,來修改當前tcp socket的傳送緩衝區大小,需要注意的是,如下的程式碼修改,只會修改當前特定的socket。

intbuffer_len=10240;
setsockopt(fd,SOL_SOCKET,SO_SNDBUF,(void*)&buffer_len,buffer_len);
接收緩衝區

接收緩衝區被TCP用來快取網路上來的資料,一直儲存到應用程序讀走為止。

對於TCP,如果應用程序一直沒有讀取,接收緩衝區滿了之後,發生的動作是:收端通知發端,接收視窗關閉(win=0)。這個便是滑動視窗的實現。保證TCP套介面接收緩衝區不會溢位,從而保證了TCP是可靠傳輸。因為對方不允許發出超過所通告視窗大小的資料。 這就是TCP的流量控制,如果對方無視視窗大小而發出了超過視窗大小的資料,則接收方TCP將丟棄它。

與檢視傳送緩衝區大小的方式一樣,接收緩衝區也是通過如上的兩種方式。 1、通過檢視/etc/sysctl.ronf下的net.ipv4.tcp_rmem值

2、通過命令'cat /proc/sys/net/ipv4/tcp_rmem'

cat/proc/sys/net/ipv4/tcp_rmem
4096873804194304

TCP接收緩衝區buffer有3個值,分別是4096 87380 4194304。

  • 第一個值是socket的接收快取區的最少位元組數,
  • 第二個值是預設值(該值會被net.core.rmem_default覆蓋),快取區在系統負載不重的情況下可以增長到這個值
  • 第三個值是接收快取區空間的最大位元組數(該值會被net.core.rmem_max覆蓋)

同樣的,可以通過如下程式碼,修改接收緩衝區的大小。

intbuffer_len=10240;
setsockopt(fd,SOL_SOCKET,SO_RCVBUF,(void*)&buffer_len,buffer_len);

實現原理

為了便於我們理解TCP的整個傳輸過程,我們先了解下TCP的四層模型以及四冊模型在資料傳輸中的流向。後面我們將從四層模型的角度來分析send和recv函式在每層中都做了什麼。

send原理
NAME
send,sendto,sendmsg-sendamessageonasocket

SYNOPSIS
#include<sys/types.h>
#include<sys/socket.h>

ssize_tsend(intsockfd,constvoid*buf,size_tlen,intflags);

DESCRIPTION
Thesystemcallssend(),sendto(),andsendmsg()areusedtotransmitamessagetoanothersocket.

當呼叫該函式時,send函式: 1、先比較待發送資料的長度len和套接字sockfd的可用傳送緩衝區的長度

  • 如果資料長度len大於傳送緩衝區的長度,則分多次傳送
  • 如果果len小於或者等於sockfd的緩衝區長度,那麼send先檢查協議是否正在傳送sockfd的傳送緩衝中的資料
    • 如果是就等待協議把資料傳送完
    • 否則,如果協議還沒有開始傳送s的傳送緩衝中的資料或者s的傳送緩衝中沒有資料,那麼send就比較sockfd的傳送緩衝區的剩餘空間和len
      • 如果len大於剩餘空間大小,send就一直等待協議把s的傳送緩衝中的資料傳送完
      • 如果len小於剩餘空間大小,send就僅僅把buf中的資料copy到剩餘空間裡。 如果send函式copy資料成功,就返回實際copy的位元組數,如果send在copy資料時出現錯誤,那麼send就返回SOCKET_ERROR; 如果send在等待協議傳送資料時網路斷開的話,那麼send函式也返回SOCKET_ERROR。 需要注意send函式把buf中的資料成功copy到s的傳送緩衝的剩餘空間裡後它就返回了,但是此時這些資料並不一定馬上被傳到連線的另一端。 如果協議在後續的傳送過程中出現網路錯誤的話,那麼下一個socket函式就會返回SOCKET_ERROR.(每一個除send外的socket函式在執行的最開始總要先等待套接字的傳送緩衝中的資料被協議傳送完畢才能繼續,如果在等待時出現網路錯誤,那麼該socket函式就返回SOCKET_ERROR)。

下面我們從從四層模型的角度來分析send實現。

應用層

對於TCP,應用程式在建立socket之後,呼叫connect()函式,通過socket使客戶端和服務端建立連線。然後就可以呼叫send函式傳送資料。

傳輸層

資料在傳輸層進行處理,以TCP協議為例,其主要有以下功能:

  • 1、構造TCP段
  • 2、計算校驗和
  • 3、傳送回覆(ACK)包
  • 4、滑動視窗(sliding windown)等操作保證可靠性。

不同的協議有不同的傳送函式,TCP呼叫tcp_sendmsg函式,而UDP則呼叫的是sock_sendmsg函式。

tcp_sendmsg()的主要工作是傳輸使用者層的資料,將資料放入skb中。然後呼叫tcp_push()傳送,tcp_push函式呼叫tcp_write_xmit() 函式,依次呼叫傳送函式tcp_transmit_skb將skb封裝tcp頭之後,回撥ip_queue_xmit。

網路層

ip_queue_xmit(skb)主要有路由查詢校驗、封裝ip頭和ip選項,最後通過ip_local_out傳送資料包。

資料鏈路層

資料鏈路層在不可靠的物理介質上提供可靠的傳輸。該層的功能包括:實體地址定址、資料成幀、流量控制、資料錯誤檢測、重發等。這一層的資料單位稱為幀(frame)。

上圖為send函式原始碼的呼叫邏輯圖,對原始碼有興趣的話,可以在net/tcp.c找到對應的實現。

recv原理
NAME
recv,recvfrom,recvmsg-receiveamessagefromasocket

SYNOPSIS
#include<sys/types.h>
#include<sys/socket.h>

ssize_trecv(intsockfd,void*buf,size_tlen,intflags);

ssize_trecvfrom(intsockfd,void*buf,size_tlen,intflags,
structsockaddr*src_addr,socklen_t*addrlen);

ssize_trecvmsg(intsockfd,structmsghdr*msg,intflags);

DESCRIPTION
Therecvfrom()andrecvmsg()callsareusedtoreceivemessagesfromasocket,andmaybeusedtoreceivedataonasocketwhetherornotitisconnection-oriented.

Ifsrc_addrisnotNULL,andtheunderlyingprotocolprovidesthesourceaddress,thissourceaddressisfilledin.Whensrc_addrisNULL,nothingisfilledin;inthiscase,addrlenisnotused,andshouldalsobeNULL.Theargument
addrlenisavalue-resultargument,whichthecallershouldinitializebeforethecalltothesizeofthebufferassociatedwithsrc_addr,andmodifiedonreturntoindicatetheactualsizeofthesourceaddress.Thereturnedaddressis
truncatedifthebufferprovidedistoosmall;inthiscase,addrlenwillreturnavaluegreaterthanwassuppliedtothecall.

Therecv()callisnormallyusedonlyonaconnectedsocket(seeconnect(2))andisidenticaltorecvfrom()withaNULLsrc_addrargument.

當呼叫該函式時候:

  • 先檢查套接字sockfd的接收緩衝區
  • 如果sockfd接收緩衝區中沒有資料或者協議正在接收資料,那麼recv就一直等待,直到協議把資料接收完畢。
  • 當協議把資料接收完畢,recv函式就把sockft的接收緩衝中的資料copy到buf中,recv函式返回其實際copy的位元組數。
  • 如果recv在copy時出錯,那麼它返回SOCKET_ERROR;
  • 如果recv函式在等待協議接收資料時網路中斷了,那麼它返回0 。
  • 對方優雅的關閉socket並不影響本地recv的正常接收資料;
  • 如果協議緩衝區內沒有資料,recv返回0,指示對方關閉;
  • 如果協議緩衝區有資料,則返回對應資料(可能需要多次recv),在最後一次recv時,返回0,指示對方關閉。

如果對具體實現不是很感興趣,可直接此部分

從四層模型的角度來分析recv實現。

資料鏈路層

當資料包到達機器的物理網絡卡時會觸發一箇中斷,中斷處理程式分配skb_buff資料結構,並將從網絡卡I/O接收到的資料幀複製到skb_buff緩衝區,並設定skb_buff相應的引數。

然後發出軟中斷,通知核心接收新的資料幀。進入軟中斷處理流程,呼叫net_rx_action函式。進入 netif _receive_skb 處理流程。

netif_receive_skb 根據在全域性陣列 ptype_all 和 ptype_base 中註冊的網路層資料報型別,將資料報傳送到不同的網路層協議接收函式(INET域主要是ip_rcv和arp_rcv)。

網路層

ip_rcv函式為網路層的入口函式。該函式做的第一件事就是資料校驗,然後呼叫ip_rcv_finish這個函式。

ip_rcv_finish函式會呼叫ip_route_input函式來更新路由,然後尋找路由,決定訊息是傳送到本地機器,轉發還是丟棄。

如果傳送到本機,則呼叫ip_local_deliver函式,可以進行碎片整理(合併多個包),並呼叫ip_local_deliver_finish。最後呼叫下一層介面,包括tcp_v4_rcv(TCP)、udp_rcv(UDP)、icmp_rcv(ICMP)、igmp_rcv(IGMP)。如果需要轉發,則進入轉發流程,呼叫dev_queue_xmit,進入鏈路層處理流程。如果不是傳送到本機,應該是轉發,呼叫 ip_forward 進行轉發 。

傳輸層

在該層,我們會做一些完整性檢查,如果發現問題就丟包。如果是tcp,則呼叫tcp_v4_do_rcv。

然後sk->sk_state == TCP_ESTABLISHED,呼叫tcp_rcv_builted,呼叫 tcp_data_queue 方法將訊息放入佇列。然後使用 tcp_ofo_queue 方法將訊息插入接收到 Queued 。

應用層

應用程式呼叫讀取或者 recv 的時候,該呼叫被對映到 /net/socket.c 中的sys_recv系統呼叫,然後呼叫 sock_recvmsg 函式。

TCP 會呼叫 tcp_recvmsg。該函式從套接字緩衝區複製資料到緩衝區。

上述過程,我們總結下就是: 1、資料幀從外部網路到達網絡卡 2、網絡卡把幀DMA到記憶體Ring Buffer中 3、硬中斷通知CPU 4、CPU響應硬中斷,簡單處理後發憷軟中斷 5、軟中斷程序處理軟中斷,呼叫網絡卡驅動註冊的pool函式開始收包 6、幀被從Ring Buffer中摘下來,儲存到skb中 7、協議層開始處理網路幀,並將處理完成後的資料放入socket的接收緩衝區中

上圖為整個網路資料接收的函式呼叫過程,對月接收端來說,當有資料來的時候,都是通過終端來通知核心,最終通過回撥,呼叫系統函式。

下圖是send和recv完整的函式呼叫過程

常見問題

在實際應用中,如果傳送端是非阻塞傳送,由於網路的阻塞或者接收端處理過慢,通常出現的情況是,傳送應用程式看起來發送了10k的資料,但是隻傳送了2k到對端快取中,還有8k在本機快取中(未傳送或者未得到接收端的確認).那麼此時,接收應用程式能夠收到的資料為2k.假如接收應用程式呼叫recv函式獲取了1k的資料在處理,在這個瞬間,發生了以下情況之一,雙方表現為:

  1. 傳送應用程式認為send完了10k資料,關閉了socket:

傳送主機作為tcp的主動關閉者,連線將處於FIN_WAIT1的半關閉狀態(等待對方的ack),並且,傳送快取中的8k資料並不清除,依然會發送給對端.如果接收應用程式依然在recv,那麼它會收到餘下的8k資料(這個前題是,接收端會在傳送端FIN_WAIT1狀態超時前收到餘下的8k資料.), 然後得到一個對端socket被關閉的訊息(recv返回0).這時,應該進行關閉.

  1. 傳送應用程式再次呼叫send傳送8k的資料:

假如傳送快取的空間為20k,那麼傳送快取可用空間為20-8=12k,大於請求傳送的8k,所以send函式將資料做拷貝後,並立即返回8192;

假如傳送快取的空間為12k,那麼此時傳送快取可用空間還有12-8=4k,send()會返回4096,應用程式發現返回的值小於請求傳送的大小值後,可以認為快取區已滿,這時必須阻塞(或通過select等待下一次socket可寫的訊號),如果應用程式不理會,立即再次呼叫send,那麼會得到-1的值, 在linux下表現為errno=EAGAIN.

  1. 接收應用程式在處理完1k資料後,關閉了socket: 接收主機作為主動關閉者,連線將處於FIN_WAIT1的半關閉狀態(等待對方的ack).然後,傳送應用程式會收到socket可讀的訊號(通常是 select呼叫返回socket可讀),但在讀取時會發現recv函式返回0,這時應該呼叫close函式來關閉socket(傳送給對方ack);

如果傳送應用程式沒有處理這個可讀的訊號,而是在send,那麼這要分兩種情況來考慮,假如是在傳送端收到RST標誌之後呼叫send,send將返回-1,同時errno設為ECONNRESET表示對端網路已斷開,但是,也有說法是程序會收到SIGPIPE訊號,該訊號的預設響應動作是退出程序,如果忽略該訊號,那麼send是返回-1,errno為EPIPE(未證實);如果是在傳送端收到RST標誌之前,則send像往常一樣工作;

以上說的是非阻塞的send情況,假如send是阻塞呼叫,並且正好處於阻塞時(例如一次性發送一個巨大的buf,超出了傳送快取),對端socket關閉,那麼send將返回成功傳送的位元組數,如果再次呼叫send,那麼會同上一樣.

  1. 交換機或路由器的網路斷開:

接收應用程式在處理完已收到的1k資料後,會繼續從快取區讀取餘下的1k資料,然後就表現為無資料可讀的現象,這種情況需要應用程式來處理超時.一般做法是設定一個select等待的最大時間,如果超出這個時間依然沒有資料可讀,則認為socket已不可用.

傳送應用程式會不斷的將餘下的資料傳送到網路上,但始終得不到確認,所以快取區的可用空間持續為0,這種情況也需要應用程式來處理.

如果不由應用程式來處理這種情況超時的情況,也可以通過tcp協議本身來處理,具體可以檢視sysctl項中的: net.ipv4.tcp_keepalive_intvl net.ipv4.tcp_keepalive_probes net.ipv4.tcp_keepalive_time

結論

  • TCP協議本身是為了保證可靠傳輸,並不等於應用程式用tcp傳送資料就一定是可靠的,必須要容錯;

  • send()只負責拷貝,拷貝到核心就返回

  • 此次send()呼叫所觸發的程式錯誤,可能會在本次返回,也可能在下次呼叫網路IO函式的時候被返回。

  • 在進行TCP協議傳輸的時候,要注意資料流傳輸的特點,recv和send不一定是一一對應的(一般情況下是一一對應),也就是說並不是send一次,就一定recv一次就接收完,有可能send一次,recv多次才接收完,也可能send多次,一次recv就接收完了。TCP協議會保證資料的有序完整的傳輸,但是如何去正確完整的處理每一條資訊,是開發人員的事情。

伺服器在迴圈recv,recv的緩衝區大小為100byte,客戶端在迴圈send,每次send 6byte資料,則recv每次收到的資料可能為6byte,12byte,18byte,這是隨機的,程式設計的時候注意正確的處理。

參考文件

https://slidetodoc.com/network-applications-user-socket-bsd-sockets-kernel-sock/ https://www.programmersought.com/article/819124112/ https://www.programmersought.com/article/749525105/ https://www.fatalerrors.org/a/0dl00z0.html https://blog.csdn.net/w839687571/article/details/44409355 http://lkml.iu.edu/hypermail/linux/kernel/1405.3/01700.html https://linux-kernel-labs.github.io/refs/heads/master/labs/networking.html https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/ipv4/tcp.c#n1581