1. 程式人生 > 其它 >計算機網路——Tracert與Ping程式設計與實現

計算機網路——Tracert與Ping程式設計與實現

技術標籤:計算機網路c++計算機網路

計算機網路——Tracert與Ping程式設計與實現

一、實驗目的

瞭解Tracert程式的實現原理,並除錯通過。

二、總體設計

1. 基本原理

tracert(跟蹤路由)是路由跟蹤實用程式,用於確定IP資料包訪問目標所採取的路徑。tracert 有一個固定的時間等待響應(ICMP TTL到期訊息)。如果這個時間過了,它將打印出一系列的*號表明:在這個路徑上,這個裝置不能在給定的時間內發出ICMP TTL到期訊息的響應。然後,Tracert給TTL記數器加1,繼續進行。

2. 設計步驟

(1)載入套接字,建立套接字型檔;
使用Socket的程式在使用Socket之前必須呼叫WSAStartup函式,以後應用程式就可以呼叫所請求的Socket庫中的其他Socket函數了。
(2)用inet_addr()將輸入的點分十進位制的IP地址轉換為無符號長整型數,轉換不成功時,按域名解析得到IP地址;
gethostbyname()是查詢主機名最基本的函式,如果呼叫成功,就返回一個指向hosten結構的指標,該結構中含有對應於給定主機名的主機名字和地址資訊,用來承接域名解析的結構。
(3)設定傳送接收超時時間,即請求超時,設定接收、傳送超時的套接字;
(4)構造ICMP回顯請求訊息,並以TTL遞增順序傳送報文,填充ICMP報文中每次傳送時不變的欄位,構造ICMP頭;

(5)設定IP報頭的TTL欄位,填充ICMP報文中每次傳送變化的欄位,記錄序列號和當前時間;
(6)指定對方資訊,傳送TCP回顯請求資訊;
sendto()函式利用資料表的方式進行資料傳輸,指定哪個socket傳送給對方
(7)接收ICMP差錯報文並進行解析:如果有資料到達,解析資料包,如果到達目的地址,輸出IP地址;如果沒有資料到達,輸出接收超時,遞增TTL值,TTL增為最大時,若還沒有到達目的地址,退出迴圈,輸出目的地址不線上;
recvform()利用資料報方式進行資料傳輸,當recvfrom()返回時,(sockaddr*)&from包含實際存入from中的資料位元組數。Recvfrom函式返回接收到的位元組數或當出現錯誤時返回-1,並置相應的errno。
(8)重複(2)-(7),實現查詢一個範圍內的IP地址。

三、詳細設計

1. 程式流程圖

在這裡插入圖片描述

2. 實驗程式碼

#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdlib.h>
#include <sstream>
using namespace std;
#pragma comment(lib, "Ws2_32.lib")
const int ipAddressSize = 14;
//int count11=0;
//IP 報頭
typedef struct
{
    unsigned char hdr_len : 4; //4 位頭部長度
    unsigned char version : 4; //4 位版本號
    unsigned char tos; //8 位服務型別
    unsigned short total_len; //16 位總長度: 和頭部長度一起就能區分 頭 主體資料了
    unsigned short identifier; //16 位識別符號: 作用是分片後的重組
    unsigned short frag_and_flags; //3 位標誌加 13 位片偏移: 標誌:MF 1是否還有分配 0 沒有分片了
    //                         DF 0 可以分片
    // 片偏移:分片後的相對於原來的偏移
    unsigned char ttl; //8 位生存時間
    unsigned char protocol; //8 位上層協議號: 指出是何種協議
    unsigned short checksum; //16 位校驗和: 檢驗是否出錯
    unsigned long sourceIP; //32 位源 IP 地址
    unsigned long destIP; //32 位目的 IP 地址
} IP_HEADER;
//ICMP 報頭,一共八個位元組,前四個位元組為:型別(1位元組)、程式碼(1位元組)和檢驗和(2位元組)。後四個位元組取決於型別
typedef struct
{
    BYTE type; //8 位型別欄位:標識ICMP的作用
    BYTE code; //8 位程式碼欄位
    USHORT cksum; //16 位校驗和
    USHORT id; //16 位識別符號
    USHORT seq; //16 位序列號
} ICMP_HEADER;

