• 动手学习TCP:客户端状态变迁


    上一篇文章中介绍了TCP连接的建立和终止。

    通过实际操作了解到,在TCP协议工作过程中,客户端和服务端都会接收或者发送特定标志的TCP数据包,然后进入不同的状态。

    也就是说,TCP协议就是一个包含多种状态转换的状态机,下面介绍一下TCP状态机。

    TCP状态机

    网络上的传输是没有连接的,包括TCP也是一样的。TCP所谓的"连接",其实是在通讯的双方维护一个"连接状态",让它看上去好像有连接一样。

    所以,了解TCP状态机,以及TCP的状态变迁是非常重要的。

    TCP 协议的操作可以使用一个具有 11 种状态的有限状态机来表示(看下图),图中的矩形表示状态,箭头表示状态之间的转换。

    1. 客户端的状态变迁用红实线,服务器端的状态变迁用蓝实线
    • 图中红实线表示客户端正常的状态变迁
    • 图中蓝实线表示服务端正常的状态变迁

        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状态变迁的逻辑。

    结合这前面的状态变迁表,这段代码就非常容易理解了。

    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客户端的状态变迁有个深刻的印象。

  • 相关阅读:
    php 数据类型
    Django REST framework基础:视图和路由
    Django REST framework基础:序列化
    android 适配器 ArrayAdapter,SimpleAdapter的学习
    关于系统模块设计的一点疑问?
    看了看 #ifndef 和#pragma once 的区别
    ace.js 中文手册
    .net core 3.1发布时视图Views文件夹不被打打包成.dll文件解决办法
    Asp.net core应用在 Kubernetes上内存使用率过高问题分析
    .Net Core内存回收模式及性能测试对比
  • 原文地址:https://www.cnblogs.com/wilber2013/p/4847040.html
Copyright © 2020-2023  润新知