正文
下面這個動圖,是我們平時客戶端和服務(wù)端建立連接時的代碼流程。
![df3b975e-1464-11ed-ba43-dac502259ad0.gif](https://file1.elecfans.com//web2/M00/95/F8/wKgZomTnE4uAZl1wAAU78Ofyn6w366.gif)
對應(yīng)的是下面一段簡化過的服務(wù)端偽代碼。
intmain()
{
/*Step1:創(chuàng)建服務(wù)器端監(jiān)聽socket描述符listen_fd*/
listen_fd=socket(AF_INET,SOCK_STREAM,0);
/*Step2:bind綁定服務(wù)器端的IP和端口,所有客戶端都向這個IP和端口發(fā)送和請求數(shù)據(jù)*/
bind(listen_fd,xxx);
/*Step3:服務(wù)端開啟監(jiān)聽*/
listen(listen_fd,128);
/*Step4:服務(wù)器等待客戶端的鏈接,返回值cfd為客戶端的socket描述符*/
cfd=accept(listen_fd,xxx);
/*Step5:讀取客戶端發(fā)來的數(shù)據(jù)*/
n=read(cfd,buf,sizeof(buf));
}
估計大家也是老熟悉這段偽代碼了。
需要注意的是,在執(zhí)行listen()
方法之后還會執(zhí)行一個accept()
方法。
一般情況下,如果啟動服務(wù)器,會發(fā)現(xiàn)最后程序會阻塞在accept()
里。
此時服務(wù)端就算ok了,就等客戶端了。
那么,再看下簡化過的客戶端偽代碼。
intmain()
{
/*Step1:創(chuàng)建客戶端端socket描述符cfd*/
cfd=socket(AF_INET,SOCK_STREAM,0);
/*Step2:connect方法,對服務(wù)器端的IP和端口號發(fā)起連接*/
ret=connect(cfd,xxxx);
/*Step4:向服務(wù)器端寫數(shù)據(jù)*/
write(cfd,buf,strlen(buf));
}
客戶端比較簡單,創(chuàng)建好socket
之后,直接就發(fā)起connect
方法。
此時回到服務(wù)端,會發(fā)現(xiàn)之前一直阻塞的accept方法,返回結(jié)果了。
這就算兩端成功建立好了一條連接。之后就可以愉快的進(jìn)行讀寫操作了。
那么,我們今天的問題是,如果沒有這個accept方法,TCP連接還能建立起來嗎?
其實只要在執(zhí)行accept()
之前執(zhí)行一個 sleep(20)
,然后立刻執(zhí)行客戶端相關(guān)的方法,同時抓個包,就能得出結(jié)論。
![df544aa6-1464-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/F8/wKgZomTnE4uAefhVAAEuQJP5l5s738.png)
從抓包結(jié)果看來,就算不執(zhí)行accept()方法,三次握手照常進(jìn)行,并順利建立連接。
更騷氣的是,在服務(wù)端執(zhí)行accept()前,如果客戶端發(fā)送消息給服務(wù)端,服務(wù)端是能夠正常回復(fù)ack確認(rèn)包的。
并且,sleep(20)
結(jié)束后,服務(wù)端正常執(zhí)行accept()
,客戶端前面發(fā)送的消息,還是能正常收到的。
通過這個現(xiàn)象,我們可以多想想為什么。順便好好了解下三次握手的細(xì)節(jié)。
三次握手的細(xì)節(jié)分析
我們先看面試八股文的老股,三次握手。
![df63c2ce-1464-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/F8/wKgZomTnE4uACIRaAAHqGa2Xc5Y734.png)
服務(wù)端代碼,對socket執(zhí)行bind方法可以綁定監(jiān)聽端口,然后執(zhí)行listen方法
后,就會進(jìn)入監(jiān)聽(LISTEN
)狀態(tài)。內(nèi)核會為每一個處于LISTEN
狀態(tài)的socket
分配兩個隊列,分別叫半連接隊列和全連接隊列。
![df74a92c-1464-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/F8/wKgZomTnE4uAOn6dAADqQPBI9sQ389.png)
半連接隊列、全連接隊列是什么
![df826e68-1464-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/F8/wKgZomTnE4uAaHqsAAG6CG0fhqU963.png)
-
半連接隊列(SYN隊列),服務(wù)端收到第一次握手后,會將
sock
加入到這個隊列中,隊列內(nèi)的sock
都處于SYN_RECV
狀態(tài)。 -
全連接隊列(ACCEPT隊列),在服務(wù)端收到第三次握手后,會將半連接隊列的
sock
取出,放到全連接隊列中。隊列里的sock
都處于ESTABLISHED
狀態(tài)。這里面的連接,就等著服務(wù)端執(zhí)行accept()后被取出了。
看到這里,文章開頭的問題就有了答案,建立連接的過程中根本不需要accept()
參與, 執(zhí)行accept()只是為了從全連接隊列里取出一條連接。
我們把話題再重新回到這兩個隊列上。
雖然都叫隊列,但其實全連接隊列(icsk_accept_queue)是個鏈表,而半連接隊列(syn_table)是個哈希表。
![df8f7cd4-1464-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/F8/wKgZomTnE4uAKQr5AAEiqRgUMrE941.png)
為什么半連接隊列要設(shè)計成哈希表
先對比下全連接里隊列,他本質(zhì)是個鏈表,因為也是線性結(jié)構(gòu),說它是個隊列也沒毛病。它里面放的都是已經(jīng)建立完成的連接,這些連接正等待被取走。而服務(wù)端取走連接的過程中,并不關(guān)心具體是哪個連接,只要是個連接就行,所以直接從隊列頭取就行了。這個過程算法復(fù)雜度為O(1)
。
而半連接隊列卻不太一樣,因為隊列里的都是不完整的連接,嗷嗷等待著第三次握手的到來。那么現(xiàn)在有一個第三次握手來了,則需要從隊列里把相應(yīng)IP端口的連接取出,如果半連接隊列還是個鏈表,那我們就需要依次遍歷,才能拿到我們想要的那個連接,算法復(fù)雜度就是O(n)。
而如果將半連接隊列設(shè)計成哈希表,那么查找半連接的算法復(fù)雜度就回到O(1)
了。
因此出于效率考慮,全連接隊列被設(shè)計成鏈表,而半連接隊列被設(shè)計為哈希表。
怎么觀察兩個隊列的大小
查看全連接隊列
#ss-lnt
StateRecv-QSend-QLocalAddress:PortPeerAddress:Port
LISTEN0128127.0.0.1:46269*:*
通過ss -lnt
命令,可以看到全連接隊列的大小,其中Send-Q
是指全連接隊列的最大值,可以看到我這上面的最大值是128
;Recv-Q
是指當(dāng)前的全連接隊列的使用值,我這邊用了0
個,也就是全連接隊列里為空,連接都被取出來了。
當(dāng)上面Send-Q
和Recv-Q
數(shù)值很接近的時候,那么全連接隊列可能已經(jīng)滿了。可以通過下面的命令查看是否發(fā)生過隊列溢出。
#netstat-s|grepoverflowed
4343timesthelistenqueueofasocketoverflowed
上面說明發(fā)生過4343次
全連接隊列溢出的情況。這個查看到的是歷史發(fā)生過的次數(shù)。
如果配合使用watch -d
命令,可以自動每2s
間隔執(zhí)行相同命令,還能高亮顯示變化的數(shù)字部分,如果溢出的數(shù)字不斷變多,說明正在發(fā)生溢出的行為。
#watch-d'netstat-s|grepoverflowed'
Every2.0s:netstat-s|grepoverflowedFriSep1709452021
4343timesthelistenqueueofasocketoverflowed
查看半連接隊列
半連接隊列沒有命令可以直接查看到,但因為半連接隊列里,放的都是SYN_RECV
狀態(tài)的連接,那可以通過統(tǒng)計處于這個狀態(tài)的連接的數(shù)量,間接獲得半連接隊列的長度。
#netstat-nt|grep-i'127.0.0.1:8080'|grep-i'SYN_RECV'|wc-l
0
注意半連接隊列和全連接隊列都是掛在某個Listen socket
上的,我這里用的是127.0.0.1:8080
,大家可以替換成自己想要查看的IP端口。
可以看到我的機(jī)器上的半連接隊列長度為0
,這個很正常,正經(jīng)連接誰會沒事老待在半連接隊列里。
當(dāng)隊列里的半連接不斷增多,最終也是會發(fā)生溢出,可以通過下面的命令查看。
#netstat-s|grep-i"SYNstoLISTENsocketsdropped"
26395SYNstoLISTENsocketsdropped
可以看到,我的機(jī)器上一共發(fā)生了26395
次半連接隊列溢出。同樣建議配合watch -d
命令使用。
#watch-d'netstat-s|grep-i"SYNstoLISTENsocketsdropped"'
Every2.0s:netstat-s|grep-i"SYNstoLISTENsocketsdropped"FriSep1708382021
26395SYNstoLISTENsocketsdropped
全連接隊列滿了會怎么樣?
如果隊列滿了,服務(wù)端還收到客戶端的第三次握手ACK,默認(rèn)當(dāng)然會丟棄這個ACK。
但除了丟棄之外,還有一些附帶行為,這會受 tcp_abort_on_overflow
參數(shù)的影響。
#cat/proc/sys/net/ipv4/tcp_abort_on_overflow
0
-
tcp_abort_on_overflow
設(shè)置為 0,全連接隊列滿了之后,會丟棄這個第三次握手ACK包,并且開啟定時器,重傳第二次握手的SYN+ACK,如果重傳超過一定限制次數(shù),還會把對應(yīng)的半連接隊列里的連接給刪掉。
![df9c5cc4-1464-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/F8/wKgZomTnE4uAD4GHAALAf60isdQ550.png)
-
tcp_abort_on_overflow
設(shè)置為 1,全連接隊列滿了之后,就直接發(fā)RST給客戶端,效果上看就是連接斷了。
這個現(xiàn)象是不是很熟悉,服務(wù)端端口未監(jiān)聽時,客戶端嘗試去連接,服務(wù)端也會回一個RST。這兩個情況長一樣,所以客戶端這時候收到RST之后,其實無法區(qū)分到底是端口未監(jiān)聽,還是全連接隊列滿了。
![dfaa62a6-1464-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/F8/wKgZomTnE4yARy9DAAIHUolBPEE999.png)
半連接隊列要是滿了會怎么樣
一般是丟棄,但這個行為可以通過 tcp_syncookies
參數(shù)去控制。但比起這個,更重要的是先了解下半連接隊列為什么會被打滿。
首先我們需要明白,一般情況下,半連接的"生存"時間其實很短,只有在第一次和第三次握手間,如果半連接都滿了,說明服務(wù)端瘋狂收到第一次握手請求,如果是線上游戲應(yīng)用,能有這么多請求進(jìn)來,那說明你可能要富了。但現(xiàn)實往往比較骨感,你可能遇到了SYN Flood攻擊。
所謂SYN Flood攻擊,可以簡單理解為,攻擊方模擬客戶端瘋狂發(fā)第一次握手請求過來,在服務(wù)端憨憨地回復(fù)第二次握手過去之后,客戶端死活不發(fā)第三次握手過來,這樣做,可以把服務(wù)端半連接隊列打滿,從而導(dǎo)致正常連接不能正常進(jìn)來。
![dfcd5ec8-1464-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/F8/wKgZomTnE4yAJFNzAAFD8oN4UJg735.png)
那這種情況怎么處理?有沒有一種方法可以繞過半連接隊列?
有,上面提到的tcp_syncookies
派上用場了。
#cat/proc/sys/net/ipv4/tcp_syncookies
1
當(dāng)它被設(shè)置為1的時候,客戶端發(fā)來第一次握手SYN時,服務(wù)端不會將其放入半連接隊列中,而是直接生成一個cookies
,這個cookies
會跟著第二次握手,發(fā)回客戶端。客戶端在發(fā)第三次握手的時候帶上這個cookies
,服務(wù)端驗證到它就是當(dāng)初發(fā)出去的那個,就會建立連接并放入到全連接隊列中。可以看出整個過程不再需要半連接隊列的參與。
![dfd804cc-1464-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/F8/wKgZomTnE4yAeD6BAAHPYnYg2v8800.png)
會有一個cookies隊列嗎
生成是cookies
,保存在哪呢?是不是會有一個隊列保存這些cookies?
我們可以反過來想一下,如果有cookies
隊列,那它會跟半連接隊列一樣,到頭來,還是會被SYN Flood 攻擊打滿。
實際上cookies
并不會有一個專門的隊列保存,它是通過通信雙方的IP地址端口、時間戳、MSS等信息進(jìn)行實時計算的,保存在TCP報頭的seq
里。
![dff73630-1464-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/F8/wKgZomTnE4yAPO1oAAEcTITGTFM797.png)
當(dāng)服務(wù)端收到客戶端發(fā)來的第三次握手包時,會通過seq還原出通信雙方的IP地址端口、時間戳、MSS,驗證通過則建立連接。
cookies方案為什么不直接取代半連接隊列?
目前看下來syn cookies
方案省下了半連接隊列所需要的隊列內(nèi)存,還能解決 SYN Flood攻擊,那為什么不直接取代半連接隊列?
凡事皆有利弊,cookies
方案雖然能防 SYN Flood攻擊,但是也有一些問題。因為服務(wù)端并不會保存連接信息,所以如果傳輸過程中數(shù)據(jù)包丟了,也不會重發(fā)第二次握手的信息。
另外,編碼解碼cookies
,都是比較耗CPU的,利用這一點,如果此時攻擊者構(gòu)造大量的第三次握手包(ACK包),同時帶上各種瞎編的cookies
信息,服務(wù)端收到ACK包
后以為是正經(jīng)cookies,憨憨地跑去解碼(耗CPU),最后發(fā)現(xiàn)不是正經(jīng)數(shù)據(jù)包后才丟棄。
這種通過構(gòu)造大量ACK包
去消耗服務(wù)端資源的攻擊,叫ACK攻擊,受到攻擊的服務(wù)器可能會因為CPU資源耗盡導(dǎo)致沒能響應(yīng)正經(jīng)請求。
![e0037ae4-1464-11ed-ba43-dac502259ad0.gif](https://file1.elecfans.com//web2/M00/95/F8/wKgZomTnE4yAeNF6AAglhZlo2CA500.gif)
沒有l(wèi)isten,為什么還能建立連接
那既然沒有accept
方法能建立連接,那是不是沒有listen
方法,也能建立連接?是的,之前寫的一篇文章提到過客戶端是可以自己連自己的形成連接(TCP自連接),也可以兩個客戶端同時向?qū)Ψ桨l(fā)出請求建立連接(TCP同時打開),這兩個情況都有個共同點,就是沒有服務(wù)端參與,也就是沒有l(wèi)isten,就能建立連接。
當(dāng)時文章最后也留了個疑問,沒有l(wèi)isten,為什么還能建立連接?
我們知道執(zhí)行listen
方法時,會創(chuàng)建半連接隊列和全連接隊列。
三次握手的過程中會在這兩個隊列中暫存連接信息。
所以形成連接,前提是你得有個地方存放著,方便握手的時候能根據(jù)IP端口等信息找到socket信息。
那么客戶端會有半連接隊列嗎?
顯然沒有,因為客戶端沒有執(zhí)行listen
,因為半連接隊列和全連接隊列都是在執(zhí)行listen
方法時,內(nèi)核自動創(chuàng)建的。
但內(nèi)核還有個全局hash表,可以用于存放sock
連接的信息。這個全局hash
表其實還細(xì)分為ehash,bhash和listen_hash
等,但因為過于細(xì)節(jié),大家理解成有一個全局hash就夠了,
在TCP自連接的情況中,客戶端在connect
方法時,最后會將自己的連接信息放入到這個全局hash表中,然后將信息發(fā)出,消息在經(jīng)過回環(huán)地址重新回到TCP傳輸層的時候,就會根據(jù)IP端口信息,再一次從這個全局hash中取出信息。于是握手包一來一回,最后成功建立連接。
TCP 同時打開的情況也類似,只不過從一個客戶端變成了兩個客戶端而已。
總結(jié)
-
每一個
socket
執(zhí)行listen
時,內(nèi)核都會自動創(chuàng)建一個半連接隊列和全連接隊列。 - 第三次握手前,TCP連接會放在半連接隊列中,直到第三次握手到來,才會被放到全連接隊列中。
-
accept方法
只是為了從全連接隊列中拿出一條連接,本身跟三次握手幾乎毫無關(guān)系。 - 出于效率考慮,雖然都叫隊列,但半連接隊列其實被設(shè)計成了哈希表,而全連接隊列本質(zhì)是鏈表。
-
全連接隊列滿了,再來第三次握手也會丟棄,此時如果
tcp_abort_on_overflow=1
,還會直接發(fā)RST
給客戶端。 -
半連接隊列滿了,可能是因為受到了
SYN Flood
攻擊,可以設(shè)置tcp_syncookies
,繞開半連接隊列。 - 客戶端沒有半連接隊列和全連接隊列,但有一個全局hash,可以通過它實現(xiàn)自連接或TCP同時打開。
-
服務(wù)器
+關(guān)注
關(guān)注
12文章
9342瀏覽量
86179 -
TCP
+關(guān)注
關(guān)注
8文章
1382瀏覽量
79359 -
代碼
+關(guān)注
關(guān)注
30文章
4841瀏覽量
69166
原文標(biāo)題:阿里二面:沒有 accept,能建立 TCP 連接嗎?
文章出處:【微信號:小林coding,微信公眾號:小林coding】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
TCP和UDP建立連接的差異和可靠性的差異
如何標(biāo)識一個TCP連接
STM32H7+FREERTOS+LWIP建立TCP連接,連接不穩(wěn)定怎么解決?
6678的sy***ios中的NDK的例程hello和clint中,使用TCP前沒有看到bing和listen accept等函數(shù)
為什么建立TCP連接有時成功有時失敗?
配置靜態(tài)IP地址時,沒有TCP連接可以建立,對ping沒有響應(yīng)
RT-Thread socket編程無法建立TCP連接是何原因
TCP通信通過網(wǎng)絡(luò)調(diào)試助手與S7-1200建立TCP連接
要是沒有一端進(jìn)行監(jiān)聽是否可以建立起TCP連接呢?
![要是<b class='flag-5'>沒有</b>一端進(jìn)行監(jiān)聽是否可以<b class='flag-5'>建立</b>起<b class='flag-5'>TCP</b><b class='flag-5'>連接</b>呢?](https://file.elecfans.com//web2/M00/7B/0E/poYBAGN0sL6AFXtHAABDoWeQnXw727.jpg)
評論