1. 程式人生 > >TCP建立連線的過程

TCP建立連線的過程

   在可靠的TCP網路通訊中,客戶端和伺服器端通訊建立連線的過程可簡單表述為三次握手(建立連線的階段)和四次揮手(釋放連線階段),下圖是這兩個階段的一個完整的表述:

 

其狀態圖可以表示為,

在TCP連線建立的時候,存在一個如下的有限狀態機:


       在狀態轉化圖中,其中客戶端的狀態轉移用帶箭頭的粗實線表示,伺服器端的狀態轉換用帶箭頭的粗虛線表示。帶箭頭的細線表示一些不常見的事件,如復位、同時開啟、同時關閉等。關於有限狀態圖可以參考部落格http://blog.csdn.net/lycb_gz/article/details/8515062,裡面的細節都將的很清楚;如果要深入理解TCP連線建立和釋放的過程就需要結合socket程式設計裡的connect(),socket(),bind(),listen(),send(),close()等函式。

從圖中看到,三次握手對應的Berkeley Socket API:connect, listen, accept   3個,connect用在客戶端,另外2個用在服務端。對於TCP/IP protocol stack來說,TCP層的tcp_in&tcp_out也參與這個過程。我們這裡只討論這3個應用層的API幹了什麼事情。

(1) connect()
傳送了一個SYN,收到Server的SYN+ACK後,代表連線完成。傳送最後一個ACK是protocol stack,tcp_out完成的。
(2)listen()
在server這端,準備了一個未完成的連線佇列,儲存只收到SYN_C的socket結構;還準備了已完成的連線佇列

,即儲存了收到了最後一個ACK的socket結構。
(3)accept()
應用程序呼叫accept的時候,就是去檢查上面說的已完成的連線佇列,如果佇列裡有連線,就返回這個連線;如果沒有,即空的,blocking方試呼叫,就睡眠等待;客戶端呼叫connect函式之後就發起完成TCP的三次握手,客戶端呼叫connect後,由核心中的TCP協議完成TCP的三次握手,close操作會完成四次揮手。

其中accept發生在三次握手之後。
第一次握手:客戶端傳送syn包(syn=j)到伺服器。
第二次握手:伺服器收到syn包,必須確認客戶的SYN(ack=j+1),同時自己也傳送一個ASK包(ask=k)。
第三次握手:客戶端收到伺服器的SYN+ACK包,向伺服器傳送確認包ACK(ack=k+1)。
三次握手完成後,客戶端和伺服器就建立了tcp連線。這時可以呼叫accept函式獲得此連線。


我們如何判斷有一個建立連結請求或一個關閉連結請求:
建立連結請求:
1、connect將完成三次握手,accept所監聽的fd上,產生讀事件,表示有新的連結請求,但此時accept函式並沒有呼叫,在核心中維持了一個完成連線的佇列;           
關閉連結請求:
1、close將完成四次揮手,如果有一方關閉sockfd,對方將感知到有讀事件,如果read讀取資料時,返回0,即讀取到0個數據,表示有斷開連結請求。(在作業系統中已經這麼定義) 關閉連結過程中的TCP狀態和SOCKET處理,及可能出現的問題:

1. TIME_WAIT
TIME_WAIT 是主動關閉 TCP 連線的那一方出現的狀態,系統會在 TIME_WAIT 狀態下等待 2MSL(maximum segment lifetime  )後才能釋放連線(埠)。通常約合 4 分鐘以內。TIME_WAIT 狀態等待 2MSL 的意義:
1、確保連線可靠地關閉; 即防止最後一個ACK丟失。
2、避免產生套接字混淆(同一個埠對應多個套接字)。
 為什麼說可以用來避免套接字混淆呢?一方close傳送了關閉連結請求,對方的應答遲遲到不了(例如網路原因),導致TIME_WAIT超時,此時這個埠又可用了,我們在這個埠上又建立了另外一個socket連結。 如果此時對方的應答到了,怎麼處理呢?其實這個在TCP層已經處理了,由於有TCP序列號,所以核心TCP層,就會將包丟掉,並給對方發包,讓對方將sockfd關閉。所以應用層是沒有關係的。即我們用socket API編寫程式,就不用處理。
注意:TIME_WAIT是指作業系統的定時器會等2MSL,而主動關閉sockfd的一方,並不會阻塞。(即應用程式在close時,並不會阻塞)。當主動方關閉sockfd後,對方可能不知道這個事件。那麼當對方(被動方)寫資料,即send時,將會產生錯誤,即errno為: ECONNRESET。伺服器產生大量 TIME_WAIT 的原因:(一般我們不這樣開發Server,但是web伺服器等這種多客戶端的Server,是需要在完成一次請求後,主動關閉連線的,否則可能因為控制代碼不夠用,而造成無法提供服務。)伺服器存在大量的主動關閉操作,需關注程式何時會執行主動關閉(如批量清理長期空閒的套接字等操作)。一般我們自己寫的伺服器進行主動斷開連線的不多,除非做了空閒超時之類的管理。(TCP短連結是指,客戶端傳送請求給伺服器,客戶端收到伺服器端的響應後,關閉連結)。

