通信編程:基於 ICMP 編寫 ping 程序


ICMP 協議

因特網控制報文協議

ICMP,即因特網控制報文協議,在主機和路由器之間起到溝通網絡層信息的作用。最典型的用途就是差錯報告,它允許主機或路由器報告查錯情況和提交有關異常情況的報告。例如網絡通不通、主機是否可達、路由是否可用等網絡本身的消息,這些控制消息雖對於數據的傳遞起着重要的作用。ICMP 報文作為 IP 有效載荷承載的,因此雖然 ICMP 被認為是 IP 的一部分,但在體系結構上 ICMP 位於 IP 之上。當主機接收到指明上層協議為 ICMP 的 IP 數據報時,該數據報分解的內容應當交給 ICMP。

ICMP 報文格式

ICMP 報文包括 IP 頭部、ICMP 頭部和 ICMP 報文 3 個部分,ICMP 報文是作為 IP 有效載荷承載的。

字段 說明
Type ICMP 的類型,標識生成的錯誤報文;
Code 進一步划分 ICMP 的類型,該字段用來查找產生錯誤的原因;
Checksum 校驗碼,字段包含有從 ICMP 報頭和數據部分計算得來的,用於檢查錯誤的數據;
ID ID 值,在 Echo Reply 類型的消息中要返回這個字段;
Sequence 這個字段包含一個序號,在 Echo Reply 類型的消息中要返回這個字段。

ICMP 報文類型

常用報文類型如下:

對於 Ping 程序而言是會發送一個回顯請求(類型 8 編碼 0)報文給目的主機,目的主機收到之后就發送回顯應答(類型 0 編碼 0)報文進行回顯。TTL 報文(類型 11 編碼 0)是在 Traceroute 程序中,路由器檢查到 Traceroute 發出的 IP 數據報中 TTL 正好過期,因此路由器就需要丟包並且發送該警告報文返回源主機。源主機就可以得到路由器的 IP 地址,以此達到路由追蹤的目的。

原始套接字

原始套接字是允許訪問底層傳輸協議的一種套接字類型,可以直接從應用層將數據送到網絡層,在網絡層對協議進行解析,起到一種“隔山打牛”的作用。原始套接字有兩種類型,第一種類型是在 IP 頭中使用預定義的協議,例如 ICMP,第二種類型是在 IP 頭中使用自定義的協議。原始套接字提供管理下層傳輸的能力,它們可能會被惡意利用,因此為了保證安全性僅 Administrator 組的成員能夠創建 SOCK_RAW 類型的套接字。
創建原始套接字的函數也是 socket 或者WSASocket,要將套接字類型指定為SOCK_RAW,第 3 個參數 protocol 的值將成為 IP 頭中協議域的值。也就是說如果創建原始套接字時,IPPROTO_ICMP 指定要使用 ICMP 協議。

SOCKET sRaw = ::Socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);

協議類型可以使用 ICMP,也可以使用 IGMP、UDP、IP,對應的宏定義分別是 IPPROTO_IGMP、IPPROTO_UDP、IPPROTO_IP或IPPROTO_RAW,其中協議標志 IPPROTO_UDP、IPPROTO_IP和IPPROTO_RAW 需要有效 IP_HDRINCL 選項。使用恰當的協議標志創建原始套接字之后,便可以在發送和接收調用中使用此套接字句柄了。

ping 程序編寫

Ping 經常用來確定特定的主機是否存在,是否可以到達。通過產生一個 ICMP 回顯請求發送給目的主機,根據應答的報文情況,便可以確定是否可以成功到達那個機器。

ICMP 頭結構

初始化 ICMP 頭時先初始化消息類型和代碼域,之后回顯請求頭,然后編寫校驗和的計算方法。發送ICMP報文時,必須由程序自己計算校驗和,將它填入 ICMP 頭部對應的域中。校驗和的計算方法是:將數據以字為單位累加到一個雙字中,如果數據長度為奇數,最后一個字節將被擴展到字,累加的結果是一個雙字,最后將這個雙字的高 16 位和低 16 位相加后取反,便得到了校驗和。

#include "initsock.h"

#include <iostream>
using namespace std;

CInitSock initSock;     // 初始化Winsock庫

typedef struct icmp_hdr
{
    unsigned char   icmp_type;      // 消息類型
    unsigned char   icmp_code;      // 代碼
    unsigned short  icmp_checksum;  // 校驗和
    // 下面是回顯頭
    unsigned short  icmp_id;        // 用來惟一標識此請求的ID號,通常設置為進程ID
    unsigned short  icmp_sequence;  // 序列號
    unsigned long   icmp_timestamp; // 時間戳
} ICMP_HDR, * PICMP_HDR;

USHORT checksum(USHORT* buff, int size)
{
    unsigned long cksum = 0;
    //將數據以字為單位累加到 cksum 中
    while (size > 1)
    {
        cksum += *buff++;
        size -= sizeof(USHORT);
    }
    // 如果為奇數,將最后一個字節擴展到雙字,再累加到cksum中
    if (size)
    {
        cksum += *(UCHAR*)buff;
    }
    // 將 32 位的 chsum 高 16 位和低 16 位相加,然后取反
    cksum = (cksum >> 16) + (cksum & 0xffff);
    cksum += (cksum >> 16);    //進位處理
    return (USHORT)(~cksum);
}

