詳解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。

  1. 執行命令 sudo pfctl – f /etc/pf.conf
  2. 執行命令 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關閉連接流程,上面程式碼運行後抓包顯示如下:

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