動手學習TCP: 環境搭建


前一段時間通過Wireshark抓包,定位了一個客戶端和服務器之間數據傳輸的問題。最近就抽空看了看《TCP/IP詳解 卷1》中關於TCP的部分,書中用了很多例子展示了TCP/IP協議中的一些基本概念。

所以,也准備自己動手,通過一些簡單的實驗來進一步了解一下TCP中的一些基本概念。

環境搭建和配置

在開始進行實驗之前,首先看看實驗環境的搭建:

  1. Wireshark:用來抓取網絡上的包,可以清楚的看到TCP/IP協議層,以及每層的詳細信息,通過此處下載
  2. 一台虛擬機:如果客戶端和服務端都在本機,那么數據通信是不經過網卡的,所以Wireshark就抓不到任何數據包。方便的辦法就是本機安裝一個虛擬機,通過本機和虛擬機通信進行實驗。我使用的是VirtualBox+winXP.
  3. Pcap.Net:是一個WinPcap的.NET wrapper,基於這個庫,我們就可以很方便的通過C#代碼來實現下面功能(通過 此處下載):
    1. 獲取網絡設備
    2. 接收、解析數據包
    3. 創建、發送數據包

在建立好實驗環境之后,還需要進行一些簡單的配置,保證宿主機和虛擬機之間的網絡是暢通的。

將虛擬機網絡設置為"Host-only Adapter"模式。

虛擬機網絡設置好之后,就可以配置本機和虛擬機IP地址了,然后保證宿主機可以ping通虛擬機。

環境驗證

通過上面的步驟,簡單的實驗環境就建立完成了,下面就要來實現客戶端和服務端了,試試實驗環境是否能夠正常工作。

服務端

首先,將虛擬機(192.168.56.102)作為服務端,運行下面一段代碼創建一個簡單的socket server,服務端綁定192.168.56.102:8081:

import sys
from socket import *

HOST = "192.168.56.102"
PORT = 8081
BUFSIZ = 1024
ADDR = (HOST, PORT)

server = socket(AF_INET, SOCK_STREAM)
print "Socket created"
try:
    server.bind(ADDR)
except error, msg:
    print 'Bind failed. Error Code : ' + str(msg[0]) + ' Message ' + msg[1]
    sys.exit()

server.listen(10)
print 'Socket now listening'

while True:
    conn, addr = server.accept()
    try:
        data = conn.recv(100)
        if data:
            print data
    except Exception, e:
     print e
    conn.close()

客戶端

客戶端的實現在本機(192.168.56.101),使用一段基於Pcap.Net的代碼向服務器發送一個[SYN]包(TCP連接建立需要進行三次握手,[SYN]包就是第一個握手包),來請求建立TCP連接。

在客戶端代碼中,通過Pcap.Net實現了兩個工具函數,一個用來獲取本機網卡設備列表,一個用在構造不同類型的TPC包。

獲取本機網卡設備列表代碼:

public static PacketDevice GetNICDevice()
{
    // Retrieve the device list from the local machine
    IList<LivePacketDevice> allDevices = LivePacketDevice.AllLocalMachine;

    if (allDevices.Count == 0)
    {
        Console.WriteLine("No interfaces found! Make sure WinPcap is installed.");
        return null;
    }

    // Print the device list
    for (int i = 0; i != allDevices.Count; ++i)
    {
        LivePacketDevice device = allDevices[i];
        Console.Write((i + 1) + ". " + device.Name);
        if (device.Description != null)
            Console.WriteLine(" (" + device.Description + ")");
        else
            Console.WriteLine(" (No description available)");
    }

    int deviceIndex = 0;
    do
    {
        Console.WriteLine("Enter the interface number (1-" + allDevices.Count + "):");
        string deviceIndexString = Console.ReadLine();
        if (!int.TryParse(deviceIndexString, out deviceIndex) ||
            deviceIndex < 1 || deviceIndex > allDevices.Count)
        {
            deviceIndex = 0;
        }
    } while (deviceIndex == 0);

    return allDevices[deviceIndex - 1];
}

另一段重要的代碼就是構造TCP包的代碼,根據OSI七層模型,下面代碼中分別創建了鏈路層、網絡層和傳輸層的部分,然后生成一個數據包:

public static Packet BuildTcpPacket(EndPointInfo endPointInfo, TcpControlBits tcpControlBits, List<TcpOption> tcpOptionList = null)
{
    EthernetLayer ethernetLayer =
        new EthernetLayer
        {
            Source = new MacAddress(endPointInfo.SourceMac),
            Destination = new MacAddress(endPointInfo.DestinationMac),
            EtherType = EthernetType.None, // Will be filled automatically.
        };

    IpV4Layer ipV4Layer =
        new IpV4Layer
        {
            Source = new IpV4Address(endPointInfo.SourceIp),
            CurrentDestination = new IpV4Address(endPointInfo.DestinationIp),
            Fragmentation = IpV4Fragmentation.None,
            HeaderChecksum = null, // Will be filled automatically.
            Identification = 123,
            Options = IpV4Options.None,
            Protocol = null, // Will be filled automatically.
            Ttl = 10,
            TypeOfService = 0,
        };

    TcpLayer tcpLayer =
        new TcpLayer
        {
            SourcePort = endPointInfo.SourcePort,
            DestinationPort = endPointInfo.DestinationPort,
            Checksum = null, // Will be filled automatically.
            SequenceNumber = seqNum,
            AcknowledgmentNumber = ackNum,
            ControlBits = tcpControlBits,
            Window = windowSize,
            UrgentPointer = 0,
            Options = (tcpOptionList == null) ? TcpOptions.None : new TcpOptions(tcpOptionList),
        };

    PacketBuilder builder = new PacketBuilder(ethernetLayer, ipV4Layer, tcpLayer);

    return builder.Build(DateTime.Now);
}

