動畫圖解 socket 緩沖區(qū)的那些事兒
先上這篇文章的目錄。

代碼執(zhí)行send成功后,數(shù)據(jù)就發(fā)出去了嗎?
回答這個問題之前,需要了解什么是Socket 緩沖區(qū)。
Socket 緩沖區(qū)
什么是 socket 緩沖區(qū)
編程的時候,如果要跟某個IP建立連接,我們需要調(diào)用操作系統(tǒng)提供的 socket API。
socket 在操作系統(tǒng)層面,可以理解為一個文件。
我們可以對這個文件進行一些方法操作。
用listen方法,可以讓程序作為服務(wù)器監(jiān)聽其他客戶端的連接。
用connect,可以作為客戶端連接服務(wù)器。
用send或write可以發(fā)送數(shù)據(jù),recv或read可以接收數(shù)據(jù)。
在建立好連接之后,這個 socket 文件就像是遠(yuǎn)端機器的 "代理人" 一樣。比如,如果我們想給遠(yuǎn)端服務(wù)發(fā)點什么東西,那就只需要對這個文件執(zhí)行寫操作就行了。

那寫到了這個文件之后,剩下的發(fā)送工作自然就是由操作系統(tǒng)內(nèi)核來完成了。
既然是寫給操作系統(tǒng),那操作系統(tǒng)就需要提供一個地方給用戶寫。同理,接收消息也是一樣。
這個地方就是 socket 緩沖區(qū)。
用戶發(fā)送消息的時候?qū)懡o send buffer(發(fā)送緩沖區(qū))
用戶接收消息的時候?qū)懡o recv buffer(接收緩沖區(qū))
也就是說一個socket ,會帶有兩個緩沖區(qū),一個用于發(fā)送,一個用于接收。因為這是個先進先出的結(jié)構(gòu),有時候也叫它們發(fā)送、接收隊列。

怎么觀察 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
這上面表明了,這里有一個協(xié)議(Proto)類型為 TCP 的連接,同時還有本地(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ù)據(jù)都被應(yīng)用進程接收干凈了。
TCP部分
我們在使用TCP建立連接之后,一般會使用 send 發(fā)送數(shù)據(jù)。
int main(int argc, char *argv[])
{
// 創(chuàng)建socket
sockfd=socket(AF_INET,SOCK_STREAM, 0))
// 建立連接
connect(sockfd, 服務(wù)器ip信息, sizeof(server))
// 執(zhí)行 send 發(fā)送消息
send(sockfd,str,sizeof(str),0))
// 關(guān)閉 socket
close(sockfd);
return 0;
}
上面是一段偽代碼,僅用于展示大概邏輯,我們在建立好連接后,一般會在代碼中執(zhí)行 send 方法。那么此時,消息就會被立刻發(fā)到對端機器嗎?
執(zhí)行 send 發(fā)送的字節(jié),會立馬發(fā)送嗎?
答案是不確定!執(zhí)行 send 之后,數(shù)據(jù)只是拷貝到了socket 緩沖區(qū)。至 于什么時候會發(fā)數(shù)據(jù),發(fā)多少數(shù)據(jù),全聽操作系統(tǒng)安排。

在用戶進程中,程序通過操作 socket 會從用戶態(tài)進入內(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ù)實際情況(比如擁塞窗口等)判斷是否要發(fā)數(shù)據(jù)。如果不發(fā)送數(shù)據(jù),那么此時直接返回。
如果緩沖區(qū)滿了會怎么辦
前面提到的情況里是,發(fā)送緩沖區(qū)有足夠的空間,可以用于拷貝待發(fā)送數(shù)據(jù)。
如果發(fā)送緩沖區(qū)空間不足,或者滿了,執(zhí)行發(fā)送,會怎么樣?
這里分兩種情況。
首先,socket在創(chuàng)建的時候,是可以設(shè)置是阻塞的還是非阻塞的。
int s = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);
比如通過上面的代碼,就可以將 socket 設(shè)置為非阻塞 (SOCK_NONBLOCK)。
當(dāng)發(fā)送緩沖區(qū)滿了,如果還向socket執(zhí)行send
如果此時 socket 是阻塞的,那么程序會在那干等、死等,直到釋放出新的緩存空間,就繼續(xù)把數(shù)據(jù)拷進去,然后返回。

