動手學習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連接的建立和終止有一個比較直觀的認識。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM