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

          阿里二面:沒(méi)有 accept,能建立 TCP 連接嗎?

          共 8140字,需瀏覽 17分鐘

           ·

          2022-08-08 20:07

          八股文網(wǎng)站:xiaolincoding.com

          大家好,我是小林。

          上周寫了一篇:字節(jié)三面:沒(méi)有 listen,能建立 TCP 連接嗎?

          這次,我們來(lái)討論一下,沒(méi)有 accept,能建立 TCP 連接嗎?

          正文

          下面這個(gè)動(dòng)圖,是我們平時(shí)客戶端和服務(wù)端建立連接時(shí)的代碼流程。

          握手建立連接流程

          對(duì)應(yīng)的是下面一段簡(jiǎn)化過(guò)的服務(wù)端偽代碼。

          int main()
          {
              /*Step 1: 創(chuàng)建服務(wù)器端監(jiān)聽socket描述符listen_fd*/    
              listen_fd = socket(AF_INET, SOCK_STREAM, 0);

              /*Step 2: bind綁定服務(wù)器端的IP和端口,所有客戶端都向這個(gè)IP和端口發(fā)送和請(qǐng)求數(shù)據(jù)*/    
              bind(listen_fd, xxx);

              /*Step 3: 服務(wù)端開啟監(jiān)聽*/    
              listen(listen_fd, 128);

              /*Step 4: 服務(wù)器等待客戶端的鏈接,返回值cfd為客戶端的socket描述符*/    
              cfd = accept(listen_fd, xxx);

                /*Step 5: 讀取客戶端發(fā)來(lái)的數(shù)據(jù)*/
                n = read(cfd, buf, sizeof(buf));
          }

          估計(jì)大家也是老熟悉這段偽代碼了。

          需要注意的是,在執(zhí)行listen()方法之后還會(huì)執(zhí)行一個(gè)accept()方法。

          一般情況下,如果啟動(dòng)服務(wù)器,會(huì)發(fā)現(xiàn)最后程序會(huì)阻塞在accept()里。

          此時(shí)服務(wù)端就算ok了,就等客戶端了。

          那么,再看下簡(jiǎn)化過(guò)的客戶端偽代碼。

          int main()
          {
              /*Step 1: 創(chuàng)建客戶端端socket描述符cfd*/    
              cfd = socket(AF_INET, SOCK_STREAM, 0);

              /*Step 2: connect方法,對(duì)服務(wù)器端的IP和端口號(hào)發(fā)起連接*/    
              ret = connect(cfd, xxxx);

              /*Step 4: 向服務(wù)器端寫數(shù)據(jù)*/
              write(cfd, buf, strlen(buf));
          }

          客戶端比較簡(jiǎn)單,創(chuàng)建好socket之后,直接就發(fā)起connect方法。

          此時(shí)回到服務(wù)端,會(huì)發(fā)現(xiàn)之前一直阻塞的accept方法,返回結(jié)果了。

          這就算兩端成功建立好了一條連接。之后就可以愉快的進(jìn)行讀寫操作了。

          那么,我們今天的問(wèn)題是,如果沒(méi)有這個(gè)accept方法,TCP連接還能建立起來(lái)嗎?

          其實(shí)只要在執(zhí)行accept() 之前執(zhí)行一個(gè) sleep(20),然后立刻執(zhí)行客戶端相關(guān)的方法,同時(shí)抓個(gè)包,就能得出結(jié)論。

          不執(zhí)行accept時(shí)抓包結(jié)果

          從抓包結(jié)果看來(lái),就算不執(zhí)行accept()方法,三次握手照常進(jìn)行,并順利建立連接。

          更騷氣的是,在服務(wù)端執(zhí)行accept()前,如果客戶端發(fā)送消息給服務(wù)端,服務(wù)端是能夠正?;貜?fù)ack確認(rèn)包的。

          并且,sleep(20)結(jié)束后,服務(wù)端正常執(zhí)行accept(),客戶端前面發(fā)送的消息,還是能正常收到的。

          通過(guò)這個(gè)現(xiàn)象,我們可以多想想為什么。順便好好了解下三次握手的細(xì)節(jié)。

          三次握手的細(xì)節(jié)分析

          我們先看面試八股文的老股,三次握手。

          TCP三次握手

          服務(wù)端代碼,對(duì)socket執(zhí)行bind方法可以綁定監(jiān)聽端口,然后執(zhí)行listen方法后,就會(huì)進(jìn)入監(jiān)聽(LISTEN)狀態(tài)。內(nèi)核會(huì)為每一個(gè)處于LISTEN狀態(tài)的socket 分配兩個(gè)隊(duì)列,分別叫半連接隊(duì)列和全連接隊(duì)列。

          每個(gè)listen Socket都有一個(gè)全連接和半連接隊(duì)列

          半連接隊(duì)列、全連接隊(duì)列是什么

          半連接隊(duì)列和全連接隊(duì)列
          • 半連接隊(duì)列(SYN隊(duì)列),服務(wù)端收到第一次握手后,會(huì)將sock加入到這個(gè)隊(duì)列中,隊(duì)列內(nèi)的sock都處于SYN_RECV 狀態(tài)。
          • 全連接隊(duì)列(ACCEPT隊(duì)列),在服務(wù)端收到第三次握手后,會(huì)將半連接隊(duì)列的sock取出,放到全連接隊(duì)列中。隊(duì)列里的sock都處于 ESTABLISHED狀態(tài)。這里面的連接,就等著服務(wù)端執(zhí)行accept()后被取出了。

          看到這里,文章開頭的問(wèn)題就有了答案,建立連接的過(guò)程中根本不需要accept() 參與, 執(zhí)行accept()只是為了從全連接隊(duì)列里取出一條連接。

          我們把話題再重新回到這兩個(gè)隊(duì)列上。

          雖然都叫隊(duì)列,但其實(shí)全連接隊(duì)列(icsk_accept_queue)是個(gè)鏈表,而半連接隊(duì)列(syn_table)是個(gè)哈希表

          半連接全連接隊(duì)列的內(nèi)部結(jié)構(gòu)

          為什么半連接隊(duì)列要設(shè)計(jì)成哈希表

          先對(duì)比下全連接里隊(duì)列,他本質(zhì)是個(gè)鏈表,因?yàn)橐彩蔷€性結(jié)構(gòu),說(shuō)它是個(gè)隊(duì)列也沒(méi)毛病。它里面放的都是已經(jīng)建立完成的連接,這些連接正等待被取走。而服務(wù)端取走連接的過(guò)程中,并不關(guān)心具體是哪個(gè)連接,只要是個(gè)連接就行,所以直接從隊(duì)列頭取就行了。這個(gè)過(guò)程算法復(fù)雜度為O(1)。

          半連接隊(duì)列卻不太一樣,因?yàn)殛?duì)列里的都是不完整的連接,嗷嗷等待著第三次握手的到來(lái)。那么現(xiàn)在有一個(gè)第三次握手來(lái)了,則需要從隊(duì)列里把相應(yīng)IP端口的連接取出,如果半連接隊(duì)列還是個(gè)鏈表,那我們就需要依次遍歷,才能拿到我們想要的那個(gè)連接,算法復(fù)雜度就是O(n)。

          而如果將半連接隊(duì)列設(shè)計(jì)成哈希表,那么查找半連接的算法復(fù)雜度就回到O(1)了。

          因此出于效率考慮,全連接隊(duì)列被設(shè)計(jì)成鏈表,而半連接隊(duì)列被設(shè)計(jì)為哈希表。

          怎么觀察兩個(gè)隊(duì)列的大小

          查看全連接隊(duì)列

          # ss -lnt
          State      Recv-Q Send-Q     Local Address:Port           Peer Address:Port
          LISTEN     0      128        127.0.0.1:46269              *:*              

          通過(guò)ss -lnt命令,可以看到全連接隊(duì)列的大小,其中Send-Q是指全連接隊(duì)列的最大值,可以看到我這上面的最大值是128;Recv-Q是指當(dāng)前的全連接隊(duì)列的使用值,我這邊用了0個(gè),也就是全連接隊(duì)列里為空,連接都被取出來(lái)了。

          當(dāng)上面Send-QRecv-Q數(shù)值很接近的時(shí)候,那么全連接隊(duì)列可能已經(jīng)滿了??梢酝ㄟ^(guò)下面的命令查看是否發(fā)生過(guò)隊(duì)列溢出。

          # netstat -s | grep overflowed
              4343 times the listen queue of a socket overflowed

          上面說(shuō)明發(fā)生過(guò)4343次全連接隊(duì)列溢出的情況。這個(gè)查看到的是歷史發(fā)生過(guò)的次數(shù)。

          如果配合使用watch -d 命令,可以自動(dòng)每2s間隔執(zhí)行相同命令,還能高亮顯示變化的數(shù)字部分,如果溢出的數(shù)字不斷變多,說(shuō)明正在發(fā)生溢出的行為。

          # watch -d 'netstat -s | grep overflowed'
          Every 2.0s: netstat -s | grep overflowed                                Fri Sep 17 09:00:45 2021

              4343 times the listen queue of a socket overflowed

          查看半連接隊(duì)列

          半連接隊(duì)列沒(méi)有命令可以直接查看到,但因?yàn)榘脒B接隊(duì)列里,放的都是SYN_RECV 狀態(tài)的連接,那可以通過(guò)統(tǒng)計(jì)處于這個(gè)狀態(tài)的連接的數(shù)量,間接獲得半連接隊(duì)列的長(zhǎng)度。

          # netstat -nt | grep -i '127.0.0.1:8080' | grep -i 'SYN_RECV' | wc -l
          0

          注意半連接隊(duì)列和全連接隊(duì)列都是掛在某個(gè)Listen socket上的,我這里用的是127.0.0.1:8080,大家可以替換成自己想要查看的IP端口。

          可以看到我的機(jī)器上的半連接隊(duì)列長(zhǎng)度為0,這個(gè)很正常,正經(jīng)連接誰(shuí)會(huì)沒(méi)事老待在半連接隊(duì)列里。

          當(dāng)隊(duì)列里的半連接不斷增多,最終也是會(huì)發(fā)生溢出,可以通過(guò)下面的命令查看。

          # netstat -s | grep -i "SYNs to LISTEN sockets dropped" 
              26395 SYNs to LISTEN sockets dropped

          可以看到,我的機(jī)器上一共發(fā)生了26395次半連接隊(duì)列溢出。同樣建議配合watch -d 命令使用。

          # watch -d 'netstat -s | grep -i "SYNs to LISTEN sockets dropped"'
          Every 2.0s: netstat -s | grep -i "SYNs to LISTEN sockets dropped"       Fri Sep 17 08:36:38 2021

              26395 SYNs to LISTEN sockets dropped

          全連接隊(duì)列滿了會(huì)怎么樣?

          如果隊(duì)列滿了,服務(wù)端還收到客戶端的第三次握手ACK,默認(rèn)當(dāng)然會(huì)丟棄這個(gè)ACK。

          但除了丟棄之外,還有一些附帶行為,這會(huì)受 tcp_abort_on_overflow 參數(shù)的影響。

          # cat /proc/sys/net/ipv4/tcp_abort_on_overflow
          0
          • tcp_abort_on_overflow設(shè)置為 0,全連接隊(duì)列滿了之后,會(huì)丟棄這個(gè)第三次握手ACK包,并且開啟定時(shí)器,重傳第二次握手的SYN+ACK,如果重傳超過(guò)一定限制次數(shù),還會(huì)把對(duì)應(yīng)的半連接隊(duì)列里的連接給刪掉。
          tcp_abort_on_overflow為0
          • tcp_abort_on_overflow設(shè)置為 1,全連接隊(duì)列滿了之后,就直接發(fā)RST給客戶端,效果上看就是連接斷了。

          這個(gè)現(xiàn)象是不是很熟悉,服務(wù)端端口未監(jiān)聽時(shí),客戶端嘗試去連接,服務(wù)端也會(huì)回一個(gè)RST。這兩個(gè)情況長(zhǎng)一樣,所以客戶端這時(shí)候收到RST之后,其實(shí)無(wú)法區(qū)分到底是端口未監(jiān)聽,還是全連接隊(duì)列滿了。

          tcp_abort_on_overflow為1

          半連接隊(duì)列要是滿了會(huì)怎么樣

          一般是丟棄,但這個(gè)行為可以通過(guò) tcp_syncookies 參數(shù)去控制。但比起這個(gè),更重要的是先了解下半連接隊(duì)列為什么會(huì)被打滿。

          首先我們需要明白,一般情況下,半連接的"生存"時(shí)間其實(shí)很短,只有在第一次和第三次握手間,如果半連接都滿了,說(shuō)明服務(wù)端瘋狂收到第一次握手請(qǐng)求,如果是線上游戲應(yīng)用,能有這么多請(qǐng)求進(jìn)來(lái),那說(shuō)明你可能要富了。但現(xiàn)實(shí)往往比較骨感,你可能遇到了SYN Flood攻擊。

          所謂SYN Flood攻擊,可以簡(jiǎn)單理解為,攻擊方模擬客戶端瘋狂發(fā)第一次握手請(qǐng)求過(guò)來(lái),在服務(wù)端憨憨地回復(fù)第二次握手過(guò)去之后,客戶端死活不發(fā)第三次握手過(guò)來(lái),這樣做,可以把服務(wù)端半連接隊(duì)列打滿,從而導(dǎo)致正常連接不能正常進(jìn)來(lái)。

          syn攻擊

          那這種情況怎么處理?有沒(méi)有一種方法可以繞過(guò)半連接隊(duì)列?

          有,上面提到的tcp_syncookies派上用場(chǎng)了。

          # cat /proc/sys/net/ipv4/tcp_syncookies
          1

          當(dāng)它被設(shè)置為1的時(shí)候,客戶端發(fā)來(lái)第一次握手SYN時(shí),服務(wù)端不會(huì)將其放入半連接隊(duì)列中,而是直接生成一個(gè)cookies,這個(gè)cookies會(huì)跟著第二次握手,發(fā)回客戶端。客戶端在發(fā)第三次握手的時(shí)候帶上這個(gè)cookies,服務(wù)端驗(yàn)證到它就是當(dāng)初發(fā)出去的那個(gè),就會(huì)建立連接并放入到全連接隊(duì)列中。可以看出整個(gè)過(guò)程不再需要半連接隊(duì)列的參與。

          tcp_syncookies=1

          會(huì)有一個(gè)cookies隊(duì)列嗎

          生成是cookies,保存在哪呢?是不是會(huì)有一個(gè)隊(duì)列保存這些cookies?

          我們可以反過(guò)來(lái)想一下,如果有cookies隊(duì)列,那它會(huì)跟半連接隊(duì)列一樣,到頭來(lái),還是會(huì)被SYN Flood 攻擊打滿。

          實(shí)際上cookies并不會(huì)有一個(gè)專門的隊(duì)列保存,它是通過(guò)通信雙方的IP地址端口、時(shí)間戳、MSS等信息進(jìn)行實(shí)時(shí)計(jì)算的,保存在TCP報(bào)頭seq里。

          tcp報(bào)頭_seq的位置

          當(dāng)服務(wù)端收到客戶端發(fā)來(lái)的第三次握手包時(shí),會(huì)通過(guò)seq還原出通信雙方的IP地址端口、時(shí)間戳、MSS,驗(yàn)證通過(guò)則建立連接。

          cookies方案為什么不直接取代半連接隊(duì)列?

          目前看下來(lái)syn cookies方案省下了半連接隊(duì)列所需要的隊(duì)列內(nèi)存,還能解決 SYN Flood攻擊,那為什么不直接取代半連接隊(duì)列?

          凡事皆有利弊,cookies方案雖然能防 SYN Flood攻擊,但是也有一些問(wèn)題。因?yàn)榉?wù)端并不會(huì)保存連接信息,所以如果傳輸過(guò)程中數(shù)據(jù)包丟了,也不會(huì)重發(fā)第二次握手的信息。

          另外,編碼解碼cookies,都是比較耗CPU的,利用這一點(diǎn),如果此時(shí)攻擊者構(gòu)造大量的第三次握手包(ACK包),同時(shí)帶上各種瞎編的cookies信息,服務(wù)端收到ACK包以為是正經(jīng)cookies,憨憨地跑去解碼(耗CPU),最后發(fā)現(xiàn)不是正經(jīng)數(shù)據(jù)包后才丟棄。

          這種通過(guò)構(gòu)造大量ACK包去消耗服務(wù)端資源的攻擊,叫ACK攻擊,受到攻擊的服務(wù)器可能會(huì)因?yàn)?strong style="font-weight: bold;line-height: 1.75em;color: #304FFE;">CPU資源耗盡導(dǎo)致沒(méi)能響應(yīng)正經(jīng)請(qǐng)求。

          ack攻擊

          沒(méi)有l(wèi)isten,為什么還能建立連接

          那既然沒(méi)有accept方法能建立連接,那是不是沒(méi)有listen方法,也能建立連接?是的,之前寫的一篇文章提到過(guò)客戶端是可以自己連自己的形成連接(TCP自連接),也可以兩個(gè)客戶端同時(shí)向?qū)Ψ桨l(fā)出請(qǐng)求建立連接(TCP同時(shí)打開),這兩個(gè)情況都有個(gè)共同點(diǎn),就是沒(méi)有服務(wù)端參與,也就是沒(méi)有l(wèi)isten,就能建立連接。

          當(dāng)時(shí)文章最后也留了個(gè)疑問(wèn),沒(méi)有l(wèi)isten,為什么還能建立連接?

          我們知道執(zhí)行listen方法時(shí),會(huì)創(chuàng)建半連接隊(duì)列和全連接隊(duì)列。

          三次握手的過(guò)程中會(huì)在這兩個(gè)隊(duì)列中暫存連接信息。

          所以形成連接,前提是你得有個(gè)地方存放著,方便握手的時(shí)候能根據(jù)IP端口等信息找到socket信息。

          那么客戶端會(huì)有半連接隊(duì)列嗎?

          顯然沒(méi)有,因?yàn)榭蛻舳藳](méi)有執(zhí)行listen,因?yàn)榘脒B接隊(duì)列和全連接隊(duì)列都是在執(zhí)行listen方法時(shí),內(nèi)核自動(dòng)創(chuàng)建的。

          但內(nèi)核還有個(gè)全局hash表,可以用于存放sock連接的信息。這個(gè)全局hash表其實(shí)還細(xì)分為ehash,bhash和listen_hash等,但因?yàn)檫^(guò)于細(xì)節(jié),大家理解成有一個(gè)全局hash就夠了,

          在TCP自連接的情況中,客戶端在connect方法時(shí),最后會(huì)將自己的連接信息放入到這個(gè)全局hash表中,然后將信息發(fā)出,消息在經(jīng)過(guò)回環(huán)地址重新回到TCP傳輸層的時(shí)候,就會(huì)根據(jù)IP端口信息,再一次從這個(gè)全局hash中取出信息。于是握手包一來(lái)一回,最后成功建立連接。

          TCP 同時(shí)打開的情況也類似,只不過(guò)從一個(gè)客戶端變成了兩個(gè)客戶端而已。

          總結(jié)

          • 每一個(gè)socket執(zhí)行listen時(shí),內(nèi)核都會(huì)自動(dòng)創(chuàng)建一個(gè)半連接隊(duì)列和全連接隊(duì)列。
          • 第三次握手前,TCP連接會(huì)放在半連接隊(duì)列中,直到第三次握手到來(lái),才會(huì)被放到全連接隊(duì)列中。
          • accept方法只是為了從全連接隊(duì)列中拿出一條連接,本身跟三次握手幾乎毫無(wú)關(guān)系
          • 出于效率考慮,雖然都叫隊(duì)列,但半連接隊(duì)列其實(shí)被設(shè)計(jì)成了哈希表,而全連接隊(duì)列本質(zhì)是鏈表。
          • 全連接隊(duì)列滿了,再來(lái)第三次握手也會(huì)丟棄,此時(shí)如果tcp_abort_on_overflow=1,還會(huì)直接發(fā)RST給客戶端。
          • 半連接隊(duì)列滿了,可能是因?yàn)槭艿搅?code style="font-size: 14px;word-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin: 0 2px;background-color: rgba(27,31,35,.05);font-family: Operator Mono, Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(100,149,237);">SYN Flood攻擊,可以設(shè)置tcp_syncookies,繞開半連接隊(duì)列。
          • 客戶端沒(méi)有半連接隊(duì)列和全連接隊(duì)列,但有一個(gè)全局hash,可以通過(guò)它實(shí)現(xiàn)自連接或TCP同時(shí)打開。

          推薦閱讀
          不鴿了,小林的「圖解網(wǎng)絡(luò) 3.0 」發(fā)布!
          TCP 半連接隊(duì)列和全連接隊(duì)列滿了會(huì)發(fā)生什么?又該如何應(yīng)對(duì)?
          TCP 重傳、滑動(dòng)窗口、流量控制、擁塞控好難?看完圖解就不愁了(重制)
          瀏覽 32
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  无码一道本一区二区无码 | 男人天堂色色色 | 超碰手机在线 | 日韩免费视频在线观看 | 欧洲无码在线播放 |