//報文解碼結構
//接收到的資料快取是字元陣列char bufRev[],因此需要通過特定的解析(也就是拆成一段一段的)獲取想要的資訊
//把資訊封裝到結構體中,就比較方便的得到序列號、往返時間和目的IP了。
typedef struct
{
    USHORT usSeqNo; //序列號
    DWORD dwRoundTripTime; //往返時間
    in_addr dwIPaddr; //返回報文的 IP 地址
} DECODE_RESULT;
//計算網際校驗和函式
USHORT checksum(USHORT *pBuf, int iSize)
{
    unsigned long cksum = 0;
    while (iSize > 1)
    {
        cksum += *pBuf++;
        iSize -= sizeof(USHORT);
    }
    if (iSize)
    {
        cksum += *(UCHAR *)pBuf;
    }
    cksum = (cksum >> 16) + (cksum & 0xffff);
    cksum += (cksum >> 16);
    return (USHORT)(~cksum);
}
//對資料包進行解碼
// 1)接收到的Buf 2)接收到的資料長度 3)解析結果封裝到Decode 4)ICMP回顯型別 5)TIMEOUT時間
BOOL DecodeIcmpResponse2(char * pBuf, int iPacketSize, DECODE_RESULT &DecodeResult, BYTE
                         ICMP_ECHO_REPLY, BYTE ICMP_TIMEOUT)
{
    //查詢資料報大小合法性
    //pBuf的首地址,就是IP報的首地址
    IP_HEADER *pIpHdr = (IP_HEADER*)pBuf;
    int iIpHdrLen = pIpHdr->hdr_len * 4;
    if(iPacketSize < (int)(iIpHdrLen + sizeof(ICMP_HEADER)))
        return FALSE;
    // 根據 ICMP 報文型別提取 ID 欄位和序列號欄位
    //ICMP欄位包含在 IP資料段的起始位置,因此扣掉IP頭,得到的就是ICMP頭
    ICMP_HEADER *pIcmpHdr = (ICMP_HEADER *)(pBuf + iIpHdrLen);

    USHORT usID, usSquNo;
    if (pIcmpHdr->type == ICMP_ECHO_REPLY) // ICMP 回顯應答報文
    {
        usID = pIcmpHdr->id;//報文 ID
        usSquNo = pIcmpHdr->seq;//報文序列號
    }
    else if (pIcmpHdr->type == ICMP_TIMEOUT)//ICMP超時差錯報文
    {
        // 如果是TIMEOUT ,那麼在ICMP資料包中,會夾帶一個IP報(荷載IP)
        char * pInnerIpHdr = pBuf + iIpHdrLen + sizeof(ICMP_HEADER); // 荷載中的 IP 的頭
        int iInnerIPHdrLen = ((IP_HEADER*)pInnerIpHdr)->hdr_len * 4;// 荷載中的IP 頭長度
        ICMP_HEADER * pInnerIcmpHdr = (ICMP_HEADER*)(pInnerIpHdr + iInnerIPHdrLen); //荷載中的ICMP頭
        usID = pInnerIcmpHdr->id;// 報文ID
        usSquNo = pInnerIcmpHdr->seq; // 序列號
    }
    else
    {
        return false;
    }

    // 檢查 ID 和序列號以確定收到期待資料報
    if (usID != (USHORT)GetCurrentProcessId() || usSquNo != DecodeResult.usSeqNo)
    {
        return false;
    }
    // 記錄 IP 地址並計算往返時間
    DecodeResult.dwIPaddr.S_un.S_addr = pIpHdr->sourceIP;
    DecodeResult.dwRoundTripTime = GetTickCount() - DecodeResult.dwRoundTripTime;
    //處理正確收到的 ICMP 資料包
    if (pIcmpHdr->type == ICMP_ECHO_REPLY || pIcmpHdr->type == ICMP_TIMEOUT)
    {
        // 輸出往返時間資訊
        if (DecodeResult.dwRoundTripTime)
            cout << " " << DecodeResult.dwRoundTripTime << "ms" << flush;
        else
            cout << " " << "<1ms" << flush;
    }
    return true;
}


char * findNextIp(char * nowIp);

