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 的編寫步驟如下:
- 創建協議類型為 IPPROTO_ICMP 的原始套接字,設置套接字的屬性;
- 創建並初始化 ICMP 封包;
- 調用 sendto 函數向遠程主機發送ICMP請求;
- 調用 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 網絡與通信編程》,陳香凝 王燁陽 陳婷婷 張錚 編著,人民郵電出版社
