<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          動畫圖解 socket 緩沖區(qū)的那些事兒

          共 9318字,需瀏覽 19分鐘

           ·

          2021-09-17 04:39

          先上這篇文章的目錄。

          目錄

          代碼執(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ù)器。

          sendwrite可以發(fā)送數(shù)據(jù),recvread可以接收數(shù)據(jù)。

          在建立好連接之后,這個 socket 文件就像是遠(yuǎn)端機器的 "代理人" 一樣。比如,如果我們想給遠(yuǎn)端服務(wù)發(fā)點什么東西,那就只需要對這個文件執(zhí)行寫操作就行了。

          socket_api

          那寫到了這個文件之后,剩下的發(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ū)

          如果想要查看 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)安排。

          tcp_sendmsg 邏輯

          在用戶進程中,程序通過操作 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ù)拷進去,然后返回

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

          send非阻塞

          我們可以簡單看下源碼是怎么實現(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ū),然后返回。

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

          recv非阻塞

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

          socket讀寫緩沖區(qū)滿了的情況匯總


          如果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ā)四次揮手。

          TCP四次揮手

          這個也是面試?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);
          }
          recvbuf非空


          如果發(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ū)是否為空的場景。

          sendbuf非空


          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】

          瀏覽 271
          點贊
          評論
          1收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          1收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产精品九九视频 | 亚欧中文在线 | 日本免费色视频 | 亚洲AV毛片在线观看。。 | 成人三级电影久久 |