详解TCP的重置功能和实现连接结束功能

上一节我们完成了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关闭连接流程,上面代码运行后抓包显示如下:

从抓包结果可见我们成功完成了三次握手以及连接关闭的整个循环。

原文发布于微信公众号 - Coding迪斯尼(gh_c9f933e7765d)

原文发表时间:2019-09-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券