2. CLOSE_WAIT
CLOSE_WAIT 是被動關閉 TCP 連線時產生的,如果收到另一端關閉連線的請求後,本地(Server端)不關閉相應套接字就會導致本地套接字進入這一狀態。
(如果對方關閉了,沒有收到關閉連結請求,就是下面的不正常情況)按TCP狀態機,我方收到FIN,則由TCP實現傳送ACK,因此進入CLOSE_WAIT狀態。但如果我方不執行close(),就不能由CLOSE_WAIT遷移到LAST_ACK,則系統中會存在很多CLOSE_WAIT狀態的連線。如果存在大量的 CLOSE_WAIT,則說明客戶端併發量大,且伺服器未能正常感知客戶端的退出,也並未及時 close 這些套接字。(如果不及時處理,將會出現沒有可用的socket描述符的問題,原因是sockfd耗盡)。
正常情況下:一方關閉sockfd,另外一方將會有讀事件產生, 當recv資料時,如果返回值為0,表示對端已經關閉。此時我們應該呼叫close,將對應的sockfd也關閉掉。
不正常情況下:一方關閉sockfd,另外一方並不知道,(比如在close時,自己斷網了,對方就收不到傳送的資料包)。此時,如果另外一方在對應的sockfd上寫send或讀recv資料。
recv時,將會返回0,表示連結已經斷開。
send時, 將會產生錯誤,errno為ECONNRESET。

3.close()函式和shutdown()函式的區別:

首先我們來看看close()函式的原型:

標頭檔案:#include <unistd.h>
定義函式:int close(int fd);
close 一個套接字的預設行為是把套接字標記為已關閉,然後立即返回到呼叫程序,該套接字描述符不能再由呼叫程序使用,也就是說它不能再作為read或write的第一個引數,然而TCP將嘗試傳送已排隊等待發送到對端的任何資料,傳送完畢後發生的是正常的TCP連線終止序列。在多程序併發伺服器中,父子程序共享著套接字,套接字描述符引用計數記錄著共享著的程序個數,當父程序或某一子程序close掉套接字時,描述符引用計數會相應的減一,當引用計數仍大於零時,這個close呼叫就不會引發TCP的四路握手斷連過程。如果是主動呼叫close()則會發起核心TCP協議的四次揮手,斷開連線.

再來看看shutdown()函式的原型:

int shutdown(int sockfd,int howto);  //返回成功為0,出錯為-1. 
該函式的行為依賴於howto的值
1.SHUT_RD:值為0,關閉連線的讀這一半。
2.SHUT_WR:值為1,關閉連線的寫這一半。
3.SHUT_RDWR:值為2,連線的讀和寫都關閉。
終止網路連線的通用方法是呼叫close函式。但使用shutdown能更好的控制斷連過程(使用第二個引數)。
當呼叫SHUT_RD的時候,套接字sockfd的讀端將會關閉,不能呼叫接受資料的函式,這對於協議層沒有影響。然和當前在sockfd讀端的資料緩衝區的資料都會被捨棄掉,程序將不能對該套接字發起讀操作,對TCP套接字呼叫SHUT_RD將會導致協議層將接收到的資料無聲的丟掉!!!如果想要繼續接受資料都要重置連結;
當呼叫SHUT_WR的時候,對於tcp套接字來說,這意味著會在所有資料傳送出並得到接受端確認後產生一個FIN包。而此時套接字的狀態會由ESTABLISHED變成FIN_WAIT_1,然後對方傳送一個 ACK包作為迴應,套接字又變成FIN_WAIT_2。如果對方也關閉了連線則對方會發出FIN,我方會迴應一個ACK並將套接字置為 TIME_WAIT。

4.如何判斷socket連線斷開:

非阻塞模式,如果暫時沒有資料,返回的值也會是<=0的,如果用阻塞模式的話,返回<=0的值是可以認為socket已經無效了。當使用 select()函式測試一個socket是否可讀時,如果select()函式返回值為1,且使用recv()函式讀取的資料長度為0 時,就說明該socket已經斷開。經過程式碼試驗,如果程序受到一些訊號時,例如:EINTR,recv()返回值小於等於0時,這是就需要判斷 errno是否等於 EINTR , 如果errno == EINTR 則說明recv函式是由於程式接收到訊號後返回的,socket連線還是正常的,不應close掉socket連線。如果write,我覺得還有一些情況需要考慮,那就是寫的太快的時候,有可能buffer寫滿了,errno是EAGAIN,可以根據實際需要,如果errno是EAGAIN的話,再寫幾次。