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