如果此時 socket 是非阻塞的,程序就會立刻返回一個
EAGAIN錯誤信息,意思是Try again, 現(xiàn)在緩沖區(qū)滿了,你也別等了,待會再試一次。

我們可以簡單看下源碼是怎么實現(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) {
// 非阻塞模式時,會等到超時返回 EAGAIN
if (等待超時))
return -EAGAIN;
// 阻塞等待時,會等到發(fā)送緩沖區(qū)有足夠的空間了,才跳出
if (sk_stream_memory_free(sk) && !vm_wait)
break;
}
return err;
}
如果接收緩沖區(qū)為空,執(zhí)行 recv 會怎么樣?
接收緩沖區(qū)也是類似的情況。
當(dāng)接收緩沖區(qū)為空,如果還向socket執(zhí)行 recv
如果此時 socket 是阻塞的,那么程序會在那干等,直到接收緩沖區(qū)有數(shù)據(jù),就會把數(shù)據(jù)從接收緩沖區(qū)拷貝到用戶緩沖區(qū),然后返回。

如果此時 socket 是非阻塞的,程序就會立刻返回一個
EAGAIN錯誤信息。

下面用一張圖匯總一下,方便大家保存面試的時候用哈哈哈。

如果socket緩沖區(qū)還有數(shù)據(jù),執(zhí)行close了,會怎么樣?
首先我們要知道,一般正常情況下,發(fā)送緩沖區(qū)和接收緩沖區(qū) 都應(yīng)該是空的。
如果發(fā)送、接收緩沖區(qū)長時間非空,說明有數(shù)據(jù)堆積,這往往是由于一些網(wǎng)絡(luò)問題或用戶應(yīng)用層問題,導(dǎo)致數(shù)據(jù)沒有正常處理。
那么正常情況下,如果 socket 緩沖區(qū)為空,執(zhí)行 close。就會觸發(fā)四次揮手。

這個也是面試?yán)习斯晌膬?nèi)容了,這里我們只需要關(guān)注第一次揮手,發(fā)的是 FIN 就夠了。
如果接收緩沖區(qū)有數(shù)據(jù)時,執(zhí)行close了,會怎么樣?
socket close 時,主要的邏輯在 tcp_close() 里實現(xiàn)。
先說結(jié)論,關(guān)閉過程主要有兩種情況:
如果接收緩沖區(qū)還有數(shù)據(jù)未讀,會先把接收緩沖區(qū)的數(shù)據(jù)清空,然后給對端發(fā)一個RST。
如果接收緩沖區(qū)是空的,那么就調(diào)用
tcp_send_fin()開始進行四次揮手過程的第一次揮手。
void tcp_close(struct sock *sk, long timeout)
{
// 如果接收緩沖區(qū)有數(shù)據(jù),那么清空數(shù)據(jù)
while ((skb = __skb_dequeue(&sk->sk_receive_queue)) != NULL) {
u32 len = TCP_SKB_CB(skb)->end_seq - TCP_SKB_CB(skb)->seq -
tcp_hdr(skb)->fin;
data_was_unread += len;
__kfree_skb(skb);
}
if (data_was_unread) {
// 如果接收緩沖區(qū)的數(shù)據(jù)被清空了,發(fā) RST
tcp_send_active_reset(sk, sk->sk_allocation);
} else if (tcp_close_state(sk)) {
// 正常四次揮手, 發(fā) FIN
tcp_send_fin(sk);
}
// 等待關(guān)閉
sk_stream_wait_close(sk, timeout);
}

