
1、建立連接協議(三次握手)(1)客戶端發送一個帶SYN標志的TCP報文到服務器。這是三次握手過程中的報文1。(2) 服務器端回應客戶端的,這是三次握手中的第2個報文,這個報文同時帶ACK標志和SYN標志。因此它表示對剛才客戶端SYN報文的回應;同時又標志SYN給客戶端,詢問客戶端是否準備好進行數據通訊。(3) 客戶必須再次回應服務段一個ACK報文,這是報文段3。2、連接終止協議(四次握手) 由于TCP連接是全雙工的,因此每個方向都必須單獨進行關閉。這原則是當一方完成它的數據發送任務后就能發送一個FIN來終止這個方向的連接。收到一個 FIN只意味著這一方向上沒有數據流動,一個TCP連接在收到一個FIN后仍能發送數據。首先進行關閉的一方將執行主動關閉,而另一方執行被動關閉。 (1) TCP客戶端發送一個FIN,用來關閉客戶到服務器的數據傳送(報文段4)。 (2) 服務器收到這個FIN,它發回一個ACK,確認序號為收到的序號加1(報文段5)。和SYN一樣,一個FIN將占用一個序號。 (3) 服務器關閉客戶端的連接,發送一個FIN給客戶端(報文段6)。 (4) 客戶段發回ACK報文確認,并將確認序號設置為收到序號加1(報文段7)。CLOSED: 這個沒什么好說的了,表示初始狀態。LISTEN: 這個也是非常容易理解的一個狀態,表示服務器端的某個SOCKET處于監瑞腦消金獸聽狀態,可以接受連接了。SYN_RCVD: 這個狀態表示接受到了SYN報文,在正常情況下,這個狀態是服務器端的SOCKET在建立TCP連接時的三次握手會話過程中的一個中間狀態,很短暫,基本上用netstat你是很難看到這種狀態的,除非你特意寫了一個客戶端測試程序,故意將三次TCP握手過程中最后一個ACK報文不予發送。因此這種狀態時,當收到客戶端的ACK報文后,它會進入到ESTABLISHED狀態。SYN_SENT: 這個狀態與SYN_RCVD遙想呼應,當客戶端SOCKET執行CONNECT連接時,它首先發送SYN報文,因此也隨即它會進入到了SYN_SENT狀態,并等待服務端的發送三次握手中的第2個報文。SYN_SENT狀態表示客戶端已發送SYN報文。ESTABLISHED:這個容易理解了,表示連接已經建立了。FIN_WAIT_1: 這個狀態要好好解釋一下,其實FIN_WAIT_1和FIN_WAIT_2狀態的真正含義都是表示等待對方的FIN報文。而這兩種狀態的區別是:FIN_WAIT_1狀態實際上是當SOCKET在ESTABLISHED狀態時,它想主動關閉連接,向對方發送了FIN報文,此時該SOCKET即進入到FIN_WAIT_1狀態。而當對方回應ACK報文后,則進入到FIN_WAIT_2狀態,當然在實際的正常情況下,無論對方何種情況下,都應該馬上回應ACK報文,所以FIN_WAIT_1狀態一般是比較難見到的,而FIN_WAIT_2狀態還有時常常可以用netstat看到。FIN_WAIT_2:上面已經詳細解釋了這種狀態,實際上FIN_WAIT_2狀態下的SOCKET,表示半連接,也即有一方要求close連接,但另外還告訴對方,我暫時還有點數據需要傳送給你,稍后再關閉連接。TIME_WAIT: 表示收到了對方的FIN報文,并發送出了ACK報文,就等2MSL后即可回到CLOSED可用狀態了。如果FIN_WAIT_1狀態下,收到了對方同時帶FIN標志和ACK標志的報文時,可以直接進入到TIME_WAIT狀態,而無須經過FIN_WAIT_2狀態。CLOSING: 這種狀態比較特殊,實際情況中應該是很少見,屬于一種比較罕見的例外狀態。正常情況下,當你發送FIN報文后,按理來說是應該先收到(或同時收到)對方的ACK報文,再收到對方的FIN報文。但是CLOSING狀態表示你發送FIN報文后,并沒有收到對方的ACK報文,反而卻也收到了對方的FIN報文。什么情況下會出現此種情況呢?其實細想一下,也不難得出結論:那就是如果雙方幾乎在同時close一個SOCKET的話,那么就出現了雙方同時發送FIN報文的情況,也即會出現CLOSING狀態,表示雙方都正在關閉SOCKET連接。CLOSE_WAIT: 這種狀態的含義其實是表示在等待關閉。怎么理解呢?當對方close一個SOCKET后發送FIN報文給自己,你系統毫無疑問地會回應一個ACK報文給對方,此時則進入到CLOSE_WAIT狀態。接下來呢,實際上你真正需要考慮的事情是察看你是否還有數據發送給對方,如果沒有的話,那么你也就可以close這個SOCKET,發送FIN報文給對方,也即關閉連接。所以你在CLOSE_WAIT狀態下,需要完成的事情是等待你去關閉連接。LAST_ACK: 這個狀態還是比較容易好理解的,它是被動關閉一方在發送FIN報文后,最后等待對方的ACK報文。當收到ACK報文后,也即可以進入到CLOSED可用狀態了。最后有2個問題的回答,我自己分析后的結論(不一定保證100%正確)1、 為什么建立連接協議是三次握手,而關閉連接卻是四次握手呢?這是因為服務端的LISTEN狀態下的SOCKET當收到SYN報文的建連請求后,它可以把ACK和SYN(ACK起應答作用,而SYN起同步作用)放在一個報文里來發送。但關閉連接時,當收到對方的FIN報文通知時,它僅僅表示對方沒有數據發送給你了;但未必你所有的數據都全部發送給對方了,所以你可以未必會馬上會關閉SOCKET,也即你可能還需要發送一些數據給對方之后,再發送FIN報文給對方來表示你同意現在可以關閉連接了,所以它這里的ACK報文和FIN報文多數情況下都是分開發送的。2、 為什么TIME_WAIT狀態還需要等2MSL后才能返回到CLOSED狀態?這是因為:雖然雙方都同意關閉連接了,而且握手的4個報文也都協調和發送完畢,按理可以直接回到CLOSED狀態(就好比從SYN_SEND狀態到ESTABLISH狀態那樣);但是因為我們必須要假想網絡是不可靠的,你無法保證你最后發送的ACK報文會一定被對方收到,因此對方處于LAST_ACK狀態下的SOCKET可能會因為超時未收到ACK報文,而重發FIN報文,所以這個TIME_WAIT狀態的作用就是用來重發可能丟失的ACK報文。

