tcp連接狀態(tài)詳解

unix的哲學(xué)是一切皆文件,可以把socket看成是一種特殊的文件,而一些socket函數(shù)就是對其進(jìn)行的操作api(讀/寫IO、打開、關(guān)閉)。我們知道普通文件的打開操作(open)返回一個文件描述字,與之類似,socket()用于創(chuàng)建一個socket描述符(socket descriptor),它唯一標(biāo)識一個socket。

socket api

int socket(int domain,int type,int protocol);

當(dāng)我們調(diào)用socket創(chuàng)建一個socket時,返回的socket描述字它存在于協(xié)議族(address family,AF_XXX)空間中,但沒有一個具體的地址。如果想要給它賦值一個地址,就必須調(diào)用bind()函數(shù),

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

?sockfd即socket描述字,它是通過socket()函數(shù)創(chuàng)建了,唯一標(biāo)識一個socket。bind()函數(shù)就是將給這個描述字綁定一個名字。
????在將一個地址綁定到socket的時候,需要先將主機(jī)字節(jié)序轉(zhuǎn)換成為網(wǎng)絡(luò)字節(jié)序,而不要假定主機(jī)字節(jié)序跟網(wǎng)絡(luò)字節(jié)序一樣使用的是Big-Endian。由于這個問題曾引發(fā)過不少血案,謹(jǐn)記對主機(jī)字節(jié)序不要做任何假定,務(wù)必將其轉(zhuǎn)化為網(wǎng)絡(luò)字節(jié)序再賦給socket。
????這里的主機(jī)字節(jié)序就是我們平常說的大端和小端模式:不同的CPU有不同的字節(jié)序類型,這些字節(jié)序是指整數(shù)在內(nèi)存中保存的順序,這個叫做主機(jī)序。引用標(biāo)準(zhǔn)的Big-Endian和Little-Endian的定義如下:

  • Little-Endian就是低位字節(jié)排放在內(nèi)存的低地址端,高位字節(jié)排放在內(nèi)存的高地址端。

  • Big-Endian就是高位字節(jié)排放在內(nèi)存的低地址端,低位字節(jié)排放在內(nèi)存的高地址端。

int listen(int sockfd, int backlog);

? ?listen函數(shù)的第一個參數(shù)即為要監(jiān)聽的socket描述字,第二個參數(shù)為socket可以接受的排隊的最大連接個數(shù)。listen函數(shù)表示等待客戶的連接請求。

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

? connect函數(shù)的第一個參數(shù)即為客戶端的socket描述字,第二參數(shù)為服務(wù)器的socket地址,第三個參數(shù)為socket地址的長度??蛻舳送ㄟ^調(diào)用connect函數(shù)來建立與TCP服務(wù)器的連接。

?TCP服務(wù)器端依次調(diào)用socket()、bind()、listen()之后,就會監(jiān)聽指定的socket地址了。TCP客戶端依次調(diào)用socket()、connect()之后就向TCP服務(wù)器發(fā)送連接請求。TCP服務(wù)器監(jiān)聽到這個請求之后,就會調(diào)用accept()函數(shù)去接收請求,這樣連接就建立好了(在connect之后就建立好了三次連接),之后就可以開始進(jìn)行類似于普通文件的網(wǎng)絡(luò)I/O操作了。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

?如果accpet成功,那么其返回值是由內(nèi)核自動生成的一個全新的描述字,代表與客戶的TCP連接。
?accept的第一個參數(shù)為服務(wù)器的socket描述字,是服務(wù)器開始調(diào)用socket()函數(shù)生成的,稱為監(jiān)聽socket描述字;而accept函數(shù)返回的是已連接的socket描述字。一個服務(wù)器通常通常僅僅只創(chuàng)建一個監(jiān)聽socket描述字,它在該服務(wù)器的生命周期內(nèi)一直存在。內(nèi)核為每個由服務(wù)器進(jìn)程接受的客戶連接創(chuàng)建了一個已連接socket描述字,當(dāng)服務(wù)器完成了對某個客戶的服務(wù),相應(yīng)的已連接socket描述字就被關(guān)閉。

