详解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关闭连接流程,上面代码运行后抓包显示如下:

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