博客 / 詳情

返回

可靠傳輸的TCP協議send成功就意味着數據一定發出去了?

本文來自小白debug的原創分享,原題“【修正版】動圖圖解!代碼執行send成功後,數據就發出去了嗎?”,下文有修訂和排版優化。

1、引言
回覆過很多IM初學者關於MobileIMSDK  通信層代碼的疑問,最基礎的問題就是“明明用的是TCP協議,而TCP協議也被稱為可靠的通信協議,那為什麼TCP代碼中明確能知道數據是否發送成功,為什麼仍然需要應用層去實現消息應答和重傳這種邏輯?”。要真正講清楚這個問題,還真不是三言兩語能講的明白。。。本篇文章我們以TCP協議的網絡編程邏輯,從Socket緩衝區的角度去拆解,為什麼號稱可靠傳輸的TCP協議,在代碼中調用send併成功發出數據,並不意味着這個數據就一定通過物理網絡發出去了。
圖片
  技術交流:

  • 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM》
  • 開源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK(備用地址點此)
    (本文已同步發佈於:http://www.52im.net/thread-4868-1-1.html)

2、系列文章
本文是系列文章中的第 21篇,大綱如下:

《不為人知的網絡編程(一):淺析TCP協議中的疑難雜症(上篇)》

《不為人知的網絡編程(二):淺析TCP協議中的疑難雜症(下篇)》

《不為人知的網絡編程(三):關閉TCP連接時為什麼會TIME_WAIT、CLOSE_WAIT》

《不為人知的網絡編程(四):深入研究分析TCP的異常關閉》

《不為人知的網絡編程(五):UDP的連接性和負載均衡》

《不為人知的網絡編程(六):深入地理解UDP協議並用好它》

《不為人知的網絡編程(七):如何讓不可靠的UDP變的可靠?》

《不為人知的網絡編程(八):從數據傳輸層深度解密HTTP》

《不為人知的網絡編程(九):理論聯繫實際,全方位深入理解DNS》

《不為人知的網絡編程(十):深入操作系統,從內核理解網絡包的接收過程(Linux篇)》

《不為人知的網絡編程(十一):從底層入手,深度分析TCP連接耗時的秘密》

《不為人知的網絡編程(十二):徹底搞懂TCP協議層的KeepAlive保活機制》

《不為人知的網絡編程(十三):深入操作系統,徹底搞懂127.0.0.1本機網絡通信》

《不為人知的網絡編程(十四):拔掉網線再插上,TCP連接還在嗎?一文即懂!》

《不為人知的網絡編程(十五):深入操作系統,一文搞懂Socket到底是什麼》

《不為人知的網絡編程(十六):深入分析與解決TCP的RST經典異常問題》

《不為人知的網絡編程(十七):冰山之下,一次網絡請求背後的技術秘密》

《不為人知的網絡編程(十八):UDP比TCP高效?還真不一定!》

《不為人知的網絡編程(十九):能Ping通,TCP就一定能連接和通信嗎?》

《不為人知的網絡編程(二十):網絡ping不通到底有多少原因?一文搞明白!》

《不為人知的網絡編程(二十一):可靠傳輸的TCP協議send成功就意味着數據一定發出去了?》(☜ 本文)

3、什麼是 socket 緩衝區
編程的時候,如果要跟某個IP建立連接,我們需要調用操作系統提供的 socket API。socket 在操作系統層面,可以理解為一個文件。

我們可以對這個文件進行一些方法操作:
1)用listen方法:可以讓程序作為服務器監聽其他客户端的連接;
2)用connect:可以作為客户端連接服務器;
3)用send或write:可以發送數據,recv或read可以接收數據。
在建立好連接之後,這個 socket 文件就像是遠端機器的 "代理人" 一樣。比如,如果我們想給遠端服務發點什麼東西,那就只需要對這個文件執行寫操作就行了。

圖片
那寫到了這個文件之後,剩下的發送工作自然就是由操作系統內核來完成了。既然是寫給操作系統,那操作系統就需要提供一個地方給用户寫。同理,接收消息也是一樣。
這個地方就是 socket 緩衝區:
1)用户發送消息的時候寫給 send buffer(發送緩衝區);
2)用户接收消息的時候寫給 recv buffer(接收緩衝區)。

也就是説:一個socket 會帶有兩個緩衝區,一個用於發送,一個用於接收(如下圖所示)。因為這是個先進先出的結構,有時候也叫它們發送、接收隊列。
圖片

4、怎麼觀察 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 代表接收緩衝區, 此時是空的,數據都被應用進程接收乾淨了。

5、執行 send 發送的字節,會立馬發送嗎?
我們在使用TCP建立連接之後,一般會使用 send 發送數據:

int main(int argc, char *argv[])

{

// 創建socket

sockfd=socket(AF_INET,SOCK_STREAM, 0))



// 建立連接 