?read函數(shù)是負(fù)責(zé)從fd中讀取內(nèi)容.當(dāng)讀成功時,read返回實際所讀的字節(jié)數(shù),如果返回的值是0表示已經(jīng)讀到文件的結(jié)束了,小于0表示出現(xiàn)了錯誤。如果錯誤為EINTR說明讀是由中斷引起的,如果是ECONNREST表示網(wǎng)絡(luò)連接出了問題。

?write函數(shù)將buf中的nbytes字節(jié)內(nèi)容寫入文件描述符fd.成功時返回寫的字節(jié)數(shù)。失敗時返回-1,并設(shè)置errno變量。 在網(wǎng)絡(luò)程序中,當(dāng)我們向套接字文件描述符寫時有倆種可能。1)write的返回值大于0,表示寫了部分或者是全部的數(shù)據(jù)。2)返回的值小于0,此時出現(xiàn)了錯誤

int close(int fd);

?在服務(wù)器與客戶端建立連接之后,會進(jìn)行一些讀寫操作,完成了讀寫操作就要關(guān)閉相應(yīng)的socket描述字,類似于操作完打開的文件要調(diào)用fclose關(guān)閉打開的文件。

?close一個TCP socket的缺省行為時把該socket標(biāo)記為已關(guān)閉,然后立即返回到調(diào)用進(jìn)程。該描述字不能再由調(diào)用進(jìn)程使用,也就是說不能再作為read或write的第一個參數(shù)

?close操作只是使相應(yīng)socket描述字的引用計數(shù)-1,只有當(dāng)引用計數(shù)為0的時候,才會觸發(fā)TCP客戶端向服務(wù)器發(fā)送終止連接請求。

?我們知道tcp建立連接要進(jìn)行“三次握手”,即交換三個分組。大致流程如下:

客戶端向服務(wù)器發(fā)送一個SYN J

服務(wù)器向客戶端響應(yīng)一個SYN K,并對SYN J進(jìn)行確認(rèn)ACK J+1

客戶端再想服務(wù)器發(fā)一個確認(rèn)ACK K+1

socket中TCP的四次握手釋放連接詳解


image.png

?某個應(yīng)用進(jìn)程首先調(diào)用close主動關(guān)閉連接,這時TCP發(fā)送一個FIN M;另一端接收到FIN M之后,執(zhí)行被動關(guān)閉,對這個FIN進(jìn)行確認(rèn)。一段時間之后,服務(wù)端調(diào)用close關(guān)閉它的socket。這導(dǎo)致它的TCP也發(fā)送一個FIN N;接收到這個FIN的源發(fā)送端TCP對它進(jìn)行確認(rèn),這樣每個方向上都有一個FIN和ACK。

為什么要三次握手?

為什么要三次握手

為什么要四次揮手?

由于tcp連接是全雙工的,存在著雙向的讀寫通道,每個方向都必須單獨進(jìn)行關(guān)閉。當(dāng)一方完成它的數(shù)據(jù)發(fā)送任務(wù)后就可以發(fā)送一個FIN來終止這個方向的連接。收到FIN只意味著這個方向上沒有數(shù)據(jù)流動,但并不表示在另一個方向上沒有讀寫,所以要雙向的讀寫關(guān)閉需要四次握手,

tcp連接過程中的各種狀態(tài)

