tcp連線的建立與終止
理解TCP連線,需要首先記住以下幾點:
- TCP是雙向連線。兩個方向的連線可以獨立關閉。
- TCP是基於位元組流的連線。每個tcp socket在核心裡有接收緩衝區和傳送緩衝區。
- 應用程式只能操縱緩衝區資料,而不能干擾實際的資料傳送過程。應用程協議可能有自己的協議格式,但在TCP看來全是一個一個的位元組。
下面基於以上3點,談一下TCP連線的建立、資料傳輸和終止過程。
建立連線
正常的連線過程需要3路握手,不再贅述。這裡主要講一下連線失敗以及非同步連線的情況。
正常來講,在客戶端呼叫connect之前,服務端應該已經呼叫socket, listen建立好監聽套介面了。這樣客戶端connect應該可以很快返回。在連線建立完成後,服務端呼叫accept就可以立即返回,否則就阻塞到有客戶端連線進來。
如果服務端在相應埠上沒有監聽socket,那麼對於客戶端發過來的SYN,服務端就會發送RST,連線失敗;
如果客戶端connect之前設定socket為O_NONBLOCK,那麼呼叫connect立即返回,並設定errno為EINPROSESS。注意,這並不屬於錯誤,因為這時候核心在為你建立連線。當3步握手完成之後,再呼叫connect就成功返回了。一般在select/epoll裡面,需要監聽多個fd的時候,可以使用非阻塞connect,避免應用程式阻塞在建立連線上。
傳輸資料
傳送資料
write/send將資料寫入到傳送緩衝區。
阻塞型write/send會一直阻塞,直到所有的資料全部寫入到傳送緩衝區,並返回寫入的位元組數。
write的行為取決於是否被設定為O_NONBLOCK。
- 預設為阻塞型,那麼write會一直阻塞,直到指定的所有位元組全部寫入核心緩衝區,除非出錯;
- 如果設定為nonblock,那麼一次write只會寫入當前可以寫入的位元組數,並返回已寫入位元組數;如果緩衝區滿,那麼返回-1,並設定errno為EAGAIN。
我們知道tcp會對每個傳送的位元組進行確認,因此被對方ACK的資料就能從傳送緩衝區刪除,這樣緩衝區就又可寫了。
write返回,僅表示資料已經寫到傳送緩衝區。至於資料是否從網絡卡傳送出去了,以及對方是否能夠接收到資料,這些還都是未知數。TCP有流控制,每次傳送資料的大小,取決於對方ACK裡攜帶的視窗大小,也就是對方接收緩衝區的空閒大小。因此,如果對方應用程式一直不read,它的接收緩衝區就會一直處於飽和狀態,那麼傳送方就不能傳送新的資料。這樣,傳送方的write也就會阻塞。
但是,當對方連線關閉(對方程序死掉了,或者呼叫了close),那麼我方在傳送資料過去的時候就會收到對方發過來的RST。這時候,應用程式再呼叫write/send就會出錯,errno是EPIPE,並且會收到SIGPIPE訊號。此訊號預設會終止程序,因此應用程式不能通過 if (ret < 0) { perror(…);}的形式解析errno,除非捕獲SIGPIPE訊號。
此外,被中斷了,write也會出錯,errno為EINTR。此種情況下,一般是重新write一遍,因此需要特殊處理。
接收資料
接收資料,會將接收緩衝區的資料拷貝到使用者空間。
可以想象,如果接收緩衝區沒有資料,那麼read/recv就會阻塞。
對於接收方來說,一次完整的TCP連線,應該從SYN分節開始,以FIN分節結束。每次read都會返回從接收緩衝區拷貝的資料位元組數。可能大於0,也可能等於0(收到FIN)。
如果在read FIN之前連接出錯,那麼read就會返回錯誤,並可以從errno獲得錯誤資訊。例如,被中斷了(EINTR)。
注意,以上討論的都是阻塞型的socket,這也是預設的形式。如果設定socket為O_NONBLOCK,那麼緩衝區沒資料的話read會返回EWOULDBLOCK或EAGAIN。
對於接受到RST的TCP連線,如果已經收到FIN分節了,那麼read會返回0,而不是出錯。因為FIN已經表示對方所有的資料都被收到了,沒有理由返貨錯誤給應用程式。一般的,應用程式在read返回0時就應該自行處理善後工作了。