connect(sockfd, 服務器ip信息, sizeof(server)) 



// 執行 send 發送消息

send(sockfd,str,sizeof(str),0)) 



// 關閉 socket

close(sockfd);



return 0;

}

上面是一段偽代碼,僅用於展示大概邏輯,我們在建立好連接後,一般會在代碼中執行 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 中, 核心工作就是將待發送的數據組織按照先後順序放入到發送緩衝區中, 然後根據實際情況(比如擁塞窗口等)判斷是否要發數據。如果不發送數據,那麼此時直接返回。

6、如果Socket緩衝區滿了會怎麼辦
前面提到的情況裏是,發送緩衝區有足夠的空間,可以用於拷貝待發送數據。

6.1 如果發送緩衝區空間不足,或者滿了,執行發送,會怎麼樣?
這裏分兩種情況。

首先:socket在創建的時候,是可以設置是阻塞的還是非阻塞的。

int s = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);

比如通過上面的代碼,就可以將 socket 設置為非阻塞 (SOCK_NONBLOCK)。

當發送緩衝區滿了,如果還向socket執行send。。。。

1)如果此時 socket 是阻塞的,那麼程序會在那乾等、死等,直到釋放出新的緩存空間,就繼續把數據拷進去,然後返回。

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

recv非阻塞:
圖片
下面用一張圖彙總一下,方便大家保存面試的時候用哈哈哈。

socket讀寫緩衝區滿了的情況彙總:
圖片

7、如果Socket緩衝區滿了會怎麼辦?
7.1概述
首先我們要知道,一般正常情況下,發送緩衝區和接收緩衝區都應該是空的。如果發送、接收緩衝區長時間非空,説明有數據堆積,這往往是由於一些網絡問題或用户應用層問題,導致數據沒有正常處理。

那麼正常情況下,如果 socket 緩衝區為空,執行 close。就會觸發四次揮手。

TCP四次揮手:

圖片
這個也是面試老八股文內容了,這裏我們只需要關注第一次揮手,發的是 FIN 就夠了。相關文章可以進一步閲讀:《理論經典:TCP協議的3次握手與4次揮手過程詳解》《腦殘式網絡編程入門(一):跟着動畫來學TCP三次握手和四次揮手》7.2 如果接收緩衝區有數據時,執行close了,會怎麼樣?

socket close 時,主要的邏輯在 tcp_close() 裏實現。

先説結論,關閉過程主要有兩種情況:

1)如果接收緩衝區還有數據未讀,會先把接收緩衝區的數據清空,然後給對端發一個RST;

2)如果接收緩衝區是空的,那麼就調用 tcp_send_fin() 開始進行四次揮手過程的第一次揮手。

void tcp_close(struct sock *sk, long timeout)

{

// 如果接收緩衝區有數據,那麼清空數據

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) {

// 如果接收緩衝區的數據被清空了,發 RST

    tcp_send_active_reset(sk, sk->sk_allocation);

 } else if (tcp_close_state(sk)) {

// 正常四次揮手, 發 FIN

    tcp_send_fin(sk);

}

// 等待關閉

sk_stream_wait_close(sk, timeout);

}

recvbuf非空:
圖片

7.3 如果發送緩衝區有數據時,執行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() 。而只有在進入了這個方法之後,我們才有可能考慮發送緩衝區是否為空的場景。
圖片

8、拓展閲讀:UDP有緩衝區嗎?
8.1 UDP也有緩衝區嗎
説完TCP了,我們聊聊UDP。這對好基友,同時都是傳輸層裏的重要協議。既然前面提到TCP有發送、接收緩衝區,那UDP有嗎?

以前我以為:

"每個UDP socket都有一個接收緩衝區,沒有發送緩衝區,從概念上來説就是隻要有數據就發,不管對方是否可以正確接收,所以不緩衝,不需要發送緩衝區。"

後來我發現我錯了:UDP socket 也是 socket,一個socket 就是會有收和發兩個緩衝區。跟用什麼協議關係不大。

有沒有是一回事,用不用又是一回事。

8.2 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用上了發送緩衝區,但實際上並沒有起到"緩衝"的作用。
9、參考資料
[1] TCP/IP詳解 - 第21章·TCP的超時與重傳

[2] 快速理解TCP協議一篇就夠

[3] 假如你來設計TCP協議,會怎麼做?

[4] 手把手教你寫基於TCP的Socket長連接

[5] 到底什麼是Socket?一文即懂!

[6] 我們在讀寫Socket時,究竟在讀寫什麼?

[7] 拔掉網線再插上,TCP連接還在嗎?一文即懂!

[8] 深入操作系統,一文搞懂Socket到底是什麼

[9] 為何基於TCP協議的移動端IM仍然需要心跳保活機制?

[10] 從客户端的角度來談談移動端IM的消息可靠性和送達機制

(本文已同步發佈於:http://www.52im.net/thread-4868-1-1.html)

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.