image.png
  • SYN_RECV : 服務(wù)端收到建立連接的SYN沒有收到ACK包的時候處在SYN_RECV狀態(tài)。有兩個相關(guān)系統(tǒng)配置:
  1. net.ipv4.tcp_synack_retries :INTEGER
    ??默認(rèn)值是5,對于遠(yuǎn)端的連接請求SYN,內(nèi)核會發(fā)送SYN + ACK數(shù)據(jù)報,以確認(rèn)收到上一個 SYN連接請求包。這是所謂的三次握手( threeway handshake)機(jī)制的第二個步驟。這里決定內(nèi)核在放棄連接之前所送出的 SYN+ACK 數(shù)目。不應(yīng)該大于255,默認(rèn)值是5,對應(yīng)于180秒左右時間。通常我們不對這個值進(jìn)行修改,因為我們希望TCP連接不要因為偶爾的丟包而無法建立。
  2. net.ipv4.tcp_syncookies
    ??一般服務(wù)器都會設(shè)net.ipv4.tcp_syncookies=1來防止SYN Flood攻擊。假設(shè)一個用戶向服務(wù)器發(fā)送了SYN報文后突然死機(jī)或掉線,那么服務(wù)器在發(fā)出SYN+ACK應(yīng)答報文后是無法收到客戶端的ACK報文的(第三次握手無法完成),這種情況下服務(wù)器端一般會重試(再次發(fā)送SYN+ACK給客戶端)并等待一段時間后丟棄這個未完成的連接,這段時間的長度我們稱為SYN Timeout,一般來說這個時間是分鐘的數(shù)量級(大約為30秒-2分鐘)。這些處在SYNC_RECV的TCP連接稱為半連接,并存儲在內(nèi)核的半連接隊列中,在內(nèi)核收到對端發(fā)送的ack包時會查找半連接隊列,并將符合的requst_sock信息存儲到完成三次握手的連接的隊列中,然后刪除此半連接。大量SYNC_RECV的TCP連接會導(dǎo)致半連接隊列溢出,這樣后續(xù)的連接建立請求會被內(nèi)核直接丟棄,這就是SYN Flood攻擊。能夠有效防范SYN Flood攻擊的手段之一,就是SYN Cookie。SYN Cookie原理由D. J. Bernstain和 Eric Schenk發(fā)明。SYN Cookie是對TCP服務(wù)器端的三次握手協(xié)議作一些修改,專門用來防范SYN Flood攻擊的一種手段。它的原理是,在TCP服務(wù)器收到TCP SYN包并返回TCP SYN+ACK包時,不分配一個專門的數(shù)據(jù)區(qū),而是根據(jù)這個SYN包計算出一個cookie值。在收到TCP ACK包時,TCP服務(wù)器再根據(jù)那個cookie值檢查這個TCP ACK包的合法性。如果合法,再分配專門的數(shù)據(jù)區(qū)進(jìn)行處理未來的TCP連接。
  • CLOSE_WAIT
    ?發(fā)起TCP連接關(guān)閉的一方稱為client,被動關(guān)閉的一方稱為server。被動關(guān)閉的server收到FIN后,但未發(fā)出ACK的TCP狀態(tài)是CLOSE_WAIT。出現(xiàn)這種狀況一般都是由于server端代碼的問題,如果你的服務(wù)器上出現(xiàn)大量CLOSE_WAIT,應(yīng)該要考慮檢查代碼。

  • 為什么連接的時候是三次握手,關(guān)閉的時候卻是四次握手?
    答:因為當(dāng)Server端收到Client端的SYN連接請求報文后,可以直接發(fā)送SYN+ACK報文。其中ACK報文是用來應(yīng)答的,SYN報文是用來同步的。但是關(guān)閉連接時,當(dāng)Server端收到FIN報文時,很可能并不會立即關(guān)閉SOCKET,所以只能先回復(fù)一個ACK報文,告訴Client端,"你發(fā)的FIN報文我收到了"。只有等到我Server端所有的報文都發(fā)送完了,我才能發(fā)送FIN報文,因此不能一起發(fā)送。故需要四步握手。

  • 根據(jù)TCP協(xié)議定義的3次握手?jǐn)嚅_連接規(guī)定,發(fā)起socket主動關(guān)閉的一方 socket將進(jìn)入TIME_WAIT狀態(tài)。TIME_WAIT狀態(tài)將持續(xù)2個MSL(Max Segment Lifetime),在Windows下默認(rèn)為4分鐘,即240秒。TIME_WAIT狀態(tài)下的socket不能被回收使用. 具體現(xiàn)象是對于一個處理大量短連接的服務(wù)器,如果是由服務(wù)器主動關(guān)閉客戶端的連接,將導(dǎo)致服務(wù)器端存在大量的處于TIME_WAIT狀態(tài)的socket, 對于訪問量大的Web Server,會存在大量的TIME_WAIT狀態(tài),假如server一秒鐘接收1000個請求,那么就會積壓240×1000=240000個TIME_WAIT的記錄,維護(hù)這些狀態(tài)給Server帶來負(fù)擔(dān)。當(dāng)然現(xiàn)代操作系統(tǒng)都會用快速的查找算法來管理這些TIME_WAIT,所以對于新的TCP連接請求,判斷是否hit中一個TIME_WAIT不會太費(fèi)時間,但是有這么多狀態(tài)要維護(hù)總是不好。
    HTTP協(xié)議1.1版規(guī)定default行為是Keep-Alive,也就是會重用TCP連接傳輸多個request/response,一個主要原因就是發(fā)現(xiàn)了這個問題。還有一個方法減緩TIME_WAIT壓力就是通過修改系統(tǒng)配置把系統(tǒng)的2MSL時間減少,因為240秒的時間實在是忒長了點。

  • 既然TIME_WAIT會帶來不少問題,那么tcp設(shè)計的時候為什么需要TIME_WAIT?原因有兩點: TIME_WAIT是TCP協(xié)議用以保證對方能夠收到最后的ack報文,也是TCP協(xié)議用以保證被重新分配的socket不會受到之前殘留的延遲重發(fā)報文影響的機(jī)制,是必要的邏輯保證。
    ????1. 如果Client直接CLOSED了,那么由于IP協(xié)議的不可靠性或者是其它網(wǎng)絡(luò)原因,導(dǎo)致Server沒有收到Client最后回復(fù)的ACK。那么Server就會在超時之后繼續(xù)發(fā)送FIN,此時由于Client已經(jīng)CLOSED了,就找不到與重發(fā)的FIN對應(yīng)的連接,最后Server就會收到RST而不是ACK,Server就會以為是連接錯誤把問題報告給高層。這樣的情況雖然不會造成數(shù)據(jù)丟失,但是卻導(dǎo)致TCP協(xié)議不符合可靠連接的要求。所以,Client不是直接進(jìn)入CLOSED,而是要保持TIME_WAIT,當(dāng)再次收到FIN的時候,能夠保證對方收到ACK,最后正確的關(guān)閉連接。
    ???? 2. 如果Client直接CLOSED,然后又再向Server發(fā)起一個新連接,我們不能保證這個新連接與剛關(guān)閉的連接的端口號是不同的。也就是說有可能新連接和老連接的端口號是相同的。一般來說不會發(fā)生什么問題,但是還是有特殊情況出現(xiàn):假設(shè)新連接和已經(jīng)關(guān)閉的老連接端口號是一樣的,如果前一次連接的某些數(shù)據(jù)仍然滯留在網(wǎng)絡(luò)中,這些延遲數(shù)據(jù)在建立新連接之后才到達(dá)Server,由于新連接和老連接的端口號是一樣的,又因為TCP協(xié)議判斷不同連接的依據(jù)是socket pair,于是,TCP協(xié)議就認(rèn)為那個延遲的數(shù)據(jù)是屬于新連接的,這樣就和真正的新連接的數(shù)據(jù)包發(fā)生混淆了。所以TCP連接還要在TIME_WAIT狀態(tài)等待2倍MSL,這樣可以保證本次連接的所有數(shù)據(jù)都從網(wǎng)絡(luò)中消失。