int main()
{
    //初始化 Windows sockets 網路環境
    WSADATA wsa;//儲存被WSAStartup函式呼叫後返回的Windows Sockets資料
    //使用Socket的程式在使用Socket之前必須呼叫WSAStartup函式,以後應用程式就可以呼叫所請求的Socket庫中的其他Socket函數了
    WSAStartup(MAKEWORD(2, 2), &wsa);//進行相應的socket庫繫結
    cout << "請輸入你要查詢的起始IP:" << endl;
    char IpAddressBeg[ipAddressSize]; // 255.255.255.255
    cin >> IpAddressBeg;
    cout << "請輸入你要查詢的終止IP:" << endl;
    char IpAddressEnd[ipAddressSize]; // 255.255.255.255
    cin >> IpAddressEnd;
    char nextIpAddress[17];
    strcpy(nextIpAddress, IpAddressBeg);
    while (strcmp(nextIpAddress, IpAddressEnd) != 0)
    {
        // 執行,單執行緒執行,實現後改成多執行緒
        //得到IP地址
        u_long ulDestIP = inet_addr(nextIpAddress);//inet_addr()的功能是將一個點分十進位制的IP轉換成一個無符號長整型數
        //轉換不成功時按域名解析
        if (ulDestIP == INADDR_NONE)
        {
            //gethostbyname()是查詢主機名最基本的函式
            //如果呼叫成功,就返回一個指向hosten結構的指標
            //該結構中含有對應於給定主機名的主機名字和地址資訊,用來承接域名解析的結構
            hostent * pHostent = gethostbyname(nextIpAddress);
            if (pHostent)//呼叫成功
            {
                //得到IP地址
                //套了兩層,IP和ICMP,ICMP是套在IP裡面的
                //h_addr返回主機IP地址
                //in_addr返回報文的IP地址
                //sin_addr.s_addr指向IP地址
                ulDestIP = (*(in_addr*)pHostent->h_addr).s_addr;
            }
            else
            {
                cout << "輸入的 IP 地址或域名無效!" << endl;
                WSACleanup();//解除與Socket庫的繫結並且釋放Socket庫所佔用的系統資源
                return 0;
            }
        }
        // 填充目的 sockaddr_in
        sockaddr_in destSockAddr;//sockaddr_in是Internet環境下套接字的地址形式
        //將指定的記憶體塊清零,使用結構前清零,而不讓結構體的成員數值具有不確定性,是一個好的程式設計習慣
        ZeroMemory(&destSockAddr, sizeof(sockaddr_in));
        destSockAddr.sin_family = AF_INET;//指代協議簇,在socket程式設計中只能是AF_INET
        destSockAddr.sin_addr.S_un.S_addr = ulDestIP;//按照網路位元組順序儲存IP地址
        //建立原始套接字
        //WSASocket()的傳送操作和接收操作都可以被重疊使用。接收函式可以被多次呼叫,發出接收緩衝區,準備接收到來的資料。傳送函式也可以被多次呼叫,組成一個傳送緩衝區佇列
        //如無錯誤發生,返回新套介面的描述字,否則的話,返回INVALID_SOCKET
        //AF_INET為地址簇描述,SOCK_RAW為新套介面的型別描述,SOCK_RAW為原始套接字,可處理PING報文等
        //IPPROTO_ICMP為套介面使用的協議,為ICMP;NULL是一個指向PROTOCOL_INFO結構的指標,該結構定義所建立套介面的特性
        //0為套介面的描述字;WSA_FLAG_OVERLAPPED為套介面屬性描述,WSA_FLAG_OVERLAPPED表示要使用重疊模型
        SOCKET sockRaw = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP, NULL, 0,
                                   WSA_FLAG_OVERLAPPED);
        // 設定傳送接收超時時間,即請求超時
        //比如請求B站的一個視訊,他超過一個時間沒回我,我就認為超時了
        //超時時間是可能變化的,這個超時時間用來儲存在不同的變數,它剛好在一個變數而已
        int iTimeout = 500;//如果沒超過超時時間就會一直等著,超過超時時間就不等了
        //接收超時
        //sockRaw為將要被設定或者獲取選項的套接字;SOL_SOCKET為在套接字級別上設定選項;SO_RCVTIMEO設定接收超時時間
        //(char*)&iTimeout指向存放選項值的緩衝區;sizeof(iTimeout)為緩衝區的長度
        setsockopt(sockRaw, SOL_SOCKET, SO_RCVTIMEO, (char *)&iTimeout, sizeof(iTimeout));
        //傳送超時
        //sockRaw為將要被設定或者獲取選項的套接字;SOL_SOCKET為在套接字級別上設定選項;SO_SNDTIMEO設定傳送超時時間
        //(char*)&iTimeout指向存放選項值的緩衝區;sizeof(iTimeout)為緩衝區的長度
        setsockopt(sockRaw, SOL_SOCKET, SO_SNDTIMEO, (char *)&iTimeout, sizeof(iTimeout));
        // 構造 ICMP 回顯請求訊息, 並以TTL 遞增順序傳送報文
        // ICMP 型別欄位
        //採用const修飾變數,功能是對變數宣告為只讀特性,並保護變數值以防被修改
        const BYTE ICMP_ECHO_REQUEST = 8;//請求回顯
        const BYTE ICMP_ECHO_REPLY = 0;//回顯應答
        //其他常量定義
        const int DEF_ICMP_DATA_SIZE = 32; // ICMP 報文預設資料欄位長度
        const int MAX_ICMP_PACKET_SIZE = 1024;//ICMP 報文最大長度(加上報頭)
        const DWORD DEF_ICMP_TIMEOUT = 500;// 回顯應答超時時間
        const int DEF_MAX_HOP = 20; // 最大跳站數
        // 填充 ICMP 報文中每次傳送時不變的欄位
        char IcmpSendBuf[sizeof(ICMP_HEADER) + DEF_ICMP_DATA_SIZE];// 傳送緩衝區
        memset(IcmpSendBuf, 0, sizeof(IcmpSendBuf));//初始化傳送緩衝區
        char IcmpRecvBuf[MAX_ICMP_PACKET_SIZE]; // 接收緩衝區
        memset(IcmpRecvBuf, 0, sizeof(IcmpRecvBuf)); //初始化接收緩衝區
        // 構造ICMP頭
        ICMP_HEADER * pIcmpHeader = (ICMP_HEADER*)IcmpSendBuf;
        pIcmpHeader->type = ICMP_ECHO_REQUEST; // 型別為請求回顯
        pIcmpHeader->code = 0;//程式碼欄位為0
        pIcmpHeader->id = (USHORT)GetCurrentProcessId();// ID欄位為當前程序號
        memset(IcmpSendBuf + sizeof(ICMP_HEADER), 'E', DEF_ICMP_DATA_SIZE);//資料欄位
        USHORT usSeqNo = 0; // ICMP 報文序列號
        int iTTL = 1; // TTL初始化值為1
        BOOL bReachDestHost = FALSE; // 迴圈退出標誌
        int iMaxHot = DEF_MAX_HOP; // 最大迴圈數
        DECODE_RESULT DecodeResult;// 傳遞給報文解碼函式的結構化引數
        //int count11=0;
        while (!bReachDestHost && iMaxHot--)
        {
            bReachDestHost = FALSE;
            // 設定 IP 報頭的 TTL 欄位
            //sockRaw為將要被設定或者獲取選項的套接字;IPPROTO_IP為套介面使用的協議,為IP;IP_TTL為設定IP報頭的TTL欄位
            //(char*)&iTTL指向存放選項值的緩衝區;sizeof(iTTL)為緩衝區的長度
            setsockopt(sockRaw, IPPROTO_IP, IP_TTL, (char *)&iTTL, sizeof(iTTL));
            cout << iTTL << flush; // 輸出當前序號,flush的作用是重新整理緩衝區
            // 填充 ICMP報文中每次傳送變化的欄位
            ((ICMP_HEADER *)IcmpSendBuf)->cksum = 0;//校驗和為0
            ((ICMP_HEADER *)IcmpSendBuf)->seq = htons(usSeqNo++);// 填充序列號
            ((ICMP_HEADER *)IcmpSendBuf)->cksum = checksum((USHORT *)IcmpSendBuf,
                                                  sizeof(ICMP_HEADER) + DEF_ICMP_DATA_SIZE); //計算校驗和

            // 記錄序列號和當前時間
            DecodeResult.usSeqNo = ((ICMP_HEADER*)IcmpSendBuf)->seq;//當前序號
            DecodeResult.dwRoundTripTime = GetTickCount();// 當前時間

            // 指定對方資訊
            // 傳送 TCP 回顯請求資訊
            //sendto()利用資料報的方式進行資料傳輸
            // 1)指定哪個Socket發給對方 2)傳送的資料 3)flag 4)目的地址  5)目的地址的sockaddr_in結構
            sendto(sockRaw, IcmpSendBuf, sizeof(IcmpSendBuf), 0, (sockaddr*)&destSockAddr, sizeof(destSockAddr));

            //接收 ICMP 差錯報文並進行解析
            sockaddr_in from; // 對端 socket地址,對方的
            int iFromLen = sizeof(from);//地址結構大小
            int iReadDataLen;// 接收資料長度

            // 接收正常的話,這個迴圈只會執行一次
            while (true)
            {
                //接收資料
                //recvfrom()利用資料報方式進行資料傳輸
                //當recvfrom()返回時,(sockaddr*)&from包含實際存入from中的資料位元組數。
                //Recvfrom()函式返回接收到的位元組數或當出現錯誤時返回-1,並置相應的errno。
                iReadDataLen = recvfrom(sockRaw, IcmpRecvBuf, MAX_ICMP_PACKET_SIZE, 0, (sockaddr*)&from, &
                                        iFromLen);

                if (iReadDataLen != SOCKET_ERROR) // 有資料到達
                {
                    //解析資料包
                    // 1)接收到的Buf 2)接收到的資料長度 3)解析結果封裝到Decode 4)ICMP回顯型別 5)TIMEOUT時間
                    if (DecodeIcmpResponse2(IcmpRecvBuf, iReadDataLen, DecodeResult, ICMP_ECHO_REPLY, DEF_ICMP_TIMEOUT))
                    {
                        // 到達目的地,退出迴圈
                        //返回報文的IP地址等於輸入的IP地址
                        if (DecodeResult.dwIPaddr.S_un.S_addr == destSockAddr.sin_addr.S_un.S_addr)
                        {
                            bReachDestHost = true;
                            // 輸出 IP 地址
                            //inet_ntoa()功能是將網路地址轉換成“.”點隔的字串格式。
                            cout << '\t' << inet_ntoa(DecodeResult.dwIPaddr) << endl;
                            strcpy(nextIpAddress, inet_ntoa(DecodeResult.dwIPaddr));
                            break;
                        }

                    }

                }
                //WSAGetLastError()當一特定的Sockets API函式指出一個錯誤已經發生,該函式就應呼叫來獲得對應的錯誤程式碼。
                //WSAETIMEDOUT在嘗試連線超時,而不建立連線。
                else if (WSAGetLastError() == WSAETIMEDOUT) //接收超時,輸出*號
                {
                    cout << " *" << '\t' << "Request timed out." << endl;
                    break;
                }
                else
                {
                    break;
                }
            }
            iTTL++;//遞增TTL值
        }

        cout << "查詢: " << nextIpAddress << "結果為 ->" << (bReachDestHost ? "線上" : "不線上") << endl;
        //if nextIpAddress ==bReachDestHost;
        // 向下推
        strcpy(nextIpAddress, findNextIp(nextIpAddress));
    }
    return 0;
}