如果發(fā)送緩沖區(qū)有數(shù)據(jù)時,執(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ù),就造一個FIN包
}
// 發(fā)送數(shù)據(jù)
__tcp_push_pending_frames(sk, tcp_current_mss(sk), TCP_NAGLE_OFF);
}
此時,還有些數(shù)據(jù)沒發(fā)出去,內(nèi)核會把發(fā)送緩沖區(qū)最后一個數(shù)據(jù)塊拿出來。然后置為 FIN。
socket 緩沖區(qū)是個先進先出的隊列,這種情況是指內(nèi)核會等待TCP層安靜把發(fā)送緩沖區(qū)數(shù)據(jù)都發(fā)完,最后再執(zhí)行四次揮手的第一次揮手(FIN包)。
有一點需要注意的是,只有在接收緩沖區(qū)為空的前提下,我們才有可能走到 tcp_send_fin() 。而只有在進入了這個方法之后,我們才有可能考慮發(fā)送緩沖區(qū)是否為空的場景。

UDP部分
UDP也有緩沖區(qū)嗎
說完TCP了,我們聊聊UDP。這對好基友,同時都是傳輸層里的重要協(xié)議。既然前面提到TCP有發(fā)送、接收緩沖區(qū),那UDP有嗎?
以前我以為:
"每個UDP socket都有一個接收緩沖區(qū),沒有發(fā)送緩沖區(qū),從概念上來說就是只要有數(shù)據(jù)就發(fā),不管對方是否可以正確接收,所以不緩沖,不需要發(fā)送緩沖區(qū)。"
后來我發(fā)現(xiàn)我錯了。
UDP socket 也是 socket,一個socket 就是會有收和發(fā)兩個緩沖區(qū),跟用什么協(xié)議關(guān)系不大。
有沒有是一回事,用不用又是一回事。
UDP不用發(fā)送緩沖區(qū)?
事實上,UDP不僅有發(fā)送緩沖區(qū),也用發(fā)送緩沖區(qū)。
一般正常情況下,會把數(shù)據(jù)直接拷到發(fā)送緩沖區(qū)后直接發(fā)送。
還有一種情況,是在發(fā)送數(shù)據(jù)的時候,設(shè)置一個 MSG_MORE 的標(biāo)記。
ssize_t send(int sock, const void *buf, size_t len, int flags); // flag 置為 MSG_MORE
大概的意思是告訴內(nèi)核,待會還有其他更多消息要一起發(fā),先別著急發(fā)出去。此時內(nèi)核就會把這份數(shù)據(jù)先用發(fā)送緩沖區(qū)緩存起來,待會應(yīng)用層說ok了,再一起發(fā)。
我們可以看下源碼。
int udp_sendmsg()
{
// corkreq 為 true 表示是 MSG_MORE 的方式,僅僅組織報文,不發(fā)送;
int corkreq = up->corkflag || msg->msg_flags&MSG_MORE;
// 將要發(fā)送的數(shù)據(jù),按照MTU大小分割,每個片段一個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ā)送隊列中的數(shù)據(jù)發(fā)送給IP。
if (!corkreq)
err = udp_push_pending_frames(sk);
}
因此,不管是不是 MSG_MORE, IP都會先把數(shù)據(jù)放到發(fā)送隊列中,然后根據(jù)實際情況再考慮是不是立刻發(fā)送。
而我們大部分情況下,都不會用 MSG_MORE,也就是來一個數(shù)據(jù)包就直接發(fā)一個數(shù)據(jù)包。從這個行為上來說,雖然UDP用上了發(fā)送緩沖區(qū),但實際上并沒有起到"緩沖"的作用。
最后
這篇文章,我也就寫了20個小時吧。畫圖也就畫吐了而已,每天早上7點鐘爬起來寫一個多小時再去上班。
歡迎點贊、在看、關(guān)注【小白debug】