主程序中,首先配置了客戶端和服務器的端口、IP/MAC地址信息,然后通過前面兩個工具函數構造一個TCP連接建立請求包([SYN]包),並通過"VirtualBox Host-Only Network"網卡發送給服務端。

static void Main(string[] args)
{
    // Take the selected adapter
    PacketDevice selectedDevice = Utils.GetNICDevice();

    // Open the output device
    using (PacketCommunicator communicator = selectedDevice.Open(System.Int32.MaxValue, // name of the device
                                                                 PacketDeviceOpenAttributes.Promiscuous, // promiscuous mode
                                                                 1)) // read timeout
    {
        EndPointInfo endPointInfo = new EndPointInfo();
        endPointInfo.SourceMac = "08:00:27:00:C0:D5";
        endPointInfo.DestinationMac = "08:00:27:70:A6:AE";
        endPointInfo.SourceIp = "192.168.56.101";
        endPointInfo.DestinationIp = "192.168.56.102";
        endPointInfo.SourcePort = 3330;
        endPointInfo.DestinationPort = 8081;

        using (BerkeleyPacketFilter filter = communicator.CreateFilter("tcp port " + endPointInfo.DestinationPort))
        {
            // Set the filter
            communicator.SetFilter(filter);
        }

        communicator.SendPacket(Utils.BuildTcpPacket(endPointInfo, TcpControlBits.Synchronize, null));
        PacketHandler(communicator, endPointInfo);
    }

    Console.WriteLine("Press Enter to Quit!");
    Console.ReadLine();

}

private static void PacketHandler(PacketCommunicator communicator, EndPointInfo endPointInfo)
{
    Packet packet = null;

    do
    {
        PacketCommunicatorReceiveResult result = communicator.ReceivePacket(out packet);

        switch (result)
        {
            case PacketCommunicatorReceiveResult.Timeout:
                // Timeout elapsed
                continue;
            case PacketCommunicatorReceiveResult.Ok:
                Utils.PacketInfoPrinter(packet);
                break;
            default:
                throw new InvalidOperationException("The result " + result + " should never be reached here");
        }
    } while (true);
}

運行代碼

代碼完成了,下面看看運行效果,為了直觀的看到數據包的傳輸,這是就可以打開Wireshark了。

為了避免抓到不相關的數據包,可以設置Wireshark中的filter,然后開始抓取。

下面運行代碼,並選擇正確的網卡。通過console和Wireshark的輸出可以看到,我們成功的生產了一個[SYN]包並發送到了服務器。

根據TCP連接建立過程可以知道,客戶端發送[SYN]包后,期待從服務器得到一個[SYN, ACK]包。

到這里,說明前面搭建的環境,以及客戶端和服務端的代碼都是可以正常工作的了。

誰的[RST]包

從上面的結果中看到,客戶端在收到[SYN, ACK]包之后,發送了一個[RST]包重置這條TCP連接。

仔細查看了代碼發現,客戶端的代碼中並沒有發送[RST]包。那么這個[RST]包是哪里來的呢?

操作系統中有協議棧的概念,所以來自應用層的數據,都會一層層的經過操作系統協議棧處理,然后通過網卡發送出去。

當客戶端網卡收到[SYN, ACK]包后,這個包會被我們的Pcap.Net程序捕獲,也會被傳送給客戶端操作系統。由於通過Pcap.Net構造的[SYN]包是沒有經過操作系統協議棧的,所以操作系統會認為[SYN, ACK]包是一個無效TCP包,並通過[RST]包重置TCP連接。

到這里,多余[RST]包就可以解釋了。

避免[RST]包

為了避免操作系統協議棧對Pcap.Net程序的影響,通過IP安全策略(通過Run->"secpol.msc"打開設置)的設置,可以避免操作系統從本機(192.168.56.101)向虛擬機(192.168.56.102)發送數據包。

設置完成后,再次運行程序,這是程序就正常了。

由於客戶端沒有發送[ACK]包來確認來自服務端的[SYN, ACK]包,根據TCP工作原理,服務端會進行重傳。

總結

本文中介紹了TCP實驗環境的搭建,通過Pcap.Net創建了一個客戶端,可以構造不同類型的TCP數據包,並通過特定網卡向服務器發送。

后面繼續基於這個環境來看看TCP的一些基本概念,TCP連接、狀態變遷等等。

 


免責聲明!

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



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