recv/send堵塞和非堵塞
recv/send堵塞和非堵塞理解
- TCP之深入淺出send和recv
- 需要理解的3個概念
- 實例詳解send()
- send函數
- recv函數
參考:
TCP之深入淺出send和recv
linux下非阻塞的tcp研究
題外話
今天在看epoll的ET模式時,說ET模式時,套接字描述符必須設置成非堵塞模式,為什么 IO 多路復用要搭配非阻塞 IO?
于是想看看堵塞和非堵塞recv/send的區別,網上魚龍混雜的博文,錯誤百出,查了好久,在此做個總結,如有錯誤的地方,希望大家指出來。
關于阻塞和非阻塞read/write,可以參考阻塞和非阻塞read/write
-------------------------------------這部分轉載于TCP之深入淺出send和recv
TCP之深入淺出send和recv
需要理解的3個概念
1.TCP socket的buffer
每個TCP socket在內核中都有一個發送緩沖區和一個接收緩沖區,TCP的全雙工的工作模式以及TCP的流量(擁塞)控制便是依賴于這兩個獨立的buffer以及buffer的填充狀態。
接收緩沖區把數據緩存入內核,應用進程一直沒有調用recv()進行讀取的話,此數據會一直緩存在相應socket的接收緩沖區內。
再啰嗦一點,不管進程是否調用recv()讀取socket,對端發來的數據都會經由內核接收并且緩存到socket的內核接收緩沖區之中。
recv()所做的工作,就是把內核緩沖區中的數據拷貝到應用層用戶的buffer里面,并返回,僅此而已。進程調用send()發送的數據的時候,最簡單情況(也是一般情況),將數據拷貝進入socket的內核發送緩沖區之中,然后send便會在上層返回。換句話說,send()返回之時,數據不一定會發送到對端去(和write寫文件有點類似),send()僅僅是把應用層buffer的數據拷貝進socket的內核發送buffer中,發送是TCP的事情,和send其實沒有太大關系。接收緩沖區被TCP用來緩存網絡上來的數據,一直保存到應用進程讀走為止。
對于TCP,如果應用進程一直沒有讀取,接收緩沖區滿了之后,發生的動作是:收端通知發端,接收窗口關閉(win=0)。這個便是滑動窗口的實現。保證TCP套接口接收緩沖區不會溢出,從而保證了TCP是可靠傳輸。因為對方不允許發出超過所通告窗口大小的數據。 這就是TCP的流量控制,如果對方無視窗口大小而發出了超過窗口大小的數據,則接收方TCP將丟棄它。
查看測試機的socket發送緩沖區大小,
cat /proc/sys/net/ipv4/tcp_wmem 4096 16384 4194304第一個值是一個限制值,socket發送緩存區的最少字節數;
第二個值是默認值;
第三個值是一個限制值,socket發送緩存區的最大字節數;
根據實際測試,發送緩沖區的尺寸在默認情況下的全局設置是16384字節,即16k。
在測試系統上,發送緩存默認值是16k。
proc文件系統下的值和sysctl中的值都是全局值,應用程序可根據需要在程序中使用setsockopt()對某個socket的發送緩沖區尺寸進行單獨修改,詳見文章《TCP選項之SO_RCVBUF和SO_SNDBUF》,不過這都是題外話。
2.接收窗口(滑動窗口)
TCP連接建立之時的收端的初始接受窗口大小是14600,細節如圖2所示(129是收端,130是發端)
圖2
接收窗口是TCP中的滑動窗口,TCP的收端用這個接受窗口----win=14600,通知發端,我目前的接收能力是14600字節。
后續發送過程中,收端會不斷的用ACK ( ACK的全部作用請參照博文《TCP之ACK發送情景 )。通知發端自己的接收窗口的大小狀態,如圖3,而發端發送數據的量,就根據這個接收窗口的大小來確定,發端不會發送超過收端接收能力的數據量。這樣就起到了一個流量控制的的作用。
圖3
圖3說明
21,22兩個包都是收端發給發端的ACK包
第21個包,收端確認收到的前7240個字節數據,7241的意思是期望收到的包從7241號開始,序號加了1.同時,接收窗口從最初的14656(如圖2)經過慢啟動階段增加到了現在的29120。用來表明現在收端可以接收29120個字節的數據,而發端看到這個窗口通告,在沒有收到新的ACK的時候,發端可以向收端發送29120字節這么多數據。
第22個包,收端確認收到的前8688個字節數據,并通告自己的接收窗口繼續增長為32000這么大。
3.單個TCP的負載量和MSS的關系
MSS在以太網上通常大小是1460字節,而我們在后續發送過程中的單個TCP包的最大數據承載量是1448字節,這二者的關系可以參考博文《TCP之1460MSS和1448負載》。
實例詳解send()
實例功能說明:接收端129作為客戶端去連接發送端130,連接上之后并不調用recv()接收,而是sleep(1000),把進程暫停下來,不讓進程接收數據。內核會緩存數據至接收緩沖區。發送端作為服務器接收TCP請求之后,立即用ret = send(sock,buf,70k,0);這個C語句,向接收端發送70k數據。
我們現在來觀察這個過程。看看究竟發生了些什么事。wireshark抓包截圖如下圖4
圖4說明,包序號等同于時序
send()要發送的數據是70k,現在發出去了66800字節,發送緩存中還有16k,應用層剩余要拷貝進內核的數據量是N=70k-66800-16k。接收端仍處于sleep狀態,無法recv()數據,這將導致接收緩沖區一直處于積壓滿的狀態,窗口會一直通告0(win=0)。發送端在這樣的狀態下徹底無法發送數據了,send()的剩余數據無法繼續拷貝進內核的發送緩沖區,最終導致send()被阻塞在應用層;
圖4和send()的關系說明完畢。
那什么時候send返回呢?有3種返回場景
send()返回場景
- 場景1,我們繼續圖4這個例子,不過這兒開始我們就跳出圖4所示的過程了
圖5
隨著進程不斷的用"recv(fd,buf,2048,0);"將數據從內核的接收緩沖區拷貝至應用層的buf,在使用win=0關閉接收窗口之后,現在接收緩沖區又逐漸恢復了緩存的能力,這個條件下,收端會主動發送攜帶"win=n(n>0)"這樣的ACK包去通告發送端接收窗口已打開;
- 場景2,我們繼續圖4這個例子,不過這兒開始我們就跳出圖4所示的過程了
- 場景3,和以上例子沒關系
連接上之后,馬上send(1k),這樣,發送的數據肯定可以一次拷貝進入發送緩沖區,send()拷貝完數據立即成功返回。
send()發送結論
send()只是負責拷貝,拷貝完立即返回,不會等待發送和發送之后的ACK。如果socket出現問題,RST包被反饋回來。在RST包返回之時,如果send()還沒有把數據全部放入內核或者發送出去,那么send()返回-1,errno被置錯誤值;如果RST包返回之時,send()已經返回,那么RST導致的錯誤會在下一次send()或者recv()調用的時候被立即返回。
概念上容易疑惑的地方
實際上理解了阻塞式的,就能理解非阻塞的。
參考linux下非阻塞的tcp研究
在阻塞模式下,send函數的過程是將應用程序請求發送的數據拷貝到內核發送緩存中,待發送數據完全被拷貝到內核發送緩存區中才返回,當然如果內核發送緩存區一直沒有空間能容納待發送的數據,則一直阻塞;
在非阻塞模式下,send函數的過程也是將應用程序請求發送的數據拷貝內核發送緩存中,區別在于非堵塞模式下,send函數不需要等到待發送數據完全被拷貝到內核發送區中才返回。
如果內核緩存區可用空間不夠容納所有待發送數據,則盡能力的拷貝,返回成功拷貝的大小;
如果緩存區可用空間為0,則返回-1,同時設置errno為EAGAIN.
-------------------------------------------------以下部分為個人思考總結所得
send函數
參考linux下非阻塞的tcp研究
注意并不是send把s的發送緩沖中的數據傳到連接的另一端的,而是底層TCP/IP協議棧傳的,send僅僅是把用戶buf中的數據copy到s的發送緩沖區的剩余空間里
在阻塞模式下,send函數的過程是將應用程序請求發送的數據拷貝到內核發送緩存中,待發送數據完全被拷貝到內核發送緩存區中才返回,當然如果內核發送緩存區一直沒有空間能容納待發送的數據,則一直阻塞;
在非阻塞模式下,send函數的過程也是將應用程序請求發送的數據拷貝內核發送緩存中,區別在于非堵塞模式下,send函數不需要等到待發送數據完全被拷貝到內核發送區中才返回。
如果內核緩存區可用空間不夠容納所有待發送數據,則盡能力的拷貝,返回成功拷貝的大小;
如果緩存區可用空間為0,則返回-1,同時設置errno為EAGAIN.
看看官方手冊中的描述https://linux.die.net/man/2/send,摘抄如下
If the message is too long to pass atomically through the underlying protocol, the error EMSGSIZE is returned, and the message is not transmitted.
No indication of failure to deliver is implicit in a send(). Locally detected errors are indicated by a >return value of -1.
When the message does not fit into the send buffer of the socket, send() normally blocks, unless the socket has been placed in nonblocking I/O mode. In nonblocking mode it would fail with the error EAGAIN or EWOULDBLOCK in this case. The select(2) call may be used to determine when it is possible to send more data.
在看看《UNIX網絡編程卷1》第 16 章 非阻塞式 I/O中的描述,如下
該函數的:
第一個參數指定發送端套接字描述符;
第二個參數指明一個存放應用程序要發送數據的緩沖區;
第三個參數指明實際要發送的數據的字節數;
第四個參數一般置0。
堵塞模式下socket的send函數的執行流程:
1.如果內核發送緩沖區可用大小為0,send()直接堵塞。。。,直到內核發送緩沖區里的數據被系統發送后,騰出空間后,send()再將剩余的待發送數據拷貝到內核發送緩沖區中去;
2.如果內核發送緩沖區可用空間小于待發送的數據長度len,則send()函數會先把部分數據拷貝到內核發送緩沖區中,然后會阻塞。。。,直到內核發送緩沖區里的數據被系統發送后,騰出空間后,send()再將剩余的待發送數據拷貝到內核發送緩沖區中去;
不知道為啥百度百科上send()是 “send先比較待發送數據的長度len和套接字s的發送緩沖的長度, 如果len大于s的發送緩沖區的長度,該函數返回SOCKET_ERROR;”
明顯感覺描述不對,按這樣說,那不就無法發送大于socket內核發送緩沖區的長度的數據了,可以看看socket內核發送緩沖區的默認大小
cat /proc/sys/net/ipv4/tcp_wmem
4096 16384 4161536
表明socket內核發送緩沖區默認大小為16kB,那發送大于16kB的數據怎么辦呢?所以這里明顯有問題
官方手冊的原話是
If the message is too long to pass atomically through the underlying protocol, the error EMSGSIZE is returned, and the message is not transmitted.
可以看看TCP之深入淺出send和recv中的抓包實驗,可知待發送數據大于socket的內核緩沖區大小時,也是可以發送的,沒有返回SOCKET_ERROR。
也可以看看這里socket之send與發送緩沖區大小的關系
可知,待發送數據的長度大于s的內核發送緩沖區的長度時,會先將s(發送端)的內核發送緩沖區填滿,然后發送端會將內核發送緩沖區的數據發送到接收端socket的內核接收緩沖區,所以s(發送端)的內核發送緩沖區又會慢慢騰出空間,send又會將待發送數據往s(發送端)的內核發送緩沖區中copy,
極端情況就是s(發送端)的內核發送緩沖區填滿,接收端socket的內核接收緩沖區也被填滿,但是send待發送的數據還是沒發完,此時會堵塞。。。,等待s(發送端)的內核發送緩沖區產生空閑內存,
3.如果內核發送緩沖區可用空間大于待發送的數據長度len,send()函數直接將待發送數據完全拷貝到內核的發送緩沖區,然后成功返回。
要注意send()函數把待發送數據完全拷貝到s的內核發送緩沖區中之后,它就返回了,但是此時這些數據并不一定馬上被傳到連接的另一端。
注意:在Unix系統下,如果send在等待協議傳送數據時網絡斷開的話,調用send的進程會接收到一個SIGPIPE信號,進程對該信號的默認處理是進程終止。
Send函數的返回值有三類:
(1)返回值=0:
(2)返回值<0:發送失敗,錯誤原因存于全局變量errno中
(3)返回值>0:表示發送的字節數(實際上是拷貝到發送緩沖中的字節數)
錯誤代碼:
EBADF 參數s 非合法的socket處理代碼。
EFAULT 參數中有一指針指向無法存取的內存空間
ENOTSOCK 參數s為一文件描述詞,非socket。
EINTR 被信號所中斷。
EAGAIN 此操作會令進程阻斷,但參數s的socket為不可阻斷。
ENOBUFS 系統的緩沖內存不足
ENOMEM 核心內存不足
EINVAL 傳給系統調用的參數不正確。
recv函數
recv函數把內核接收緩沖中的數據copy到buf
int recv( SOCKET s,char FAR *buf,int len, int flags);具體的看看https://linux.die.net/man/2/recv
有一段話摘錄在此
If no messages are available at the socket, the receive calls wait for a message to arrive, unless the socket is nonblocking (see fcntl(2)), in which case the value -1 is returned and the external variable errno is set to EAGAIN or EWOULDBLOCK.
如果內核接收緩沖區內沒有數據可讀,則recv會堵塞,直到有數據到達,然后返回;
如果內核接收緩沖區內沒有數據可讀,但是recv設置為非堵塞,那么recv會返回-1,同時將errno置為EAGAIN or EWOULDBLOCK.
The receive calls normally return any data available, up to the requested amount, rather than waiting for receipt of the full amount requested.
recv返回值為讀取到的字節數(這個數是內核接收緩沖區中可讀取的字節數,可能是1個字節或者某個字節),最大可為buf區的大小len。recv并不是要等到讀取完len個字節才返回。
posix 系統上,
在阻塞模式下
如果內核緩沖區沒有數據可讀, recv ()會阻塞,直到有一些數據存在可以讀取為止。然后,它將返回讀取到的數據(可能少于請求的數量len) ,返回的數最大為len。
在非阻塞模式下
如果內核緩沖區沒有數據可讀,recv 將立即返回 -1,設置 errno 為 EAGAIN 或 ewoudblock。
所以,通常在循環中調用 recv,直到得到所需的量,同時檢查返回碼是0(另一端斷開)還是 -1(一些錯誤)。
在看看《UNIX網絡編程卷1》第 16 章 非阻塞式 I/O中的描述,如下
該函數的:
第一個參數指定接收端套接字描述符;
第二個參數指明一個緩沖區,該緩沖區用來存放recv函數接收到的數據;
第三個參數指明buf的長度;
第四個參數一般置0。
堵塞socket的recv函數的執行流程:
來自https://baike.baidu.com/item/recv%28%29
下面的描述正確性還不確定,反正網上都是這么說的,暫且看看吧
當應用程序調用recv函數時,recv先等待s的發送緩沖 中的數據被協議傳送完畢,如果協議在傳送s的發送緩沖中的數據時出現網絡錯誤,那么recv函數返回SOCKET_ERROR,
如果s的發送緩沖中沒有數 據或者數據被協議成功發送完畢后,**recv先檢查套接字s的接收緩沖區,如果s接收緩沖區中沒有數據或者協議正在接收數據,那么recv就一直等待,直到協議把數據接收完畢。 **
個人質疑:直到協議把數據接收完畢是什么意思?TCP是數據流,那什么叫做完畢呢?
假如,接收端套接字的接收緩沖區為16kB大小,發送端套接字一次發送8kB數據過來,肯定需要一點時間后,這8KB的數據才能完全到達接收端套接字的接收緩沖區,那接收端recv是一看到接收緩沖區有一點數據就返回?還是等接收緩沖區有8KB數據之后才返回?那如果發送端套接字發送完8kB數據后,立馬又發送8kB數據呢?
所以接收端套接字的recv根本不知道發送端什么時候才把數據發送完畢,因為發送端可以想發多少發多少
看看《UNIX網絡編程卷1》第 16 章 非阻塞式 I/O中的描述中的原話
可以知道正確的描述應該是:調用recv時,若該套接字的內核接收緩沖區沒有數據可讀,recv會堵塞(注意,這里前提是在堵塞模式下),直到有一些數據到達,這一些數據可能是單個字節,也可能是一個完整的TCP分節中的數據,recv就會被喚醒,然后返回copy到的字節數。
(注意協議接收到的數據可能大于buf的長度,所以 在這種情況下要調用幾次recv函數才能把s的接收緩沖中的數據copy完。recv函數僅僅是copy數據,真正的接收數據是協議來完成的)
recv函數返回其實際copy的字節數。如果recv在copy時出錯,那么它返回SOCKET_ERROR;如果recv函數在等待協議接收數據時網絡中斷了,那么它返回0。
注意:在Unix系統下,如果recv函數在等待協議接收數據時網絡斷開了,那么調用recv的進程會接收到一個SIGPIPE信號,進程對該信號的默認處理是進程終止。
- 默認情況下socket是阻塞的。
阻塞與非阻塞recv返回值沒有區別,都是:
<0 出錯
=0 對方調用了close API來關閉連接
> 0 接收到的數據大小,
特別地:返回值<0時并且(errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)的情況下認為連接是正常的,繼續接收。
只是阻塞模式下recv會一直阻塞直到接收到數據,非阻塞模式下如果沒有數據就會返回,不會阻塞著讀,因此需要循環讀取)。
返回說明:
(1)成功執行時,返回接收到的字節數。
(2)若另一端已關閉連接則返回0,這種關閉是對方主動且正常的關閉
(3)失敗返回-1,errno被設為以下的某個值
EAGAIN:套接字已標記為非阻塞,而接收操作被阻塞或者接收超時
EBADF:sock不是有效的描述詞
ECONNREFUSE:遠程主機阻絕網絡連接
EFAULT:內存空間訪問出錯
EINTR:操作被信號中斷
EINVAL:參數無效
ENOMEM:內存不足
ENOTCONN:與面向連接關聯的套接字尚未被連接上
ENOTSOCK:sock索引的不是套接字
總結
以上是生活随笔為你收集整理的recv/send堵塞和非堵塞的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: sql trim函数_SQL TRIM函
- 下一篇: 情不知所起,一 网 而深