Ping 使用 Internet
控制訊息協定(ICMP
)來測試主機之間的連線。當用戶傳送一個 ping
請求時,則對應的傳送一個 ICMP Echo
請求訊息到目標主機,並等待目標主機回覆一個 ICMP Echo
迴應訊息。如果目標主機接收到請求並且網路連線正常,則會返回一個迴應訊息,表示主機之間的網路連線是正常的。如果目標主機沒有收到請求訊息或網路連線不正常,則不會有迴應訊息返回。
Ping 工作的步驟如下:
ICMP Echo
請求訊息到目標主機。IP
地址是否正確,並回復一個ICMP Echo
迴應訊息表示收到請求。Ping的實現依賴於ICMP
協定,Internet控制訊息協定(Internet Control Message Protocol,簡稱 ICMP)是一種在IP
網路上傳送控制訊息的協定。主要是用於在 IP
網路上進行錯誤處理和診斷。ICMP協定是執行在網路層的協定,它的主要作用是向源主機和目標主機傳送控制訊息,幫助網路診斷和監控。這些控制訊息通常是由網路裝置(如路由器、交換機、防火牆等)生成或捕獲,並在整個網路傳輸。
ICMP協定的訊息格式通常由兩個部分組成:訊息頭和資料。其中,訊息頭包含以下欄位:
ICMP 協定中常見的訊息型別包括:
在Windows
平臺下要實現Ping
命令有多種方法,首先我們先來講解第一種實現方式,通過自己構造ICMP
封包並行包實現,首先該功能的實現需要定義一個icmp_header
頭部,並定義好所需要的傳送與迴應定義,如下所示;
// ICMP頭部定義部分
struct icmp_header
{
unsigned char icmp_type; // 訊息型別
unsigned char icmp_code; // 程式碼
unsigned short icmp_checksum; // 校驗和
unsigned short icmp_id; // ICMP唯一ID
unsigned short icmp_sequence; // 序列號
unsigned long icmp_timestamp; // 時間戳
};
// 計算出ICMP頭部長度
#define ICMP_HEADER_SIZE sizeof(icmp_header)
// ICMP回送請求訊息程式碼
#define ICMP_ECHO_REQUEST 0x08
// ICMP回送響應訊息程式碼
#define ICMP_ECHO_REPLY 0x00
當有了結構體定義那麼接著就需要實現一個ICMP
校驗和的計算方法,ICMP
報文檢驗和是一種用於檢測 ICMP
報文資料正確性的校驗和。它是 ICMP
協定中一種重要的錯誤檢測機制,用於驗證傳送和接收的 ICMP
報文的資料是否完整、正確。
校驗和計算方法如下:
當 ICMP
接收到 ICMP
報文時,將立即計算校驗和,比對接收到的校驗和值與計算所得的校驗和值是否相同,從而決定 ICMP
報文是否正確接收及響應。這樣做的好處是可以有效地檢測資料在傳輸過程中的誤碼、中間路由裝置的錯誤操作等問題,保障 ICMP
報文的正確性。
根據上述描述,計算校驗和CheckSum
函數,首先對報文的資料進行分組,並依次計算每個16
位數位的和。當相加的結果有進位時,將進位加到低位上,並將進位部分加到下一組中。處理完所有數位之後,還需要對結果進行二進位制反轉,得到最終的校驗和值。
// 計算校驗和
unsigned short CheckSum(struct icmp_header *picmp, int len)
{
long sum = 0;
unsigned short *pusicmp = (unsigned short *)picmp;
// 將資料按16位元分組,相鄰的兩個16位元取出並相加,直到處理完所有資料
while (len > 1)
{
sum += *(pusicmp++);
// 如果相加的結果有進位,則將進位加到低16位元上
if (sum & 0x80000000)
{
sum = (sum & 0xffff) + (sum >> 16);
}
// 減去已經處理完的位元組數
len -= 2;
}
// 如果資料的位元組數為奇數,則將最後一個位元組視為16位元,高8位元設為0,低8位元取餘部分。
if (len)
{
sum += (unsigned short)*(unsigned char *)pusicmp;
}
// 如果計算完校驗和後還有進位,則將進位加到低16位元上
while (sum >> 16)
{
sum = (sum & 0xffff) + (sum >> 16);
}
// 取反得到最終的校驗和
return (unsigned short)~sum;
}
接著就是實現ICMP測試函數,如下函數首先進行初始化,並建立原始通訊端,然後構造 ICMP
報文,計算報文的校驗和。接著傳送 ICMP
報文,並接收 ICMP
回覆報文,解析其中的資訊,判斷延遲超時,最後返回 ping
測試結果。
傳送 ICMP
報文使用 sendto
函數,第一個引數是原始通訊端,第二個引數是 ICMP
報文資料快取區,第三個引數是快取區的長度,第四個引數是標誌,第五個引數是目的地址資訊。接收 ICMP
回覆報文使用 recvfrom
函數,第一個引數和第五個引數與 sendto
函數相同。函數返回時,判斷接收到的 IP
地址是否與傳送 ICMP
報文的 IP
地址相同,如果相同,解析 ICMP
回覆報文中的資訊並返回 true
,否則返回 false
。
在 ICMP
報文構造中,使用了 Winsock
函數庫中的 inet_addr
將 IP
地址轉換為網路位元組序。在計算 ICMP
報文的校驗和時,呼叫了 CheckSum
函數。
BOOL MyPing(char *szDestIp)
{
BOOL bRet = TRUE;
WSADATA wsaData;
int nTimeOut = 1000;
char szBuff[ICMP_HEADER_SIZE + 32] = { 0 };
icmp_header *pIcmp = (icmp_header *)szBuff;
char icmp_data[32] = { 0 };
// 初始化Winsock動態連結庫
WSAStartup(MAKEWORD(2, 2), &wsaData);
// 建立原始通訊端
SOCKET s = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP);
// 設定接收超時
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (char const*)&nTimeOut, sizeof(nTimeOut));
// 設定目的地址
sockaddr_in dest_addr;
dest_addr.sin_family = AF_INET;
dest_addr.sin_addr.S_un.S_addr = inet_addr(szDestIp);
dest_addr.sin_port = htons(0);
// 構造ICMP封包
pIcmp->icmp_type = ICMP_ECHO_REQUEST;
pIcmp->icmp_code = 0;
pIcmp->icmp_id = (USHORT)::GetCurrentProcessId();
pIcmp->icmp_sequence = 0;
pIcmp->icmp_timestamp = 0;
pIcmp->icmp_checksum = 0;
// 拷貝ICMP協定中附帶的資料
memcpy((szBuff + ICMP_HEADER_SIZE), "abcdefghijklmnopqrstuvwabcdefghi", 32);
// 計算校驗和
pIcmp->icmp_checksum = CheckSum((struct icmp_header *)szBuff, sizeof(szBuff));
// 接收ping返回的ICMP封包
sockaddr_in from_addr;
char szRecvBuff[1024];
int nLen = sizeof(from_addr);
// 傳送UDP封包
sendto(s, szBuff, sizeof(szBuff), 0, (SOCKADDR *)&dest_addr, sizeof(SOCKADDR));
// 等待響應
recvfrom(s, szRecvBuff, MAXBYTE, 0, (SOCKADDR *)&from_addr, &nLen);
// 判斷接收到的是否是自己請求的地址
if (lstrcmp(inet_ntoa(from_addr.sin_addr), szDestIp))
{
bRet = FALSE;
}
else
{
// 如果是自己請求的地址,則解析 ICMP 回覆報文中的資訊
struct icmp_header *pIcmp1 = (icmp_header *)(szRecvBuff + 20);
printf("%s \r\n", inet_ntoa(from_addr.sin_addr));
}
return bRet;
}
當讀者有了上述函數封裝那麼實現Ping測試將變得很容易,首先如下呼叫範例中,通過GetHostByName
函數獲取到對應域名的IP地址資訊返回字串,並將該字串傳入MyPing
函數內,該函數會測試當前主機是否可通訊,如果可以返回狀態值1,否則返回0。
int main(int argc, char **argv)
{
// 獲得指定網址的IP地址
char * ptr = GetHostByName("www.lyshark.com");
// 開始測試
for (size_t i = 0; i < 5; i++)
{
int ret = MyPing(ptr);
printf("測試結果 = %d \n", ret);
}
system("pause");
return 0;
}
執行程式碼後讀者可看到如下圖所示的提示資訊;
除了通過自己封裝介面外,Windows系統中還為我們提供了一個專用函數IcmpSendEcho
,該函數用於通過 ICMP
協定向遠端主機傳送 Echo
請求並接收 Echo
回覆。如果傳送 Echo
請求併成功接收 Echo
回覆,則函數返回值為非零,否則為零。
該函數的宣告如下:
BOOL IcmpSendEcho
(
HANDLE IcmpHandle,
IPAddr DestinationAddress,
LPVOID RequestData,
WORD RequestSize,
PIP_OPTION_INFORMATION RequestOptions,
LPVOID ReplyBuffer,
DWORD ReplySize,
DWORD Timeout
);
函數引數:
如下函數則是通過IcmpCreateFile
和IcmpSendEcho
函數實現的Ping測試,函數首先將 IP
地址轉換為網路位元組序,建立 ICMP
控制程式碼並初始化 IP
選項資訊。然後,設定要傳送的 ICMP
資料包文,和接收 ICMP
資料包文的大小和緩衝區。接著傳送 ICMP
資料包文,等待接收回復,並將回覆解析為 ICMP_ECHO_REPLY
結構體。最後,判斷回覆的狀態,如果不為 0
則返回失敗,否則輸出回覆資訊並返回成功。
// 呼叫API實現ping
bool IcmpPing(char *Address)
{
// 設定超時為1000ms
DWORD timeOut = 1000;
// IP地址轉為網路位元組序
ULONG hAddr = inet_addr(Address);
HANDLE handle = IcmpCreateFile();
IP_OPTION_INFORMATION ipoi;
memset(&ipoi, 0, sizeof(IP_OPTION_INFORMATION));
// Time-To-Live
ipoi.Ttl = 64;
// 設定傳送封包
unsigned char SendData[32] = { "send icmp pack" };
int repSize = sizeof(ICMP_ECHO_REPLY)+32;
// 設定接收封包
unsigned char pReply[128];
ICMP_ECHO_REPLY* pEchoReply = (ICMP_ECHO_REPLY*)pReply;
// 傳送ICMP資料包文
DWORD nPackets = IcmpSendEcho(handle, hAddr, SendData, sizeof(SendData), &ipoi, pReply, repSize, timeOut);
if (pEchoReply->Status != 0)
{
IcmpCloseHandle(handle);
return false;
}
in_addr inAddr;
inAddr.s_addr = pEchoReply->Address;
printf("回覆地址: %13s 狀態: %1d 初始TTL: %3d 回覆: TTL: %3d \n",
inet_ntoa(inAddr), pEchoReply->Status, ipoi.Ttl, pEchoReply->Options.Ttl);
return true;
}
該段程式碼的呼叫與上述一致,讀者只需要傳入主機IP地址的字串即可,具體呼叫實現如下所示;
int main(int argc, char *argv[])
{
// 解析域名
char * HostAddress = GetHostByName("www.lyshark.com");
printf("網站IP地址 = %s \n", HostAddress);
// 呼叫Ping
for (int x = 0; x < 3; x++)
{
IcmpPing(HostAddress);
Sleep(1000);
}
system("pause");
return 0;
}
執行程式碼後讀者可看到如下圖所示的提示資訊;
通過使用Ping
命令我們還可以實現針對主機路由的追蹤功能,路由追蹤功能的原理是,它實際上是傳送一系列ICMP
封包,封包每經過一個路由節點則TTL值會減去1,假設TTL
值等於0時封包還沒有到達目標主機,那麼該路由則會回覆給目標主機一個封包不可達,由此我們就可以獲取到目標主機的IP地址。
其跟蹤原理如下:
TTL
為1的包,這樣到達第一個路由器的時候就已經超時了,第一個路由器就會返回一個ICMP
通知,該通知包含了對端的IP
地址,這樣就能夠記錄下所經過的第一個路由器的IP。TTL
加1,讓其能夠安全的通過第一個路由器,而第二個路由器的的處理過程會自動丟包,發通知說包超時了,這樣記錄下第二個路由器IP,由此能夠一直進行下去,直到這個封包到達目標主機,由此列印出全部經過的路由器。由上述流程並配合使用IcmpSendEcho
函數設定預設最大跳數為64,通過不間斷的迴圈即可輸出本機封包到達目標之間的所有路由資訊,程式碼片段如下所示;
// 實現路由跟中
void Tracert(char *Address)
{
ULONG hAddr = inet_addr(Address);
HANDLE handle = IcmpCreateFile();
IP_OPTION_INFORMATION ipoi;
memset(&ipoi, 0, sizeof(IP_OPTION_INFORMATION));
unsigned char SendData[32] = { "send ttl pack" };
int repSize = sizeof(ICMP_ECHO_REPLY)+32;
unsigned char pReply[128];
ICMP_ECHO_REPLY* pEchoReply = (ICMP_ECHO_REPLY*)pReply;
for (int ttl = 1; ttl < 64; ttl++)
{
ipoi.Ttl = ttl;
DWORD nPackets = IcmpSendEcho(handle, hAddr, SendData, sizeof(SendData), &ipoi, pReply, repSize, 1000);
if (pEchoReply->Status != 0)
{
in_addr inAddr;
inAddr.s_addr = pEchoReply->Address;
printf("-> 第 %2d 跳 --> 地址: %15s -> TTL: %2d \n", ttl, inet_ntoa(inAddr), pEchoReply->Options.Ttl);
}
}
IcmpCloseHandle(handle);
}
上述程式碼讀者可自行執行並傳入Tracert(HostAddress)
被測試主機IP地址,即可輸出當前經過路由的完整資訊,如果路由TTL
為0則可能是對端路由過濾掉了ICMP
請求,如下圖所示;
本文作者: 王瑞
本文連結: https://www.lyshark.com/post/6ffe4618.html
版權宣告: 本部落格所有文章除特別宣告外,均採用 BY-NC-SA 許可協定。轉載請註明出處!