上一节我们完成了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关闭连接流程,上面代码运行后抓包显示如下:
从抓包结果可见我们成功完成了三次握手以及连接关闭的整个循环