initsock.h

#include <winsock2.h>
#pragma comment(lib, "WS2_32")  // 鏈接到 WS2_32.lib

class CInitSock
{
public:
    /*CInitSock 的構造器*/
    CInitSock(BYTE minorVer = 2, BYTE majorVer = 2)
    {
        // 初始化WS2_32.dll
        WSADATA wsaData;
        WORD sockVersion = MAKEWORD(minorVer, majorVer);
        if (::WSAStartup(sockVersion, &wsaData) != 0)
        {
            exit(0);
        }
    }

    /*CInitSock 的析構器*/
    ~CInitSock()
    {
        ::WSACleanup();
    }
};

主函數

Ping 的編寫步驟如下:

  1. 創建協議類型為 IPPROTO_ICMP 的原始套接字,設置套接字的屬性;
  2. 創建並初始化 ICMP 封包;
  3. 調用 sendto 函數向遠程主機發送ICMP請求;
  4. 調用 recvfrom 函數接收ICMP響應。
int main()
{
    // 目的IP地址,即要Ping的IP地址
    char szDestIp[] = "36.152.44.96";

    // 創建原始套節字
    SOCKET sRaw = ::socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);

    // 設置目的地址
    SOCKADDR_IN dest;
    dest.sin_family = AF_INET;
    dest.sin_port = htons(0);
    dest.sin_addr.S_un.S_addr = inet_addr(szDestIp);

    // 創建ICMP封包
    char buff[sizeof(ICMP_HDR) + 32];
    ICMP_HDR* pIcmp = (ICMP_HDR*)buff;
    // 填寫ICMP封包數據
    pIcmp->icmp_type = 8;   // 請求一個ICMP回顯
    pIcmp->icmp_code = 0;
    pIcmp->icmp_id = (USHORT)::GetCurrentProcessId();
    pIcmp->icmp_checksum = 0;
    pIcmp->icmp_sequence = 0;
    // 填充數據部分,可以為任意
    memset(&buff[sizeof(ICMP_HDR)], 'E', 32);

    // 開始發送和接收ICMP封包
    USHORT  nSeq = 0;
    char recvBuf[1024];
    SOCKADDR_IN from;
    int nLen = sizeof(from);
    while (TRUE)
    {
        static int nCount = 0;
        int nRet;
        if (nCount++ == 4) {
            break;
        }
        pIcmp->icmp_checksum = 0;
        pIcmp->icmp_timestamp = ::GetTickCount();
        pIcmp->icmp_sequence = nSeq++;
        pIcmp->icmp_checksum = checksum((USHORT*)buff, sizeof(ICMP_HDR) + 32);
        nRet = ::sendto(sRaw, buff, sizeof(ICMP_HDR) + 32, 0, (SOCKADDR*)&dest, sizeof(dest));
        if (nRet == SOCKET_ERROR)
        {
            cout << " sendto() failed: " << ::WSAGetLastError() << endl;
            return -1;
        }
        nRet = ::recvfrom(sRaw, recvBuf, 1024, 0, (sockaddr*)&from, &nLen);
        if (nRet == SOCKET_ERROR)
        {
            if (::WSAGetLastError() == WSAETIMEDOUT)
            {
                cout << " timed out" << endl;
                continue;
            }
            cout << " recvfrom() failed: " << ::WSAGetLastError() << endl;
            return -1;
        }

        // 下面開始解析接收到的ICMP封包
        int nTick = ::GetTickCount();
        if (nRet < sizeof(IPHeader) + sizeof(ICMP_HDR))
        {
            cout << " Too few bytes from %s" << ::inet_ntoa(from.sin_addr) << endl;
        }
        // 接收到的數據中包含IP頭,IP頭大小為20個字節,所以加20得到ICMP頭
        ICMP_HDR* pRecvIcmp = (ICMP_HDR*)(recvBuf + sizeof(IPHeader));
        if (pRecvIcmp->icmp_type != 0)   // 回顯
        {
            cout << " nonecho type " << pRecvIcmp->icmp_type << " recvd" << endl;
            return -1;
        }

        if (pRecvIcmp->icmp_id != ::GetCurrentProcessId())
        {
            cout << " someone else's packet!" << endl;
            return -1;
        }

        cout << nRet << " bytes from " << inet_ntoa(from.sin_addr) 
            << " icmp_seq = " << pRecvIcmp->icmp_sequence << "." 
            << " time: " << nTick - pRecvIcmp->icmp_timestamp << "ms" << endl;

        ::Sleep(1000);
    }

    return 0;
}

運行效果

參考資料

《Windows 網絡與通信編程》,陳香凝 王燁陽 陳婷婷 張錚 編著,人民郵電出版社


免責聲明!

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



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