1. TCP建立串連(三向交握)
下面兩個圖是從協議和介面兩個角度來解釋TCP的三向交握過程(分別摘自電腦網路-謝希仁和UNIX網路編程卷1):
1.1. tcpdump三向交握
下面通過tcpdump來查看tcp建立串連的過程:用nc命令來進行測試,
server:nc –l 9000client:nc 9000[anonymalias@qcloud ~]$sudo tcpdump -i lo port 9000 -X –S
tcpdump預設情況下,只會在SYN報文段中顯示絕對的sequence number, 其他報文段只會顯示相對的sequence number,所以需要加上-S才能在握手以外的階段顯示絕對sequence number。-X參數用於解析和顯示每一個包的包頭和包體。這裡只顯示IP header + TCP packet, 不顯示鏈路層link level的頭部。
圖中第一個tcp建立串連的請求包中6個控制位欄位:URG|ACK|PSH|RST|SYN|FIN,SYN置為1,表示是一個串連請求包;
圖中第二個tcp包為伺服器對串連請求的應答包,6個控制位欄位:URG|ACK|PSH|RST|SYN|FIN,ACK和SYN均置為1,表示串連請求的應答包。TCP協議規定,連結建立後,所有的報文的ACK控制位必須置為1;
圖中第三個tcp包為用戶端對應答包的確認包,6個控制位欄位:URG|ACK|PSH|RST|SYN|FIN,ACK置為1。
對於tcp建立連結的過程,控制位SYN只有在連結建立前兩次握手中會置為1, 其他階段的包該控制位不能設定。控制位ACK,在第二次握手開始及以後會一直置為1,直到最後一個報文包。 為什麼要採用三向交握,而不是兩次握手
這主要是為了防止已失效的串連請求報文段突然又傳送到伺服器,產生錯誤。
已失效的串連請求報文段的產生原因:當客戶A發送串連請求,但因串連請求報文丟失而未收到確認。於是A會再次重傳一次串連請求,此時伺服器端B收到再次重傳的串連請求,建立了串連,然後進行資料轉送,資料轉送完了後,就釋放了此串連。假設A第一次發送的串連請求並沒有丟失,而是在網路結點中滯留了太長時間,以致在AB通訊完後,才到達B。此時這個串連請求其實已經是被A認為丟失的了。如果不進行第三向交握,那麼伺服器B可能在收到這個已失效的串連請求後,進行確認,然後單方面進入ESTABLISHED狀態,而A此時並不會對B的確認進行理睬,這樣就白白的浪費了伺服器的資源。 1.2. tcp同時開啟
TCP協議也是支援同時開啟的,儘管這個機率是非常小的,因為這要求通訊雙方都知道對方的連接埠號碼,這需要雙方都事先bind()各自的連接埠,並同時發出SYN包。TCP針對同時開啟的情況,僅建立一條串連而不是兩條串連。
對於同時開啟串連的雙方,在發送SYN包後,都進入SYN_SEND狀態,在收到對端的SYN包後,進入SYN_RCVD狀態,並對此SYN包再次回複SYN包進行確認,當雙方都收到對端的SYN包ack後,就會進入ESTABLISHED狀態。如下:
tcp同時開啟需要4次握手,比正常的建立串連多了一個包。 1.3. TCP釋放串連(四次揮手)
下面兩個圖是從協議和介面兩個角度來解釋TCP的釋放過程(分別摘自電腦網路-謝希仁和UNIX網路編程卷1)
1.3.1. tcpdump四次揮手
和建立串連的三向交握一樣,在連結建立後,然後關閉連結,tcpdump的結果如下:
tcpdump的結果如下:
圖中第一個tcp的請求包中6個控制位欄位:URG|ACK|PSH|RST|SYN|FIN,ACK和FIN控制位置為1, client發起關閉tcp串連的請求。
圖中第二個tcp包,為伺服器響應用戶端的關閉包,其中6個控制位欄位:URG|ACK|PSH|RST|SYN|FIN, ACK和FIN控制位置為1, 且回包的ack = 前一個包seq + 1,
圖中第三個tcp包,為用戶端對伺服器關閉連結的應答,其中6個控制位欄位:URG|ACK|PSH|RST|SYN|FIN,ACK控制位被置為1,這個包代錶鏈接正式關閉;
TCP規定,FIN報文段即使不攜帶資料,它也消耗掉一個序號。
TCP關閉時的狀態兩點需要說明: CLOSE_WAIT狀態
在被動關閉串連情況下,在已經接收到FIN,但是還沒有發送自己的FIN的時刻,串連處於CLOSE_WAIT狀態,此時該tcp串連就處於半關閉狀態。 TIME_WAIT狀態
在用戶端收到伺服器端最後的串連釋放報文段後,用戶端不會立即進入CLOSED狀態。必須經過2MSL(Maximum Segment Lifetime)後,才進入CLOSED狀態。
為了確保用戶端發送的最後一個ACK報文能夠到達伺服器端。 防止TCP串連過程中所說的“已失效的串連請求報文段“出現在本串連中。用戶端在發送完最後一個ACK報文後,再經過2MSL,就可以是本串連期間內的所有報文都在從網路中消失。這樣就可以使下一個新的串連中不會出現舊的串連請求報文。 1.3.2. tcp同時關閉
TCP協議是允許通訊的雙方同時執行關閉操作,即同時發送FIN包,當雙方發送FIN包後,都進入FIN_WAIT_1狀態(雙方互不知對方已發送FIN,所以和正常關閉一樣),當雙方都收到FIN包後,TCP協議層就會知道是雙方是同時關閉,然後就會進入CLOSING狀態,並對對方的SYN包進行ack確認,當雙方收到對方的ack確認後,就會進入TIME_WAIT狀態。過程如下圖:
1.4. TCP的半關閉(half-close)
TCP之所以在釋放連結的時候進行四次揮手,是因為TCP連結是全雙工系統的,因為每個方向的連結都需要單獨關閉。這個是TCP協議的最基本原則,這個原則要求:當一方完成資料的發送後,就發送FIN包來終止這個方向的連結。
所以,TCP的半關閉就是:在串連一端發送結束並關閉後,還擁有可以從另一端接收資料的能力。
不過這個功能只有很少數的應用會使用它,我們平時基本上都是通過close()直接全關閉一個連結。如果應用程式要使用這個功能,可以通過調用shutdown,且第二個參數傳1
下面介紹一下:shutdown 與 close 函數 的區別
int close(int sockfd);
close一個TCP的socket,預設行為是把該通訊端標記成已關閉,然後立即返回到調用進程,該通訊端描述符不能再由調用進程使用,也就是說它不能被read或write,否則會報錯,bad file descriptor,然後TCP將嘗試發送已排隊等待發送到對端的任何資料,發送完畢後發生的是正常的TCP終止序列。
server端調用close()函數後,該連結不再是半關閉,而是全關閉。調用方TCP協議層會把該連結的FD標識為全關閉。server發送完FIN包後,對於用戶端並不知道所謂的全關閉, 因為client只會收到一個FIN包, 按照tcp協議規定: client只會知道server到client方向的資料轉送已經關閉,所以client理論上是允許再次向server端發送資料。
如果,此時client再次向server發送資料會成功,send()只負責把資料交給核心TCP發送緩衝區,然後就成功返回了,所以不會出錯。但是client的tcp核心協議層會收到server的一個RST回包,表示server已不能接收資料,串連被重設。client的tcp核心協議層收到這個RST回包,無法立刻通知應用程式層,只是儲存這個狀態,如果client再次進行: read()操作:返回0,表示連結已關閉。 write()操作:tcp協議層已經處於RST狀態了,會直接觸發SIGPIPE訊號,預設該訊號會終止程式。
int shutdown(int sockfd, int how);
shutdown()可以選擇關閉TCP全雙工系統的單向或雙向的串連。第二參數how值如下:
| how的值 |
描述 |
| 0 |
Further receives are disallowed |
| 1 |
Further sends are disallowed |
| 2 |
Further sends and receives are disallowed (like close()) |
當how為0時,shutdown()關閉串連的讀通道,並丟棄上層還沒有讀走的所有資料以及調用shutdown之後到達的資料,並且此時不會發送FIN包。
當how為1時,shutdown()關閉串連的寫通道,所有的剩餘資料被發送,完成後發送FIN包,執行TCP的半關閉。
這裡需要指出(基本不會發生):
close()不能保證一定會發送FIN包,只有當某個sockfd的引用計數為0,close 才會發送FIN段,否則只是將引用計數減1而已。也就是說只有當所有進程(可能fork多個子進程都開啟了這個通訊端)都關閉了這個通訊端,close 才會發送FIN 段。 1.5. TCP的半開啟(half-open)
TCP的半開啟是一種非常常見的連結異常情況:一方已經關閉或異常終止串連,而另外一方去還不知道。
這種情況在:server調用close()關閉串連後,client在收到FIN包後,並沒有關閉連結,此時這個TCP連結就是半開啟的,此時client再次發送資料對端就會在TCP協議層觸發RST回包,例如下面的tcpdump抓包:
當然還有經常會發生的就是,另一端網路斷開和斷電等導致的TCP處於半開啟狀態。
因為用戶端的多樣性已經網路的複雜,伺服器端很容易就產生很多半開啟的串連,半開啟連結的檢測,有很多方法, 通過應用程式層,對每一個串連添加定時器監聽,逾時無資料就進行關閉操作。 通過TCP協議層的keepalive選項來開始TCP的保活定時器,但保活不是TCP標準規範的(但很多tcp的實現中實現了此功能),TCP RFC中給出了3個不使用保活定時器的理由:
在出現一個短暫的差錯情況下,可能會是一個非常好的串連被釋放掉; 保活功能會耗費不必要的頻寬; 在按流量計費的情況下,會花掉更多的money;
下面是通過SO_KEEPALIVE選項來設定TCP串連支援保活的代碼:
int open = 1; // 開啟keepalive屬性. 預設值: 0(關閉) int idle = 60; // 如果在60秒內沒有任何資料互動,則進行探測. 預設值:7200(s) int interval = 5; // 探測時發探測包的時間間隔為5秒. 預設值:75(s) int retry_cnt = 2; // 探測重試的次數. 全部逾時則認定串連失效..預設值:9(次) setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &open, sizeof(open)); setsockopt(s, SOL_TCP, TCP_KEEPIDLE, idle, sizeof(idle)); setsockopt(s, SOL_TCP, TCP_KEEPINTVL, interval, sizeof(interval)); setsockopt(s, SOL_TCP, TCP_KEEPCNT, retry_cnt, sizeof(retry_cnt));
1.6. TCP的複位報文段
TCP的複位報文是一個很常見的資料,引用TCP/IP詳解的話:無論何時,一個報文段發往基準的串連(referenced connection)出現錯誤,TCP都會發出一個複位報文段。
主要有三種情況: 前面提到的半開啟狀態:一個串連的另一端已經關閉,此時發送資料到對端,就會觸發RST回包; 到不存在的連接埠的串連請求:請求串連一個並沒有監聽的連接埠,TCP則會返回RST(UDP將會產生一個ICMP連接埠不可達的資訊)。 異常終止一個串連:在關閉串連的時候不是通過FIN報文進行正常關閉,而是通過直接發送RST進行串連關閉。兩者的區別:
通過FIN包進行關閉,又稱:有序釋放(orderly release),因為close()/shutdown()都會將緩衝區中的資料全部發送出去之後,才會發送FIN。 通過RST複位包進行關閉,又稱:異常釋放(abortive release)。異常釋放的特點:
丟棄掉發送緩衝區中的全部資料,立刻發送RST報文; RST接收方會區分另一端執行的是正常關閉,還是異常關閉。
直接通過發送RST報文進行串連的異常關閉的代碼如下,具體細節詳見下節的TCP的延遲關閉:
struct linger so_linger;so_linger.l_onoff = true;so_linger.l_linger = 0;ret = setsockopt(events[i].data.fd, SOL_SOCKET, SO_LINGER, &so_linger, sizeof(so_linger));if(ret < 0){ printf("%s():[ERROR]: setsockopt error, errno:%d, info:%s\n", \ __FUNCTION__, errno, strerror(errno));}close(events[i].data.fd);
1.7. TCP的延遲關閉
TCP支援通過SO_LINGER選項來設定串連延遲關閉。前面說過close()一個socket的預設行為是把該通訊端標記成已關閉,然後立即返回到調用進程,然後TCP協議層將嘗試發送已排隊等待發送到對端的任何資料,發送完畢後發生的是正常的TCP終止序列。
SO_LINGER選項可以改變close()的預設行為,選項要求傳入核心的參數的結構體如下:
struct linger{ int l_onoff; //0=off, non-zero = on int l_linger; //延遲關閉的時間,單位second};
l_onoff開啟時,l_linger為非0,調用close()關閉串連時, fd阻塞情況下,如果socket的發送緩衝區殘留為發送的資料,那麼進程將會被投入睡眠,直到所有資料被發送完並收到確定或則l_linger時間逾時。如果延遲時間逾時發送緩衝區的資料還沒有發送完畢,那麼close()會返回EWOULDBLOCK錯誤,並丟棄發送緩衝區的全部資料。 fd非阻塞情況下,close()會立即返回,接下來TCP協議層就是和fd阻塞情況一樣的操作。
l_onoff開啟時,l_linger為0,調用close()關閉串連時,就會觸發前面說的異常釋放的流程,即發送複位報文進行串連的關閉。具體TCP協議層的操作和這麼做的好處請見前文。
下圖是close()的行為在有無SO_LINGER選項下的行為對比,截自《UNIX網路編程劵1:通訊端網路API》
1.8. TCP狀態說明
TCP串連的雙方在串連的不同階段會處於不同的階段,對每一個階段的瞭解,是很好的使用TCP的一個基礎,下表是TCP串連的各個狀態:
| TCP連接埠狀態 |
描述 |
| LISTEN |
等待從任何遠端TCP和連接埠的串連請求 |
| SYN_SENT |
發送完一個串連請求後等待一個匹配的串連請求。 |
| SYN_RECEIVED |
發送串連請求並且接收到匹配的串連請求以後等待串連請求確認。 |
| ESTABLISHED |
表示一個開啟的串連,接收到的資料可以被投遞給使用者。串連的資料轉送階段的正常狀態。 |
| FIN_WAIT_1 |
等待遠端TCP的串連終止請求,或者等待之前發送的串連終止請求的確認。 |
| FIN_WAIT_2 |
等待遠端TCP的串連終止請求 |
| CLOSE_WAIT |
等待本機使用者的串連終止請求 |
| CLOSING |
(同時關閉時)等待遠端TCP的串連終止請求確認 |
| LAST_ACK |
等待先前發送給遠端TCP的串連終止請求的確認(包括它位元組的串連終止請求的確認) |
| TIME_WAIT |
等待足夠的時間過去以確保遠端TCP接收到它的串連終止請求的確認 |
| CLOSED |
不在串連狀態(這是為方便描述假想的狀態,實際不存在) |
下圖是TCP狀態轉換圖(摘自:UNIX網路編程劵1:通訊端網路API)
5.9. 參考
http://zheming.wang/blog/2013/08/18/4417B74D-F037-414A-A291-CEF7B3CD511D/
http://xstarcd.github.io/wiki/shell/tcpdump_TCP_three-way_handshake.html
http://www.cnblogs.com/lshs/p/6038458.html
http://blog.csdn.net/jnu_simba/article/details/9068059
《UNIX網路編程劵1:通訊端網路API》
《TCP/IP詳解 劵1:協議》
《電腦網路(謝希仁)》