char * findNextIp(char * nowIp)
{
    char nextIpAddress[ipAddressSize];
    char z[4][4];
    int idxIp = 0, idxj = 0;
    for (int i = 0; i < strlen(nowIp); i++)
    {
        if (nowIp[i] == '.')
        {
            z[idxIp][idxj] = '\0';

            idxIp++;
            idxj = 0;

            continue;

        }
        z[idxIp][idxj++] = nowIp[i];
    }
    z[idxIp][idxj] = '\0';

    //for (int i = 0; i < 4; i++)
    //{
    //	puts(z[i]);
    //}
    //cout << endl;

    for (int i = 3; i >= 0; i--)
    {
        if (strcmp("254", z[i]) == 0)
        {
            strcpy(z[i], "1"); // 這裡讓ip 1-254
        }
        else
        {
            int x;
            x = atoi(z[i]) + 1;
            //stringstream ss;
            //ss << x;
            //string z[i] = ss.str();
            itoa(x,z[i],10); // 第三個引數是 int的進位制

            break;
        }
    }


    char retIp[ipAddressSize];
    strcpy(retIp, z[0]);
    char c[2] = ".";
    for (int i = 1; i < 4; i++)
    {
        strcat(retIp, c);
        strcat(retIp, z[i]);
    }
    /*cout << retIp << endl;*/

    return retIp;
}

四、實驗結果

在這裡插入圖片描述