詳解TCP的重置功能和實現連接結束功能
- 2019 年 10 月 5 日
- 筆記
上一節我們完成了TCP三次握手原則,當雙方通過三次握手交換了各自用於傳遞資訊的參數後,雙方進入數據分發模式,在TCP協議上說雙方都進入了ESTABLISHED狀態。基於早期品質低下的數據傳輸網路,連接建立只不過是開始,在通訊過程中保持穩定和通暢是TCP協議的重要內容。
由於TCP協議目的是保持長時間數據傳輸的穩定,因此它必須有效應對在連接過程中出現的突然中斷情況。突然中斷最常見的叫」半開「過程,也就是一方已經已經斷開連接而另一方並不知情,它還以為對方正常在跟它傳輸數據。為了面對這種情況,TCP引入了Reset功能,上一節我們編碼完成三次握手時,如果抓包觀察就會發現,我們程式碼並沒有發出reset數據包,但是抓包卻發現我方發出了reset數據包,這是因為一旦某一方發現對方沒有按照「套路出牌」時他就會像對方發送reset消息。
在上節我們的編碼實現中,我們像對方發送SYN數據包時,對方回應了ACK數據包,由於我們直接繞開底層TCP模組,作業系統底層TCP模組便會覺得迷惑,兩種原因會讓TCP模組發出reset數據包,一種是當收到SYN數據包時,TCP模組發現並沒有對應的進程使用相應埠對數據進行接收,於是他就會發生reset數據包,我們上一節屬於這種情況,二是收到ACK包時對方回復的關鍵參數不對。
對方接收到reset數據包時也不會直接斷開連接,而是檢驗對方發來的reset是否合理,如果接收方發現reset數據包是合理的,它會根據自己當前狀態來做出多種不同應對。如果接收方處於監聽狀態,那麼它會保持當前狀態不變,如果接收方向對方發出了SYN+ACK包,但還沒有收到對方的ACK包卻收到reset包,那麼它會退回到監聽狀態,其他情況下接收方會把當前連接中斷掉。
為了防止我們程式繞過作業系統TCP底層模組進行三次握手而導致它向對方發送rest數據包的問題,在mac上我們可以指定讓TCP模組對指定的IP和埠不發生RST數據包,其方法如下: 1, 首先通過sudo /etc/pf.conf打開編輯文件 2, 在文件中添加一行: block drop proto tcp from 192.168.2.243 to 220.181.43.8 flags R/R 其中192.168.2.243是發出方的ip,可以換成你運行程式的ip,220.181.43.8是對方ip,你可以換成想要進行tcp交互的ip。
- 執行命令 sudo pfctl – f /etc/pf.conf
- 執行命令 sudo pfctl -e 讓設置的命令生效。
執行上述步驟後,運行我們上一節的程式碼,在wireshark抓包將不會再看到底層TCP模組發送reset數據包給對方。在TCP數據傳輸管理過程中協議還需要控制連接中的「閑置」過程,也就是雙方保持連接但沒有數據發送或接收的時候。如果長時間沒有數據傳輸,協議需要確保雙方依然處於正常連接狀態,於是作業系統上的TCP協議棧實現都會向對方發送一個不含任何數據的空消息,然後對方回復一個ACK數據包,這種用於表明「依然在線」的消息包叫做「keepalive」機制。
該機制並非屬於TCP協議規定而是TCP協議具體實現方自行加入的機制。這種機制有很多爭論,但支援方認為伺服器有必要使用keepalive方式確保連接的有效性,因為伺服器要同時接收很多客戶端的連接,因此每個連接都意味著對伺服器資源的損耗,如果連接失效伺服器要及時斷開連接,以便把資源留給其他客戶端。
當所有數據發送完畢,雙方就進入連接中斷階段。問題在於TCP中斷連接的過程比想像要複雜,這點我們在前面也提及過。當通訊的一方向對方發出關閉連接請求時,這隻意味著它不再向對方發送數據,但它不能立馬下線,因為對方可能有數據要發送給自己,因此它必須等待對方傳輸完所有數據後才能下線。
因此在一方發起連接終結時,會向對方發送一個FIN包,這個數據包甚至有可能還會攜帶發送給對方的數據。接收到FIN數據包的一方會向對方發送FIN+ACK數據包,然後對方再次發送ACK包,整個通訊流程才算結束。
接下來我們在上一節的基礎上添加關閉連接的功能,相應程式碼如下:
public class TCPThreeHandShakes extends Application{ .... //增加協議狀態 private static int CONNECTION_IDLE = 0; private static int CONNECTION_INIT = 1; private static int CONNECTION_SUCCESS = 2; private static int CONNECTION_FIN_INIT = 3; private static int CONNECTION_FIN_SUCCESS = 4; private int tcp_state = CONNECTION_IDLE; .... //向伺服器發起關閉流程 public void beginClose() throws Exception { // this.seq_num += 1; createAndSendPacket(null, "FIN,ACK"); this.tcp_state = CONNECTION_FIN_INIT; } @Override public void handleData(HashMap<String, Object> headerInfo) { short src_port = (short)headerInfo.get("src_port"); System.out.println("receive TCP packet with port:" + src_port); boolean ack = false, syn = false, fin = false; if (headerInfo.get("ACK") != null) { System.out.println("it is a ACK packet"); ack = true; } if (headerInfo.get("SYN") != null) { System.out.println("it is a SYN packet"); syn = true; } if (headerInfo.get("FIN") != null) { System.out.println("it is a FIN packet"); fin = true; } if (ack && syn) { int seq_num = (int)headerInfo.get("seq_num"); int ack_num = (int)headerInfo.get("ack_num"); System.out.println("tcp handshake from othersize with seq_num" + seq_num + " and ack_num: " + ack_num); this.seq_num += 1; this.ack_num = seq_num + 1; try { if (this.tcp_state == CONNECTION_INIT) { this.tcp_state = CONNECTION_SUCCESS; System.out.println("three hanshake complete"); } createAndSendPacket(null, "ACK"); //啟動關閉流程 beginClose(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } //收到伺服器發回的fin+ack包,正式關閉連接 if (ack && fin) { System.out.println("receive fin packet and close connection"); if (this.tcp_state == CONNECTION_FIN_INIT) { this.tcp_state = CONNECTION_FIN_SUCCESS; System.out.println("three hanshake shutdown"); try { int seq_num = (int)headerInfo.get("seq_num"); int ack_num = (int)headerInfo.get("ack_num"); System.out.println("tcp handshake closing from othersize with seq_num" + seq_num + " and ack_num: " + ack_num); this.seq_num += 1; this.ack_num = seq_num + 1; createAndSendPacket(null, "ACK"); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } }
在上面程式碼中,我們增加 一個函數beginClose()用於向對方發送ACK+FIN數據包告知對方關閉當前連接。這個函數在我們完成三次握手後被調用,當我們向對方發送ACK+FIN數據包後,對方也會向我們發送ACK+FIN數據包,最後我們再次向對方發送一個ACK包,由此完成TCP關閉連接流程,上面程式碼運行後抓包顯示如下:

從抓包結果可見我們成功完成了三次握手以及連接關閉的整個循環。