上一篇文章中介绍了TCP连接的建立和终止。
通过实际操作了解到,在TCP协议工作过程中,客户端和服务端都会接收或者发送特定标志的TCP数据包,然后进入不同的状态。
也就是说,TCP协议就是一个包含多种状态转换的状态机,下面介绍一下TCP状态机。
网络上的传输是没有连接的,包括TCP也是一样的。TCP所谓的"连接",其实是在通讯的双方维护一个"连接状态",让它看上去好像有连接一样。
所以,了解TCP状态机,以及TCP的状态变迁是非常重要的。
TCP 协议的操作可以使用一个具有 11 种状态的有限状态机来表示(看下图),图中的矩形表示状态,箭头表示状态之间的转换。
2. 虚线用于不常见的序列,如复位、同时打开、同时关闭等等
根据上面的状态变迁图,可以看到在TCP状态机的全部11种状态中:
下面就主要来看看客户端的状态变迁。
根据状态变迁图,客户端的正常状态变迁流程如下:
具体的将状态跟TCP包关联起来就如下表所示,根据这张表,我们就可以构建客户端正常状态变迁的状态机了:
From State | To State | Recv Packet | Send Packet |
CLOSED | SYN_SENT | - | [SYN] |
SYN_SENT | ESTABLISHED | [SYN, ACK] | [ACK] |
ESTABLISHED | FIN_WAIT_1 | - | [FIN, ACK] |
FIN_WAIT_1 | FIN_WAIT_2 | [ACK] | - |
FIN_WAIT_2 | TIME_WAIT | [FIN, ACK] | [ACK] |
TIME_WAIT | CLOSED | - | - |
有了上面的客户端状态变迁表之后,我们就清楚客户端会接受或发送什么类型的包,然后进入什么特定的状态了。
下面就可以通过Pcap.Net来模拟一些这个状态变迁过程了。
首先在代码中定义了一个枚举类型,列出了TCP状态机的所有11中状态。
public enum TCPStatus { CLOSED, LISTENING, SYN_RECEIVED, SYN_SEND, ESTABLISHED, CLOSE_WAIT, LAST_ACK, FIN_WAIT_1, FIN_WAIT_2, TIME_WAIT, CLOSING, NULL, }
主程序开始之前,会将TCP状态机的初始状态设置为"CLOSED":
private static TCPStatus tcpStatus = TCPStatus.CLOSED;
主程序跟上一次TCP连接的实验类似,只是加入了TCP状态变迁的过程。
例如,当客户端发送过[SYN]数据包之后,根据上面总结的客户端TCP状态变迁表,将“tcpStatus”设置为“SYN_SEND”。
bool clientToSendFin = true; communicator.SendPacket(Utils.BuildTcpPacket(endPointInfo, TcpControlBits.Synchronize, null)); tcpStatus = TCPStatus.SYN_SEND; PacketHandler(communicator, endPointInfo, clientToSendFin); if (clientToSendFin) { Thread.Sleep(10000); communicator.SendPacket(Utils.BuildTcpPacket(endPointInfo, TcpControlBits.Fin | TcpControlBits.Acknowledgment)); tcpStatus = TCPStatus.FIN_WAIT_1; PacketHandler(communicator, endPointInfo); }
注意,代码中有一点特殊的就是 bool clientToSendFin = true 这个标志 :
但是很多应用服务器为了提高TCP连接的利用效率,会在TCP连接长时间空闲的情况下,会主动向客户端发送[FIN]包。
这次实验中的"PacketHandler"也跟上次有所不同,在TCP包的接收或发送的过程中,都加入了TCP状态变迁的逻辑。
结合这前面的状态变迁表,这段代码就非常容易理解了。
private static void PacketHandler(PacketCommunicator communicator, EndPointInfo endPointInfo, bool clientToSendFin = true) { Packet packet = null; bool running = true; do { PacketCommunicatorReceiveResult result = communicator.ReceivePacket(out packet); switch (result) { case PacketCommunicatorReceiveResult.Timeout: // Timeout elapsed continue; case PacketCommunicatorReceiveResult.Ok: bool isRecvedPacket = (packet.Ethernet.IpV4.Destination.ToString() == endPointInfo.SourceIp) ? true : false; if (isRecvedPacket) { switch (packet.Ethernet.IpV4.Tcp.ControlBits) { case (TcpControlBits.Synchronize | TcpControlBits.Acknowledgment): if (tcpStatus == TCPStatus.SYN_SEND) { Utils.PacketInfoPrinter(packet); Packet ack = Utils.BuildTcpResponsePacket(packet, TcpControlBits.Acknowledgment); communicator.SendPacket(ack); tcpStatus = TCPStatus.ESTABLISHED; } break; case (TcpControlBits.Fin | TcpControlBits.Acknowledgment): if (tcpStatus == TCPStatus.FIN_WAIT_2) { Utils.PacketInfoPrinter(packet); Packet ack = Utils.BuildTcpResponsePacket(packet, TcpControlBits.Acknowledgment); communicator.SendPacket(ack); tcpStatus = TCPStatus.TIME_WAIT; } else if (tcpStatus == TCPStatus.ESTABLISHED) { Utils.PacketInfoPrinter(packet); Packet ack = Utils.BuildTcpResponsePacket(packet, TcpControlBits.Acknowledgment); communicator.SendPacket(ack); tcpStatus = TCPStatus.CLOSE_WAIT; } break; case TcpControlBits.Acknowledgment: if (tcpStatus == TCPStatus.FIN_WAIT_1) { tcpStatus = TCPStatus.FIN_WAIT_2; Utils.PacketInfoPrinter(packet, tcpStatus); } else if (tcpStatus == TCPStatus.LAST_ACK) { tcpStatus = TCPStatus.CLOSED; Utils.PacketInfoPrinter(packet, tcpStatus); running = false; } break; default: Utils.PacketInfoPrinter(packet); break; } } else { switch (packet.Ethernet.IpV4.Tcp.ControlBits) { case TcpControlBits.Synchronize: if (tcpStatus == TCPStatus.SYN_SEND) { Utils.PacketInfoPrinter(packet, tcpStatus); } break; case TcpControlBits.Acknowledgment: if (tcpStatus == TCPStatus.ESTABLISHED) { Utils.PacketInfoPrinter(packet, tcpStatus); if (clientToSendFin) running = false; } else if (tcpStatus == TCPStatus.TIME_WAIT) { Utils.PacketInfoPrinter(packet, tcpStatus); running = false; } else if (tcpStatus == TCPStatus.CLOSE_WAIT) { Utils.PacketInfoPrinter(packet, tcpStatus); Packet fin = Utils.BuildTcpPacket(endPointInfo, TcpControlBits.Fin | TcpControlBits.Acknowledgment); communicator.SendPacket(fin); tcpStatus = TCPStatus.LAST_ACK; } break; case (TcpControlBits.Fin | TcpControlBits.Acknowledgment): if (tcpStatus == TCPStatus.FIN_WAIT_1) { Utils.PacketInfoPrinter(packet, tcpStatus); } else if (tcpStatus == TCPStatus.LAST_ACK) { Utils.PacketInfoPrinter(packet, tcpStatus); } break; default: Utils.PacketInfoPrinter(packet); break; } } break; default: throw new InvalidOperationException("The result " + result + " should never be reached here"); } } while (running); }
下面,将"clientToSendFin"设置为"true",看看正常情况下客户端的状态变迁。
打开Wireshark监听"VirtualBox Host-Only Network"网卡,并设置filter为"port 8081"。
运行程序,通过console可以看到客户端和服务端之间的包,以及客户端的状态变迁。
下面是Wireshark抓到的包,这七个数据包就表示了TCP连接的建立和终止过程。
本文介绍了TCP状态变迁图,根据客户端的状态变迁过程,得到了客户端的状态变迁表。
然后使用Pcap.Net,基于客户端的状态变迁表,构建了一个简单的客户端,展示了客户端状态变迁的过程。
通过这个实验,一定能够对TCP客户端的状态变迁有个深刻的印象。