动画图解 socket 缓冲区的那些事儿
先上這篇文章的目錄。
目錄代碼執行send成功后,數據就發出去了嗎?
回答這個問題之前,需要了解什么是Socket 緩沖區。
Socket 緩沖區
什么是 socket 緩沖區
編程的時候,如果要跟某個IP建立連接,我們需要調用操作系統提供的 socket API。
socket 在操作系統層面,可以理解為一個文件。
我們可以對這個文件進行一些方法操作。
用listen方法,可以讓程序作為服務器監聽其他客戶端的連接。
用connect,可以作為客戶端連接服務器。
用send或write可以發送數據,recv或read可以接收數據。
在建立好連接之后,這個 socket 文件就像是遠端機器的 "代理人" 一樣。比如,如果我們想給遠端服務發點什么東西,那就只需要對這個文件執行寫操作就行了。
socket_api那寫到了這個文件之后,剩下的發送工作自然就是由操作系統內核來完成了。
既然是寫給操作系統,那操作系統就需要提供一個地方給用戶寫。同理,接收消息也是一樣。
這個地方就是 socket 緩沖區。
用戶發送消息的時候寫給 send buffer(發送緩沖區)
用戶接收消息的時候寫給 recv buffer(接收緩沖區)
也就是說一個socket ,會帶有兩個緩沖區,一個用于發送,一個用于接收。因為這是個先進先出的結構,有時候也叫它們發送、接收隊列。
一個socket有兩個緩沖區怎么觀察 socket 緩沖區
如果想要查看 socket 緩沖區,可以在linux環境下執行 netstat -nt 命令。
#?netstat?-nt Active?Internet?connections?(w/o?servers) Proto?Recv-Q?Send-Q?Local?Address???????????Foreign?Address?????????State?????? tcp????????0?????60?172.22.66.69:22?????????122.14.220.252:59889????ESTABLISHED這上面表明了,這里有一個協議(Proto)類型為 TCP 的連接,同時還有本地(Local Address)和遠端(Foreign Address)的IP信息,狀態(State)是已連接。
還有Send-Q 是發送緩沖區,下面的數字60是指,當前還有60 Byte在發送緩沖區中未發送。而 Recv-Q 代表接收緩沖區,此時是空的,數據都被應用進程接收干凈了。
TCP部分
我們在使用TCP建立連接之后,一般會使用 send 發送數據。
上面是一段偽代碼,僅用于展示大概邏輯,我們在建立好連接后,一般會在代碼中執行 send 方法。那么此時,消息就會被立刻發到對端機器嗎?
執行 send 發送的字節,會立馬發送嗎?
答案是不確定!執行 send 之后,數據只是拷貝到了socket 緩沖區。至 于什么時候會發數據,發多少數據,全聽操作系統安排。
tcp_sendmsg 邏輯在用戶進程中,程序通過操作 socket 會從用戶態進入內核態,而 send方法會將數據一路傳到傳輸層。在識別到是 TCP協議后,會調用 tcp_sendmsg 方法。
//?net/ipv4/tcp.c //?以下省略了大量邏輯 int?tcp_sendmsg() {??//?如果還有可以放數據的空間if?(skb_availroom(skb)?>?0)?{//?嘗試拷貝待發送數據到發送緩沖區err?=?skb_add_data_nocache(sk,?skb,?from,?copy);}??//?下面是嘗試發送的邏輯代碼,先省略????? }在 tcp_sendmsg 中, 核心工作就是將待發送的數據組織按照先后順序放入到發送緩沖區中, 然后根據實際情況(比如擁塞窗口等)判斷是否要發數據。如果不發送數據,那么此時直接返回。
如果緩沖區滿了會怎么辦
前面提到的情況里是,發送緩沖區有足夠的空間,可以用于拷貝待發送數據。
如果發送緩沖區空間不足,或者滿了,執行發送,會怎么樣?
這里分兩種情況。
首先,socket在創建的時候,是可以設置是阻塞的還是非阻塞的。
int?s?=?socket(AF_INET,?SOCK_STREAM?|?SOCK_NONBLOCK,?IPPROTO_TCP);比如通過上面的代碼,就可以將 socket 設置為非阻塞 (SOCK_NONBLOCK)。
當發送緩沖區滿了,如果還向socket執行send
如果此時 socket 是阻塞的,那么程序會在那干等、死等,直到釋放出新的緩存空間,就繼續把數據拷進去,然后返回。
如果此時 socket 是非阻塞的,程序就會立刻返回一個 EAGAIN 錯誤信息,意思是 ?Try again , 現在緩沖區滿了,你也別等了,待會再試一次。
我們可以簡單看下源碼是怎么實現的。還是回到剛才的 tcp_sendmsg 發送方法中。
int?tcp_sendmsg() {??if?(skb_availroom(skb)?>?0)?{//?..如果有足夠緩沖區就執行balabla}?else?{//?如果發送緩沖區沒空間了,那就等到有空間,至于等的方式,分阻塞和非阻塞if?((err?=?sk_stream_wait_memory(sk,?&timeo))?!=?0)goto?do_error;}??? }????????里面提到的 ?sk_stream_wait_memory 會根據socket是否阻塞來決定是一直等等一會就返回。
int?sk_stream_wait_memory(struct?sock?*sk,?long?*timeo_p) {while?(1)?{//?非阻塞模式時,會等到超時返回?EAGAINif?(等待超時))return?-EAGAIN;?????//?阻塞等待時,會等到發送緩沖區有足夠的空間了,才跳出if?(sk_stream_memory_free(sk)?&&?!vm_wait)break;}return?err; }如果接收緩沖區為空,執行 recv 會怎么樣?
接收緩沖區也是類似的情況。
當接收緩沖區為空,如果還向socket執行 recv
如果此時 socket 是阻塞的,那么程序會在那干等,直到接收緩沖區有數據,就會把數據從接收緩沖區拷貝到用戶緩沖區,然后返回。
如果此時 socket 是非阻塞的,程序就會立刻返回一個 EAGAIN 錯誤信息。
下面用一張圖匯總一下,方便大家保存面試的時候用哈哈哈。
socket讀寫緩沖區滿了的情況匯總如果socket緩沖區還有數據,執行close了,會怎么樣?
首先我們要知道,一般正常情況下,發送緩沖區和接收緩沖區 都應該是空的。
如果發送、接收緩沖區長時間非空,說明有數據堆積,這往往是由于一些網絡問題或用戶應用層問題,導致數據沒有正常處理。
那么正常情況下,如果 socket 緩沖區為空,執行 close。就會觸發四次揮手。
TCP四次揮手這個也是面試老八股文內容了,這里我們只需要關注第一次揮手,發的是 FIN 就夠了。
如果接收緩沖區有數據時,執行close了,會怎么樣?
socket close 時,主要的邏輯在 tcp_close() 里實現。
先說結論,關閉過程主要有兩種情況:
如果接收緩沖區還有數據未讀,會先把接收緩沖區的數據清空,然后給對端發一個RST。
如果接收緩沖區是空的,那么就調用 tcp_send_fin() 開始進行四次揮手過程的第一次揮手。
如果發送緩沖區有數據時,執行close了,會怎么樣?
以前以為在這種情況下內核會把發送緩沖區數據清空,然后四次揮手。
但是發現源碼并不是這樣的。
void?tcp_send_fin(struct?sock?*sk) {//?獲得發送緩沖區的最后一塊數據struct?sk_buff?*skb,?*tskb?=?tcp_write_queue_tail(sk);struct?tcp_sock?*tp?=?tcp_sk(sk);//?如果發送緩沖區還有數據if?(tskb?&&?(tcp_send_head(sk)?||?sk_under_memory_pressure(sk)))?{TCP_SKB_CB(tskb)->tcp_flags?|=?TCPHDR_FIN;?//?把最后一塊數據值為?FIN?TCP_SKB_CB(tskb)->end_seq++;tp->write_seq++;}??else?{//?發送緩沖區沒有數據,就造一個FIN包}//?發送數據__tcp_push_pending_frames(sk,?tcp_current_mss(sk),?TCP_NAGLE_OFF); }此時,還有些數據沒發出去,內核會把發送緩沖區最后一個數據塊拿出來。然后置為 FIN。
socket 緩沖區是個先進先出的隊列,這種情況是指內核會等待TCP層安靜把發送緩沖區數據都發完,最后再執行四次揮手的第一次揮手(FIN包)。
有一點需要注意的是,只有在接收緩沖區為空的前提下,我們才有可能走到 tcp_send_fin() 。而只有在進入了這個方法之后,我們才有可能考慮發送緩沖區是否為空的場景。
sendbuf非空UDP部分
UDP也有緩沖區嗎
說完TCP了,我們聊聊UDP。這對好基友,同時都是傳輸層里的重要協議。既然前面提到TCP有發送、接收緩沖區,那UDP有嗎?
以前我以為:
"每個UDP socket都有一個接收緩沖區,沒有發送緩沖區,從概念上來說就是只要有數據就發,不管對方是否可以正確接收,所以不緩沖,不需要發送緩沖區。"
后來我發現我錯了。
UDP socket 也是 socket,一個socket 就是會有收和發兩個緩沖區,跟用什么協議關系不大。
有沒有是一回事,用不用又是一回事。
UDP不用發送緩沖區?
事實上,UDP不僅有發送緩沖區,也用發送緩沖區。
一般正常情況下,會把數據直接拷到發送緩沖區后直接發送。
還有一種情況,是在發送數據的時候,設置一個 MSG_MORE 的標記。
ssize_t?send(int?sock,?const?void?*buf,?size_t?len,?int?flags);?//?flag?置為?MSG_MORE大概的意思是告訴內核,待會還有其他更多消息要一起發,先別著急發出去。此時內核就會把這份數據先用發送緩沖區緩存起來,待會應用層說ok了,再一起發。
我們可以看下源碼。
int?udp_sendmsg() {// corkreq 為 true 表示是 MSG_MORE 的方式,僅僅組織報文,不發送;int?corkreq = up->corkflag || msg->msg_flags&MSG_MORE;//??將要發送的數據,按照MTU大小分割,每個片段一個skb;并且這些//? skb會放入到套接字的發送緩沖區中;該函數只是組織數據包,并不執行發送動作。err?=?ip_append_data(sk,?fl4,?getfrag,?msg->msg_iov,?ulen,sizeof(struct?udphdr),?&ipc,?&rt,corkreq???msg->msg_flags|MSG_MORE?:?msg->msg_flags);//?沒有啟用 MSG_MORE 特性,那么直接將發送隊列中的數據發送給IP。?if?(!corkreq)err?=?udp_push_pending_frames(sk);}因此,不管是不是 MSG_MORE, IP都會先把數據放到發送隊列中,然后根據實際情況再考慮是不是立刻發送。
而我們大部分情況下,都不會用 ?MSG_MORE,也就是來一個數據包就直接發一個數據包。從這個行為上來說,雖然UDP用上了發送緩沖區,但實際上并沒有起到"緩沖"的作用。
最后
這篇文章,我也就寫了20個小時吧。畫圖也就畫吐了而已,每天早上7點鐘爬起來寫一個多小時再去上班。
歡迎點贊、在看、關注【小白debug】
總結
以上是生活随笔為你收集整理的动画图解 socket 缓冲区的那些事儿的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 写 Go 时如何优雅地查文档
- 下一篇: 再见了 Docker!Go 落地的 K8