????3. time_wait狀態(tài)如何避免?

首先服務(wù)器可以設(shè)置SO_REUSEADDR套接字選項來通知內(nèi)核,如果端口忙,但TCP連接位于TIME_WAIT狀態(tài)時可以重用端口。在一個非常有用的場景就是,如果你的服務(wù)器程序停止后想立即重啟,而新的套接字依舊希望使用同一端口,此時SO_REUSEADDR選項就可以避免TIME_WAIT狀態(tài)。

例子

1.客戶端連接服務(wù)器的80服務(wù),這時客戶端會啟用一個本地的端口訪問服務(wù)器的80,訪問完成后關(guān)閉此連接,立刻再次訪問服務(wù)器的
80,這時客戶端會啟用另一個本地的端口,而不是剛才使用的那個本地端口。原因就是剛才的那個連接還處于TIME_WAIT狀態(tài)。

2.客戶端連接服務(wù)器的80服務(wù),這時服務(wù)器關(guān)閉80端口,立即再次重啟80端口的服務(wù),這時可能不會成功啟動,原因也是服務(wù)器的連
接還處于TIME_WAIT狀態(tài)。
實戰(zhàn)分析:

首先,根據(jù)一個查詢TCP連接數(shù),來說明這個問題。
netstat -ant|awk '/^tcp/ {++S[$NF]} END {for(a in S) print (a,S[a])}'
LAST_ACK 14 
SYN_RECV 348 
ESTABLISHED 70
 FIN_WAIT1 229
 FIN_WAIT2 30
 CLOSING 33 
