• 动手学习TCP:TCP连接建立与终止


    TCP是一个面向连接的协议,任何一方在发送数据之前,都必须先在双方之间建立一条连接。所以,本文就主要看看TCP连接的建立和终止。

    在开始介绍TCP连接之前,先来看看TCP数据包的首部,首部里面有很多重要的字段,在我们实现程序的时候需要进行设置。

    TCP的首部

    在OSI七层模型中,上层的数据包都会作为下层数据包的数据部分(payload)。

    也就是说,当构造TCP数据包的时候,会把应用层的数据包作为TCP包的数据部分,然后加上TCP头构成TCP数据包;同样,当构造IP数据包的时候,整个TCP包就会被当作数据部分,然后加上IP头构成IP数据包。

    TCP头的数据格式如下,在不包括可选字段的情况下,一般TCP头会占用20个字节。

    在TCP首部中,有几个字段是需要关注一下:

    • 在TCP首部中没有源和目标的IP、MAC地址(IP和MAC地址分别是网络层和链路层首部的信息),只有源和目标的端口
    • Sequence Number是包的序号,网络层(IP层)的传输是不可靠的,可能产生包乱序,所以这个需要可以解决网络包乱序的问题
    • Acknowledgement Number用来确认收到数据包的确认序号,为TCP的传输提供了可靠性保证
    • TCP Flags包括了8个bit,通过对这些bit的设置,可以代表不同类型的TCP数据包

    下面就看看TCP连接的建立和终止。

    TCP连接建立

    TCP连接建立的过程被称为三次握手过程:

    1. 连接建立发起端发送[SYN]包,该端将主动打开(active open)
    2. 接收端将发送[SYN, ACK]包,该端将被动打开(passive open),ACK标志表示对收到的[SYN]包的确认
    3. 连接建立发起端发送[ACK]包确认[SYN, ACK]包

    Initial Sequence Number

    连接建立过程中,一个重要的工作就是初始化Sequence Number,通信的双方在建立连接的过程中互相通知对方自己的初始Sequence Number(ISN:Initial Sequence Number)。ISN不是固定的,ISN跟时钟绑定,根据特定的间隔自增,直到超过2^32,又从0开始。

    SYN全称就是Synchronize Sequence Number,通过seq序号,TCP就可以保证数据包的顺序;通过ack序号,TCP就有了可靠性。

    连接建立注意点

    在建立TCP连接的过程中,有以下两点需要注意一下:

    1. [SYN]标志的数据包会使用消耗一个序号,所以对端的确认号(ack)是当前序号(seq)加一
    2. 当被动打开端发送[ACK]确认包的时候,同时设置了[SYN]标志,所以TCP连接建立的过程只需要三次握手,而不是四次

    TCP连接终止

    TCP连接终止的过程被称为四次挥手过程,以下图为例:

    1. 连接终止端(client)发送[FIN, ACK] 包,关闭client到server方向的数据发送通路
    2. server端发送[ACK]包来确认来自client的[FIN, ACK] 包
    3. server端发送[FIN, ACK] 包,关闭server到client方向的数据发送通路
    4. client端发送[ACK]包来确认来自server的[FIN, ACK] 包,到此TCP连接关闭

    连接终止注意点

    在建立TCP连接的过程中,有以下两点需要注意一下:

    • [FIN]标志的数据包会使用消耗一个序号,所以对端的确认号(ack)是当前序号(seq)加一
    • 与建立连接时的三次 握手不同,终止连接需要四次挥手
      • 因为TCP连接是全双工的,每个方向都必须单独进行关闭。当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,但是TCP连接在收到一个FIN后仍能发送数据

    TCP连接实验

    好了,了解了TCP连接建立和终止的基本知识后,就可以通过Pcap.Net来进行TCP连接建立和终止的实验了。

    建立连接代码的基本流程如下:

    1. client程序使用一个初始的seq序号(100),然后生成并发送一个带[SYN]标志的TCP包
    2. client将期待来自服务端的[SYN, ACK]包
    3. 当收到[SYN, ACK]包之后,client需要生成并发送一个[ACK]包进行确认,这个[ACK]包的ack号是[SYN, ACK]包seq号加一

    终止连接代码的基本流程如下:

    1. client程序delay 10秒钟,然后发送[FIN, ACK]包关闭client到server的通路,继续使用全局的seq号
    2. client将期待来自服务端的[ACK]包,以及[FIN, ACK]包
    3. 最后client发送[ACK]包,seq号需要加一,因为[FIN]标志的包将消耗一个序号,TCP连接终止完成

    主程序如下,发送TCP连接建立和终止请求,每个请求发送后都用PacketHandler处理收到的包:

    communicator.SendPacket(Utils.BuildTcpPacket(endPointInfo, TcpControlBits.Synchronize, null));
    PacketHandler(communicator, endPointInfo);
    
    // delay 10 secs, then client to send Fin
    Thread.Sleep(10000);
    
    communicator.SendPacket(Utils.BuildTcpPacket(endPointInfo, TcpControlBits.Fin | TcpControlBits.Acknowledgment));
    PacketHandler(communicator, endPointInfo);

    程序的主要逻辑在PacketHandler中,这个函数根据收到的不同TCP包的类型完成不同的逻辑,产生并发送不同类型的包。

    例如,当PacketHandler接收到来自服务端的[SYN, ACK]包后,处理函数就会生成并发送一个[ACK]确认包。也就是说,PacketHandler的逻辑就是实现了TCP连接建立和终止的逻辑。

    private static void PacketHandler(PacketCommunicator communicator, EndPointInfo endPointInfo)
    {
        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):
                                Utils.PacketInfoPrinter(packet);
                                Packet ack4SynAck = Utils.BuildTcpResponsePacket(packet, TcpControlBits.Acknowledgment);
                                communicator.SendPacket(ack4SynAck);
                                break;
                            
                            case (TcpControlBits.Fin | TcpControlBits.Acknowledgment):
                                Utils.PacketInfoPrinter(packet);
                                Packet ack4FinAck = Utils.BuildTcpResponsePacket(packet, TcpControlBits.Acknowledgment);
                                communicator.SendPacket(ack4FinAck);
                                break;
                            case TcpControlBits.Acknowledgment:
                                Utils.PacketInfoPrinter(packet);
                                break;
                            default:
                                Utils.PacketInfoPrinter(packet);
                                break;
                        }
                    }
                    else
                    {
                        switch (packet.Ethernet.IpV4.Tcp.ControlBits)
                        {
                            case (TcpControlBits.Fin | TcpControlBits.Acknowledgment):
                                Utils.PacketInfoPrinter(packet);
                                break;
                            case TcpControlBits.Synchronize:
                                Utils.PacketInfoPrinter(packet);
                                break;
                            case TcpControlBits.Acknowledgment:
                                Utils.PacketInfoPrinter(packet);
                                running = false;
                                break;
                            default:
                                Utils.PacketInfoPrinter(packet);
                                break;
                        }
                    }
                    break;
                default:
                    throw new InvalidOperationException("The result " + result + " should never be reached here");
            }
        } while (running);
    
    }

    在PacketHandler函数中用到了BuildTcpResponsePacket这个函数,这个函数根据收到的TCP包,来构建response包。

    这个函数有下面几个注意点:

    • 该函数会根据收到的包,设置response包的源和目的地址
    • 该函数会接受PacketHandler传递来的TCP flags,并设置到TCP首部中
    • 该函数的另一个重要部分就是会计算并设置TCP首部中的seq好ack号,这一点很重要
    public static Packet BuildTcpResponsePacket(Packet packet, TcpControlBits tcpControlBits)
    {
        EthernetLayer ethernetHeader = new EthernetLayer
        {
            Source = new MacAddress(packet.Ethernet.Destination.ToString()),
            Destination = new MacAddress(packet.Ethernet.Source.ToString()),
            EtherType = EthernetType.None, // Will be filled automatically.
        };
    
        IpV4Layer ipHeader = new IpV4Layer
        {
            Source = new IpV4Address(packet.Ethernet.IpV4.Destination.ToString()),
            CurrentDestination = new IpV4Address(packet.Ethernet.IpV4.Source.ToString()),
            Fragmentation = IpV4Fragmentation.None,
            HeaderChecksum = null, // Will be filled automatically.
            Identification = 123,
            Options = IpV4Options.None,
            Protocol = null, // Will be filled automatically.
            Ttl = 100,
            TypeOfService = 0,
        };
    
        TcpLayer tcpHeader = new TcpLayer
        {
            SourcePort = packet.Ethernet.IpV4.Tcp.DestinationPort,
            DestinationPort = packet.Ethernet.IpV4.Tcp.SourcePort,
            Checksum = null, // Will be filled automatically.
            SequenceNumber = seqNum = packet.Ethernet.IpV4.Tcp.AcknowledgmentNumber,
            AcknowledgmentNumber = ackNum = packet.Ethernet.IpV4.Tcp.SequenceNumber + (uint)((packet.Ethernet.IpV4.Tcp.Payload.Length > 0) ? packet.Ethernet.IpV4.Tcp.Payload.Length : 1),
            ControlBits = tcpControlBits,
            Window = windowSize,
            UrgentPointer = 0,
            Options = TcpOptions.None,
        };
    
    
        PacketBuilder builder = new PacketBuilder(ethernetHeader, ipHeader, tcpHeader);
    
        return builder.Build(DateTime.Now);
    }

    运行效果

    打开Wireshark监听"VirtualBox Host-Only Network"网卡,并设置filter为"port 8081"。

    然后运行程序,通过console可以看到客户端发送的包,以及服务端返回的包,通过这些包完成了TCP连接的建立和终止。

    下面是Wireshark中显示的结果,Wireshark比较友好,会显示相对seq号,所以看到的都是从0开始编号。

    注意seq号和ack号的变化,[SYN]和[FIN]标志的TCP包都会消耗一个序号。

    总结

    本文介绍了TCP首部,通过设置TCP首部中的[SYN]标志,可以构造TCP连接建立请求包;通过设置[FIN]标志,可以构造TCP连接终止请求包。

    文中使用Pcap.Net构建了一个简单的客户端,完成了向服务器建立(三次握手)和终止(四次挥手)连接的过程。

    通过这个实验,一定会对TCP连接的建立和终止有一个比较直观的认识。

  • 相关阅读:
    深入Java虚拟机(4)——网络移动性
    安装ftp碰到的问题及解决方法
    6.设置ListView的Item的高度无效
    hdu1181(变形课)
    ZooKeeper分布式集群部署及问题
    管理线程之创建线程
    Linux系统编程——多线程实现多任务
    Request.Params用法,后台接收httpget参数
    resharper警告 :linq replace with single call to FirstOrDefault
    SQL Prompt几个快捷键
  • 原文地址:https://www.cnblogs.com/wilber2013/p/4847017.html
Copyright © 2020-2023  润新知