TCP協(xié)議結(jié)構(gòu)
里面主要部分是包序號(hào)和確認(rèn)序號(hào)乒省,包序號(hào)是為了解決亂序的問題巧颈,知道那個(gè)包在前,哪個(gè)包在后袖扛。確認(rèn)序號(hào)砸泛,發(fā)出去的包要有個(gè)確認(rèn),如果沒有收到就要重新發(fā)送蛆封。
狀態(tài)位:
- SYN:發(fā)起一個(gè)連接
- ACK:回復(fù)
- RST: 重新連接
- FIN:結(jié)束連接
窗口大小唇礁,TCP要做流量控制,通信雙方各聲明一個(gè)窗口惨篱,標(biāo)識(shí)自己的處理能力盏筐,讓對(duì)端別發(fā)送太快,撐死我砸讳,也別發(fā)送太慢琢融,餓死我
TCP的3次握手
TCP建立連接時(shí)的3次握手也常稱為“請(qǐng)求->應(yīng)答->應(yīng)答之應(yīng)答”。為什么是3次呢簿寂,因?yàn)榻ㄟB接的雙方都要做一個(gè)發(fā)送和收到確認(rèn)才能證明雙方連接建立了漾抬。
3次握手除了建立連接之外,還為了溝通意見事情常遂,就是TCP包序號(hào)的問題纳令,這也是SYN的由來,全稱Synchronize Sequence Number。雙方要確定一個(gè)起始的序號(hào)平绩,序號(hào)不能每次都從1開始圈匆,因?yàn)閺?開始如果掉線又重連,上次的包可能還在路上捏雌,那序號(hào)就重復(fù)了跃赚。所以每個(gè)連接都要有個(gè)不同的起始序號(hào)。這個(gè)起始序號(hào)是隨著時(shí)間變化的腹忽,可以看作一個(gè)32位的計(jì)數(shù)器来累,每4ms加1砚作,這樣到兩個(gè)包重復(fù)窘奏,需要4個(gè)小時(shí),早就超過上一個(gè)的生存時(shí)間了(TTL)葫录。
Sequence Number如何增加
sequence number不是每次加1的着裹,而是和傳輸?shù)淖止?jié)數(shù)有關(guān)。如果雙方3次握手后seq都是1米同,這時(shí)候A給B發(fā)了一個(gè)長度1440的包骇扇,那seq就變成1440,。B給1440的ACK回1441面粮,證明1440收到了少孝。
狀態(tài)演變
Server端監(jiān)聽某個(gè)端口,處于LISTEN狀態(tài)熬苍∩宰撸客戶端主動(dòng)發(fā)起連接發(fā)送SYN,之后處于SYN-SENT狀態(tài)柴底。服務(wù)端收到SYN之后婿脸,向客戶端發(fā)送SYN和ACK,之后服務(wù)端處于SYN-RCVD狀態(tài)柄驻『鳎客戶端收到服務(wù)端的反饋后再發(fā)送ACK,之后客戶端處于ESTABLISHED狀態(tài)鸿脓,服務(wù)端收到后抑钟,也處于ESTABLISHED狀態(tài)。
這里有一個(gè)問題野哭,如果client發(fā)送SYN之后掉線了在塔,服務(wù)端發(fā)送SYN-ACK后會(huì)一直保持住資源,等待客戶端回來的ACK虐拓。如果沒收到心俗,就會(huì)重試SYN-ACK,默認(rèn)重試5次,間隔從1s開始城榛,每次翻倍揪利。所以,總共需要1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s狠持,Server端才會(huì)結(jié)束疟位,釋放資源。
SYN flood攻擊
client利用server端會(huì)等待的特性(63s),不斷給server發(fā)SYN喘垂,發(fā)完就下線甜刻,把服務(wù)端的SYN隊(duì)列耗盡,讓正常的鏈接請(qǐng)求不能處理正勒。linux處理這個(gè)的方法是設(shè)置tcp_syncookies參數(shù)得院,當(dāng)SYN隊(duì)列滿了以后,TCP發(fā)SYN-ACK的時(shí)候會(huì)帶上cookie章贞,然后正常的client再ACK的時(shí)候帶上就可以建立鏈接祥绞,但是這種方式不是常規(guī)用法。更通用的方法是調(diào)整幾個(gè)參數(shù)鸭限,第一個(gè)是
tcp_synack_retries
,用來減少Server端SYN-ACK重試次數(shù)蜕径;第二個(gè)是tcp_max_syn_backlog
,增大隊(duì)列長度败京;第三個(gè)是tcp_abort_on_overflow
兜喻,處理不過來干脆拒絕連接。
TCP的四次揮手
一方發(fā)起關(guān)閉請(qǐng)求
發(fā)起端(A)想斷開連接的時(shí)候赡麦,發(fā)送FIN朴皆,然后進(jìn)入FIN_WAIT_1的狀態(tài)。接收端(B)收到后隧甚,發(fā)送ACK车荔,表示自己知道了,進(jìn)入CLOSE_WAIT狀態(tài)戚扳。
A收到B的確認(rèn)后進(jìn)入FIN_WAIT_2的狀態(tài)忧便,如果這個(gè)時(shí)候B跑路,A將永遠(yuǎn)處于這個(gè)狀態(tài)帽借。linux針對(duì)這種情況珠增,支持tcp_fin_timeout參數(shù),超時(shí)后A就算B跑路了砍艾。
如果B沒跑路蒂教,會(huì)再發(fā)FIN和ACK,表示我也不玩了脆荷,然后B進(jìn)入LAST-ACK狀態(tài)凝垛。A收到后回復(fù)ACK懊悯,進(jìn)入TIME-WAIT狀態(tài)。而B收到A的ACK后進(jìn)入CLOSED狀態(tài)梦皮。A等待一段時(shí)間也會(huì)進(jìn)入CLOSE就徹底結(jié)束了炭分。
A為什么要等一段時(shí)間呢,一是為了防止A最后的ACK B沒收到剑肯,B會(huì)要求重發(fā)捧毛,如果A直接CLOSE,就不會(huì)重發(fā)了。二是B在發(fā)送最后的FIN和ACK之前的數(shù)據(jù)包可能比這兩個(gè)包晚到让网。所以A要等一段再關(guān)閉呀忧。
A的TIME_WAIT的時(shí)間是2個(gè)MSL(Max Segment Lifetime)報(bào)文最大生存時(shí)間。
還有一種情況是B一直沒收到最后的ACK溃睹,于是要求重發(fā)而账,而A已經(jīng)過了2MSL了,A會(huì)回一個(gè)RST表示自己跑路了丸凭。
雙方都發(fā)起斷鏈請(qǐng)求
因?yàn)門CP是全雙工的福扬,所以,發(fā)送方和接收方都需要Fin和Ack惜犀。只不過,有一方是被動(dòng)的狠裹,所以看上去就成了所謂的4次揮手虽界。如果兩邊同時(shí)斷連接,那就會(huì)就進(jìn)入到CLOSING狀態(tài)涛菠,然后到達(dá)TIME_WAIT狀態(tài)莉御。
TCP狀態(tài)機(jī)
擴(kuò)展問題思考
日常維護(hù)中查看服務(wù)器所有連接的狀態(tài)
$netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
#結(jié)果實(shí)例
TIME_WAIT 814
CLOSE_WAIT 1
FIN_WAIT1 1
ESTABLISHED 634
SYN_RECV 2
LAST_ACK 1
如果web服務(wù)器連接出異常,百分之八九十都是下面兩種情況:
- 服務(wù)器保持了大量TIME_WAIT狀態(tài)
- 服務(wù)器保持了大量CLOSE_WAIT狀態(tài)
這個(gè)時(shí)候新的請(qǐng)求就無法被處理了俗冻,接著就是大量Too Many Open Files異常
服務(wù)器保持了大量TIME_WAIT怎么解決
這個(gè)是服務(wù)器主動(dòng)發(fā)起關(guān)閉連接引起礁叔,所以要調(diào)整的是本端服務(wù)器的參數(shù),修改/etc/sysctl.conf
:
#對(duì)于一個(gè)新建連接迄薄,內(nèi)核要發(fā)送多少個(gè) SYN 連接請(qǐng)求才決定放棄,不應(yīng)該大于255琅关,默認(rèn)值是5,對(duì)應(yīng)于180秒左右時(shí)間
net.ipv4.tcp_syn_retries=2
#net.ipv4.tcp_synack_retries=2
#表示當(dāng)keepalive起用的時(shí)候讥蔽,TCP發(fā)送keepalive消息的頻度涣易。缺省是2小時(shí),改為300秒
net.ipv4.tcp_keepalive_time=1200
net.ipv4.tcp_orphan_retries=3
#表示如果套接字由本端要求關(guān)閉冶伞,這個(gè)參數(shù)決定了它保持在FIN-WAIT-2狀態(tài)的時(shí)間
net.ipv4.tcp_fin_timeout=30
#表示SYN隊(duì)列的長度新症,默認(rèn)為1024,加大隊(duì)列長度為8192响禽,可以容納更多等待連接的網(wǎng)絡(luò)連接數(shù)徒爹。
net.ipv4.tcp_max_syn_backlog = 4096
#表示開啟SYN Cookies荚醒。當(dāng)出現(xiàn)SYN等待隊(duì)列溢出時(shí),啟用cookies來處理隆嗅,可防范少量SYN攻擊腌且,默認(rèn)為0,表示關(guān)閉
net.ipv4.tcp_syncookies = 1
#表示開啟重用榛瓮。允許將TIME-WAIT sockets重新用于新的TCP連接铺董,默認(rèn)為0,表示關(guān)閉
net.ipv4.tcp_tw_reuse = 1
#表示開啟TCP連接中TIME-WAIT sockets的快速回收禀晓,默認(rèn)為0精续,表示關(guān)閉
net.ipv4.tcp_tw_recycle = 1
##減少超時(shí)前的探測次數(shù)
net.ipv4.tcp_keepalive_probes=5
##優(yōu)化網(wǎng)絡(luò)設(shè)備接收隊(duì)列
net.core.netdev_max_backlog=3000
這里頭主要注意到的是net.ipv4.tcp_tw_reuse
net.ipv4.tcp_tw_recycle
net.ipv4.tcp_fin_timeout
net.ipv4.tcp_keepalive_*
這幾個(gè)參數(shù)。
net.ipv4.tcp_tw_reuse和net.ipv4.tcp_tw_recycle的開啟都是為了回收處于TIME_WAIT狀態(tài)的資源粹懒。
net.ipv4.tcp_fin_timeout這個(gè)時(shí)間可以減少在異常情況下服務(wù)器從FIN-WAIT-2轉(zhuǎn)到TIME_WAIT的時(shí)間重付。
net.ipv4.tcp_keepalive_*一系列參數(shù),是用來設(shè)置服務(wù)器檢測連接存活的相關(guān)配置凫乖。
注意tcp_tw_reuse确垫,tcp_tw_recycle開啟的風(fēng)險(xiǎn),用這個(gè)是非常危險(xiǎn)的:http://blog.csdn.net/wireless_tech/article/details/6405755
所以設(shè)置keepAlive是非常重要的帽芽。
服務(wù)器保持了大量TIME_WAIT怎么解決
如果一直保持在CLOSE_WAIT狀態(tài)删掀,那么只有一種情況,就是在對(duì)方關(guān)閉連接之后服務(wù)器程序自己沒有進(jìn)一步發(fā)出ack信號(hào)导街。換句話說披泪,就是在對(duì)方連接關(guān)閉之后,程序里沒有檢測到搬瑰,或者程序壓根就忘記了這個(gè)時(shí)候需要關(guān)閉連接款票,于是這個(gè)資源就一直被程序占著。
如果你使用的是HttpClient并且你遇到了大量CLOSE_WAIT的情況泽论,那么這篇日志也許對(duì)你有用:http://blog.csdn.net/shootyou/article/details/6615051
在那邊日志里頭我舉了個(gè)場景艾少,來說明CLOSE_WAIT和TIME_WAIT的區(qū)別,這里重新描述一下:
服務(wù)器A是一臺(tái)爬蟲服務(wù)器翼悴,它使用簡單的HttpClient去請(qǐng)求資源服務(wù)器B上面的apache獲取文件資源缚够,正常情況下,如果請(qǐng)求成功抄瓦,那么在抓取完資源后潮瓶,服務(wù)器A會(huì)主動(dòng)發(fā)出關(guān)閉連接的請(qǐng)求,這個(gè)時(shí)候就是主動(dòng)關(guān)閉連接钙姊,服務(wù)器A的連接狀態(tài)我們可以看到是TIME_WAIT毯辅。如果一旦發(fā)生異常呢?假設(shè)請(qǐng)求的資源服務(wù)器B上并不存在煞额,那么這個(gè)時(shí)候就會(huì)由服務(wù)器B發(fā)出關(guān)閉連接的請(qǐng)求思恐,服務(wù)器A就是被動(dòng)的關(guān)閉了連接沾谜,如果服務(wù)器A被動(dòng)關(guān)閉連接之后程序員忘了讓HttpClient釋放連接,那就會(huì)造成CLOSE_WAIT的狀態(tài)了胀莹。
所以如果將大量CLOSE_WAIT的解決辦法總結(jié)為一句話那就是:查代碼基跑。因?yàn)閱栴}出在服務(wù)器程序里頭啊。
參考文章:
[極客時(shí)間] 趣談網(wǎng)絡(luò)協(xié)議 --劉超
[CSDN] 再談應(yīng)用環(huán)境下的TIME_WAIT和CLOSE_WAIT