動手學習TCP系列之客戶端狀態變遷
上一篇文章中介紹了TCP連接的建立和終止。
通過實際操作了解到,在TCP協議工作過程中,客戶端和服務端都會接收或者發送特定標志的TCP數據包,然后進入不同的狀態。
也就是說,TCP協議就是一個包含多種狀態轉換的狀態機,下面介紹一下TCP狀態機。
TCP狀態機
網絡上的傳輸是沒有連接的,包括TCP也是一樣的。TCP所謂的"連接",其實是在通訊的雙方維護一個"連接狀態",讓它看上去好像有連接一樣。
所以,了解TCP狀態機,以及TCP的狀態變遷是非常重要的。
TCP 協議的操作可以使用一個具有 11 種狀態的有限狀態機來表示(看下圖),圖中的矩形表示狀態,箭頭表示狀態之間的轉換。
客戶端的狀態變遷用紅實線,服務器端的狀態變遷用藍實線
圖中紅實線表示客戶端正常的狀態變遷
圖中藍實線表示服務端正常的狀態變遷
2. 虛線用于不常見的序列,如復位、同時打開、同時關閉等等

根據上面的狀態變遷圖,可以看到在TCP狀態機的全部11種狀態中:
客戶端特有的狀態:SYN_SENT、FIN_WAIT_1、FIN_WAIT_2、CLOSING、TIME_WAIT 。
服務端特有的狀態:LISTEN、SYN_RCVD、CLOSE_WAIT、LAST_ACK 。
共有的狀態:CLOSED、ESTABLISHED 。
下面就主要來看看客戶端的狀態變遷。
客戶端狀態變遷
根據狀態變遷圖,客戶端的正常狀態變遷流程如下:
CLOSED -> SYN_SENT -> ESTABLISHED -> FIN_WAIT_1 -> FIN_WAIT_2 -> TIME_WAIT -> CLOSED
具體的將狀態跟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 這個標志:
正常情況下客戶端在完成請求之后,會發送[FIN]包來請求終止TCP連接
但是很多應用服務器為了提高TCP連接的利用效率,會在TCP連接長時間空閑的情況下,會主動向客戶端發送[FIN]包。
例如,我通過nodejs實現了一個http server進行測試,在TCP連接空閑3分鐘之后,服務端會發送[FIN]終止連接
這次實驗中的"PacketHandler"也跟上次有所不同,在TCP包的接收或發送的過程中,都加入了TCP狀態變遷的邏輯。
結合這前面的狀態變遷表,這段代碼就非常容易理解了。
#p#
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客戶端的狀態變遷有個深刻的印象。





