TIME_WAIT 8

狀態(tài)描述:

CLOSED:無連接是活動的或正在進(jìn)行
LISTEN:服務(wù)器在等待進(jìn)入呼叫
SYN_RECV:一個連接請求已經(jīng)到達(dá),等待確認(rèn)
SYN_SENT:應(yīng)用已經(jīng)開始,打開一個連接
ESTABLISHED:正常數(shù)據(jù)傳輸狀態(tài)
FIN_WAIT1:應(yīng)用說它已經(jīng)完成
FIN_WAIT2:另一邊已同意釋放
ITMED_WAIT:等待所有分組死掉
CLOSING:兩邊同時嘗試關(guān)閉
TIME_WAIT:另一邊已初始化一個釋放
LAST_ACK:等待所有分組死掉</pre>

命令解釋:

先來看看netstat:
netstat -n
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 123.123.123.123:80 234.234.234.234:12345 TIME_WAIT
你實際執(zhí)行這條命令的時候,可能會得到成千上萬條類似上面的記錄,不過我們就拿其中的一條就足夠了。

再來看看awk: /^tcp/ 濾出tcp開頭的記錄,屏蔽udp, socket等無關(guān)記錄。
state[]相當(dāng)于定義了一個名叫state的數(shù)組
NF
表示記錄的字段數(shù),如上所示的記錄,NF等于6
$NF
表示某個字段的值,如上所示的記錄,$NF也就是$6,表示第6個字段的值,也就是TIME_WAIT
state[$NF]表示數(shù)組元素的值,如上所示的記錄,就是state[TIME_WAIT]狀態(tài)的連接數(shù) ++state[$NF]表示把某個數(shù)加一,如上所示的記錄,就是把state[TIME_WAIT]狀態(tài)的連接數(shù)加一
END
表示在最后階段要執(zhí)行的命令 for(key in state)

如何盡量處理TIMEWAIT過多?

編輯內(nèi)核文件/etc/sysctl.conf,加入以下內(nèi)容:
net.ipv4.tcp_syncookies = 1 表示開啟SYN Cookies。當(dāng)出現(xiàn)SYN等待隊列溢出時,啟用cookies來處理,可防范少量SYN攻擊,默認(rèn)為0,表示關(guān)閉;
net.ipv4.tcp_tw_reuse = 1 表示開啟重用。允許將TIME-WAIT sockets重新用于新的TCP連接,默認(rèn)為0,表示關(guān)閉;
net.ipv4.tcp_tw_recycle = 1 表示開啟TCP連接中TIME-WAIT sockets的快速回收,默認(rèn)為0,表示關(guān)閉。
net.ipv4.tcp_fin_timeout 修改系默認(rèn)的 TIMEOUT 時間</pre>

然后執(zhí)行 /sbin/sysctl -p 讓參數(shù)生效.
/etc/sysctl.conf是一個允許改變正在運(yùn)行中的Linux系統(tǒng)的接口,它包含一些TCP/IP堆棧和虛擬內(nèi)存系統(tǒng)的高級選項,修改內(nèi)核參數(shù)永久生效。

簡單來說,就是打開系統(tǒng)的TIMEWAIT重用和快速回收。

小結(jié)

? 本文主要講述了socket的主要api,以及tcp的連接過程和其中各個階段的連接狀態(tài),理解這些是更深入了解tcp的基礎(chǔ)!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容