TCP是一個面向連接的協議,所以在連接雙方發送數據之前,都需要首先建立一條連接。這和前面講到的協議完全不同。前面講的所有協議都只是發送數據而已,大多數都不關心發送的數據是不是送到,UDP尤其明顯,從編程的角度來說,UDP編程也要簡單的多----UDP都不用考慮數據分片。
書中用telnet登陸退出來解釋TCP協議連接的建立和中止的過程,可以看到,TCP連接的建立可以簡單的稱為三次握手,而連接的中止則可以叫做四次握手。
1.連接的建立在建立連接的時候,客戶端首先向服務器申請打開某一個端口(用SYN段等于1的TCP報文),然后服務器端發回一個ACK報文通知客戶端請求報文收到,客戶端收到確認報文以后再次發出確認報文確認剛才服務器端發出的確認報文(繞口么),至此,連接的建立完成。這就叫做三次握手。如果打算讓雙方都做好準備的話,一定要發送三次報文,而且只需要三次報文就可以了。
可以想見,如果再加上TCP的超時重傳機制,那么TCP就完全可以保證一個數據包被送到目的地。
2.結束連接TCP有一個特別的概念叫做half-close,這個概念是說,TCP的連接是全雙工(可以同時發送和接收)連接,因此在關閉連接的時候,必須關閉傳和送兩個方向上的連接。客戶機給服務器一個FIN為1的TCP報文,然后服務器返回給客戶端一個確認ACK報文,并且發送一個FIN報文,當客戶機回復ACK報文后(四次握手),連接就結束了。
3.最大報文長度在建立連接的時候,通信的雙方要互相確認對方的最大報文長度(MSS),以便通信。一般這個SYN長度是MTU減去固定IP首部和TCP首部長度。對于一個以太網,一般可以達到1460字節。當然如果對于非本地的IP,這個MSS可能就只有536字節,而且,如果中間的傳輸網絡的MSS更佳的小的話,這個值還會變得更小。
4.TCP的狀態遷移圖書P182頁給出了TCP的狀態圖,這是一個看起來比較復雜的狀態遷移圖,因為它包含了兩個部分---服務器的狀態遷移和客戶端的狀態遷移,如果從某一個角度出發來看這個圖,就會清晰許多,這里面的服務器和客戶端都不是絕對的,發送數據的就是客戶端,接受數據的就是服務器。
4.1.客戶端應用程序的狀態遷移圖客戶端的狀態可以用如下的流程來表示:
CLOSED->SYN_SENT->ESTABLISHED->FIN_WAIT_1->FIN_WAIT_2->TIME_WAIT->CLOSED
以上流程是在程序正常的情況下應該有的流程,從書中的圖中可以看到,在建立連接時,當客戶端收到SYN報文的ACK以后,客戶端就打開了數據交互地連接。而結束連接則通常是客戶端主動結束的,客戶端結束應用程序以后,需要經歷FIN_WAIT_1,FIN_WAIT_2等狀態,這些狀態的遷移就是前面提到的結束連接的四次握手。
4.2.服務器的狀態遷移圖服務器的狀態可以用如下的流程來表示:
CLOSED->LISTEN->SYN收到->ESTABLISHED->CLOSE_WAIT->LAST_ACK->CLOSED
在建立連接的時候,服務器端是在第三次握手之后才進入數據交互狀態,而關閉連接則是在關閉連接的第二次握手以后(注意不是第四次)。而關閉以后還要等待客戶端給出最后的ACK包才能進入初始的狀態。
4.3.其他狀態遷移書中的圖還有一些其他的狀態遷移,這些狀態遷移針對服務器和客戶端兩方面的總結如下
書中給的圖里面,有一個TIME_WAIT等待狀態,這個狀態又叫做2MSL狀態,說的是在TIME_WAIT2發送了最后一個ACK數據報以后,要進入TIME_WAIT狀態,這個狀態是防止最后一次握手的數據報沒有傳送到對方那里而準備的(注意這不是四次握手,這是第四次握手的保險狀態)。這個狀態在很大程度上保證了雙方都可以正常結束,但是,問題也來了。
由于插口的2MSL狀態(插口是IP和端口對的意思,socket),使得應用程序在2MSL時間內是無法再次使用同一個插口的,對于客戶程序還好一些,但是對于服務程序,例如httpd,它總是要使用同一個端口來進行服務,而在2MSL時間內,啟動httpd就會出現錯誤(插口被使用)。為了避免這個錯誤,服務器給出了一個平靜時間的概念,這是說在2MSL時間內,雖然可以重新啟動服務器,但是這個服務器還是要平靜的等待2MSL時間的過去才能進行下一次連接。
4.5.FIN_WAIT_2狀態這就是著名的半關閉的狀態了,這是在關閉連接時,客戶端和服務器兩次握手之后的狀態。在這個狀態下,應用程序還有接受數據的能力,但是已經無法發送數據,但是也有一種可能是,客戶端一直處于FIN_WAIT_2狀態,而服務器則一直處于WAIT_CLOSE狀態,而直到應用層來決定關閉這個狀態。
5.RST,同時打開和同時關閉RST是另一種關閉連接的方式,應用程序應該可以判斷RST包的真實性,即是否為異常中止。而同時打開和同時關閉則是兩種特殊的TCP狀態,發生的概率很小。
6.TCP服務器設計前面曾經講述過UDP的服務器設計,可以發現UDP的服務器完全不需要所謂的并發機制,它只要建立一個數據輸入隊列就可以。但是TCP不同,TCP服務器對于每一個連接都需要建立一個獨立的進程(或者是輕量級的,線程),來保證對話的獨立性。所以TCP服務器是并發的。而且TCP還需要配備一個呼入連接請求隊列(UDP服務器也同樣不需要),來為每一個連接請求建立對話進程,這也就是為什么各種TCP服務器都有一個最大連接數的原因。而根據源主機的IP和端口號碼,服務器可以很輕松的區別出不同的會話,來進行數據的分發。
執行以下命令:
#netstat -n | awk ‘/^tcp/ {++state[$NF]} END {for(key in state) PRint key."/t".state[key]}’
會得到類似下面的結果,具體數字會有所不同:
FIN_WAIT_1 286
FIN_WAIT_2 960
SYN_SENT 3
LAST_ACK 32
CLOSING 1
CLOSED 36
SYN_RCVD 144
TIME_WAIT 2520
ESTABLISHED 352 #差不多等于連接的并發數
這條命令可以把當前系統的網絡連接狀態分類匯總。
這條語句是在張宴那邊看到,據說是從新浪互動社區事業部技術總監王老大那兒獲得的,非常不錯。
返回參數的說明如下:
SYN_RECV表示正在等待處理的請求數;
ESTABLISHED表示正常數據傳輸狀態;
TIME_WAIT表示處理完畢,等待超時結束的請求數。
------------------------------------------------------------------再來看看awk:/^tcp/濾出tcp開頭的記錄,屏蔽udp, socket等無關記錄。state[]相當于定義了一個名叫state的數組NF表示記錄的字段數,如上所示的記錄,NF等于6$NF表示某個字段的值,如上所示的記錄,$NF也就是$6,表示第6個字段的值,也就是TIME_WAITstate[$NF]表示數組元素的值,如上所示的記錄,就是state[TIME_WAIT]狀態的連接數++state[$NF]表示把某個數加一,如上所示的記錄,就是把state[TIME_WAIT]狀態的連接數加一END表示在最后階段要執行的命令for(key in state)遍歷數組print key,"/t",state[key]打印數組的鍵和值,中間用/t制表符分割,美化一下。
CLOSE_WAIT狀態原因以及處理辦法:
在被動關閉連接情況下,在已經接收到FIN,但是還沒有發送自己的FIN的時刻,連接處于CLOSE_WAIT狀態。通常來講,CLOSE_WAIT狀態的持續時間應該很短,正如SYN_RCVD狀態。但是在一些特殊情況下,就會出現連接長時間處于CLOSE_WAIT狀態的情況。出現大量close_wait的現象,主要原因是某種情況下對方關閉了socket鏈接,但是我方忙與讀或者寫,沒有關閉連接。
CLOSE_WAIT會維持最長會維持2個小時的時間,如果系統有太多CLOSE_WAIT狀態,可能導致“Too many open files”
CLOSE_WAIT首先需要排除應用程序原因,其次可以通過修改內核縮短CLOSE_WAIT的時間:
net.ipv4.tcp_keepalive_time = 1800 net.ipv4.tcp_keepalive_probes = 3 net.ipv4.tcp_keepalive_intvl = 15
TIME_WAIT狀態原因以及處理辦法:
這種情況比較常見,一些爬蟲服務器或者WEB服務器(如果網管在安裝的時候沒有做內核參數優化的話)上經常會遇到這個問題,這個問題是怎么產生的呢?
從 上面的示意圖可以看得出來,TIME_WAIT是主動關閉連接的一方保持的狀態,對于爬蟲服務器來說他本身就是“客戶端”,在完成一個爬取任務之后,他就 會發起主動關閉連接,從而進入TIME_WAIT的狀態,然后在保持這個狀態2MSL(max segment lifetime)時間之后,徹底關閉回收資源。為什么要這么做?明明就已經主動關閉連接了為啥還要保持資源一段時間呢?這個是TCP/IP的設計者規定 的,主要出于以下兩個方面的考慮:
1.防止上一次連接中的包,迷路后重新出現,影響新連接(經過2MSL,上一次連接中所有的重復包都會消失)2. 可靠的關閉TCP連接。在主動關閉方發送的最后一個 ack(fin) ,有可能丟失,這時被動方會重新發fin, 如果這時主動方處于 CLOSED 狀態 ,就會響應 rst 而不是 ack。所以主動方要處于 TIME_WAIT 狀態,而不能是 CLOSED 。另外這么設計TIME_WAIT 會定時的回收資源,并不會占用很大資源的,除非短時間內接受大量請求或者受到攻擊。
值 得一說的是,對于基于TCP的HTTP協議,關閉TCP連接的是Server端,這樣,Server端會進入TIME_WAIT狀態,可 想而知,對于訪 問量大的Web Server,會存在大量的TIME_WAIT狀態,假如server一秒鐘接收1000個請求,那么就會積壓 240*1000=240,000個 TIME_WAIT的記錄,維護這些狀態給Server帶來負擔。當然現代操作系統都會用快速的查找算法來管理這些 TIME_WAIT,所以對于新的 TCP連接請求,判斷是否hit中一個TIME_WAIT不會太費時間,但是有這么多狀態要維護總是不好。 HTTP協議1.1版規定default行為是Keep-Alive,也就是會重用TCP連接傳輸多個 request/response,一個主要原因就是發現了這個問題。
也就是說HTTP的交互跟上面畫的那個圖是不一樣的,關閉連接的不是客戶端,而是服務器,所以web服務器也是會出現大量的TIME_WAIT的情況的。現在來說如何來解決這個問題。解決思路很簡單,就是讓服務器能夠快速回收和重用那些TIME_WAIT的資源。下面來看一下我們網管對/etc/sysctl.conf文件的修改:#對于一個新建連接,內核要發送多少個 SYN 連接請求才決定放棄,不應該大于255,默認值是5,對應于180秒左右時間 net.ipv4.tcp_syn_retries=2 #net.ipv4.tcp_synack_retries=2 #表示當keepalive起用的時候,TCP發送keepalive消息的頻度。缺省是2小時,改為300秒 net.ipv4.tcp_keepalive_time=1200 net.ipv4.tcp_orphan_retries=3 #表示如果套接字由本端要求關閉,這個參數決定了它保持在FIN-WAIT-2狀態的時間 net.ipv4.tcp_fin_timeout=30 #表示SYN隊列的長度,默認為1024,加大隊列長度為8192,可以容納更多等待連接的網絡連接數。 net.ipv4.tcp_max_syn_backlog = 4096 #表示開啟SYN Cookies。當出現SYN等待隊列溢出時,啟用cookies來處理,可防范少量SYN攻擊,默認為0,表示關閉 net.ipv4.tcp_syncookies = 1 #表示開啟重用。允許將TIME-WAIT sockets重新用于新的TCP連接,默認為0,表示關閉 net.ipv4.tcp_tw_reuse = 1 #表示開啟TCP連接中TIME-WAIT sockets的快速回收,默認為0,表示關閉 net.ipv4.tcp_tw_recycle = 1 ##減少超時前的探測次數 net.ipv4.tcp_keepalive_probes=5 ##優化網絡設備接收隊列 net.core.netdev_max_backlog=3000
新聞熱點
疑難解答