Ping 使用 Internet 控制消息協議(ICMP)來測試主機之間的連接。當用戶發送一個 ping 請求時,則對應的發送一個 ICMP Echo 請求消息到目標主機,並等待目標主機回覆一個 ICMP Echo 迴應消息。如果目標主機接收到請求並且網絡連接正常,則會返回一個迴應消息,表示主機之間的網絡連接是正常的。如果目標主機沒有收到請求消息或網絡連接不正常,則不會有迴應消息返回。
編譯報錯問題解決
在Windows
環境下編程不可避免的會用到windows.h
和winsock.h
頭文件,在默認情況下windows.h
頭文件會包含winsock.h
,此時當嘗試包含winsock.h
時就會出現頭文件定義衝突的情況。解決這個衝突的方式有兩種,第一種,在頭部定義#define WIN32_LEAN_AND_MEAN
來主動去除winsock.h
頭文件包含。第二種是將#include <winsock2.h>
頭文件,放在#include<windows.h>
之前。兩種方式均可,這些方法在進行Windows
套接字編程時非常重要,可以防止頭文件衝突,確保編譯順利進行。
Ping頭文件
如下頭文件代碼定義了幾個結構體,用於表示IP協議頭、ICMP協議頭和Ping的回覆信息。這些結構體主要用於網絡編程中,解析和構建網絡數據包。
#pragma once
#include <winsock2.h>
#pragma comment(lib, "WS2_32")
#define DEF_PACKET_SIZE 32
#define ECHO_REQUEST 8
#define ECHO_REPLY 0
struct IPHeader
{
BYTE m_byVerHLen; // 4位版本+4位首部長度
BYTE m_byTOS; // 服務類型
USHORT m_usTotalLen; // 總長度
USHORT m_usID; // 標識
USHORT m_usFlagFragOffset; // 3位標誌+13位片偏移
BYTE m_byTTL; // TTL
BYTE m_byProtocol; // 協議
USHORT m_usHChecksum; // 首部檢驗和
ULONG m_ulSrcIP; // 源IP地址
ULONG m_ulDestIP; // 目的IP地址
};
struct ICMPHeader
{
BYTE m_byType; // 類型
BYTE m_byCode; // 代碼
USHORT m_usChecksum; // 檢驗和
USHORT m_usID; // 標識符
USHORT m_usSeq; // 序號
ULONG m_ulTimeStamp; // 時間戳(非標準ICMP頭部)
};
struct PingReply
{
USHORT m_usSeq; // 來源IP
DWORD m_dwRoundTripTime; // 時間戳
DWORD m_dwBytes; // 返回長度
DWORD m_dwTTL; // TTL值
};
class CPing
{
public:
CPing(); // 構造函數
~CPing(); // 析構函數
// 執行 Ping 操作的方法,傳入目標 IP 地址或域名、PingReply 結構體和超時時間
BOOL Ping(DWORD dwDestIP, PingReply *pPingReply = NULL, DWORD dwTimeout = 2000);
BOOL Ping(char *szDestIP, PingReply *pPingReply = NULL, DWORD dwTimeout = 2000);
private:
// Ping 核心方法,傳入目標 IP 地址、PingReply 結構體和超時時間
BOOL PingCore(DWORD dwDestIP, PingReply *pPingReply, DWORD dwTimeout);
// 計算檢驗和的方法,傳入緩衝區和大小
USHORT CalCheckSum(USHORT *pBuffer, int nSize);
// 獲取時鐘計時器的校準值
ULONG GetTickCountCalibrate();
private:
SOCKET m_sockRaw; // 原始套接字
WSAEVENT m_event; // WSA 事件
USHORT m_usCurrentProcID; // 當前進程 ID
char *m_szICMPData; // ICMP 數據
BOOL m_bIsInitSucc; // 初始化是否成功
private:
static USHORT s_usPacketSeq; // 靜態變量,用於記錄 ICMP 包的序列號
};
下面是對每個結構體成員的簡要說明:
- IPHeader 結構體:
m_byVerHLen
: 4位版本號 + 4位首部長度。m_byTOS
: 服務類型。m_usTotalLen
: 總長度。m_usID
: 標識。m_usFlagFragOffset
: 3位標誌 + 13位片偏移。m_byTTL
: 生存時間。m_byProtocol
: 協議類型。m_usHChecksum
: 首部檢驗和。m_ulSrcIP
: 源IP地址。m_ulDestIP
: 目的IP地址。
- ICMPHeader 結構體:
m_byType
: ICMP類型。m_byCode
: ICMP代碼。m_usChecksum
: 檢驗和。m_usID
: 標識符。m_usSeq
: 序號。m_ulTimeStamp
: 時間戳(非標準ICMP頭部)。
- PingReply 結構體:
m_usSeq
: 序列號。m_dwRoundTripTime
: 往返時間。m_dwBytes
: 返回長度。m_dwTTL
: TTL值。
這些結構體主要用於在網絡編程中處理與IP、ICMP和Ping相關的數據包。在實際應用中,可以使用這些結構體來解析接收到的網絡數據包,或者構建要發送的數據包。
類成員說明:
m_sockRaw
: 用於發送原始套接字的成員變量。m_event
: WSA 事件。m_usCurrentProcID
: 當前進程 ID。m_szICMPData
: ICMP 數據。m_bIsInitSucc
: 初始化是否成功的標誌。s_usPacketSeq
: 靜態變量,用於記錄 ICMP 包的序列號。
類方法說明:
Ping
: 執行 Ping 操作的方法,可以傳入目標 IP 地址或域名、PingReply 結構體和超時時間。PingCore
: Ping 核心方法,用於發送 ICMP 數據包,計算往返時間等。CalCheckSum
: 計算檢驗和的方法。GetTickCountCalibrate
: 獲取時鐘計時器的校準值。
MyPing實現
1. CPing 構造函數和析構函數
CPing::CPing() : m_szICMPData(NULL), m_bIsInitSucc(FALSE)
{
// ...(省略其他初始化代碼)
m_szICMPData = (char*)malloc(DEF_PACKET_SIZE + sizeof(ICMPHeader));
if (m_szICMPData == NULL)
{
m_bIsInitSucc = FALSE;
}
}
CPing::~CPing()
{
WSACleanup();
if (NULL != m_szICMPData)
{
free(m_szICMPData);
m_szICMPData = NULL;
}
}
構造函數中,首先進行 Winsock 初始化,創建原始套接字,並分配內存用於存儲 ICMP 數據。如果分配內存失敗,則初始化標誌 m_bIsInitSucc
置爲 FALSE
。析構函數負責清理 Winsock 資源和釋放內存。
2. PingCore 函數
BOOL CPing::PingCore(DWORD dwDestIP, PingReply *pPingReply, DWORD dwTimeout)
{
// ...(省略其他代碼)
if (!m_bIsInitSucc)
{
return FALSE;
}
// ...(省略其他代碼)
if (sendto(m_sockRaw, m_szICMPData, nICMPDataSize, 0, (struct sockaddr*)&sockaddrDest, nSockaddrDestSize) == SOCKET_ERROR)
{
return FALSE;
}
// ...(省略其他代碼)
char recvbuf[256] = { "\0" };
while (TRUE)
{
// ...(省略其他代碼)
if (WSAWaitForMultipleEvents(1, &m_event, FALSE, 100, FALSE) != WSA_WAIT_TIMEOUT)
{
WSANETWORKEVENTS netEvent;
WSAEnumNetworkEvents(m_sockRaw, m_event, &netEvent);
if (netEvent.lNetworkEvents & FD_READ)
{
// ...(省略其他代碼)
if (nPacketSize != SOCKET_ERROR)
{
IPHeader *pIPHeader = (IPHeader*)recvbuf;
USHORT usIPHeaderLen = (USHORT)((pIPHeader->m_byVerHLen & 0x0f) * 4);
ICMPHeader *pICMPHeader = (ICMPHeader*)(recvbuf + usIPHeaderLen);
if (pICMPHeader->m_usID == m_usCurrentProcID && pICMPHeader->m_byType == ECHO_REPLY && pICMPHeader->m_usSeq == usSeq)
{
// ...(省略其他代碼)
return TRUE;
}
}
}
}
// ...(省略其他代碼)
if (GetTickCountCalibrate() - ulSendTimestamp >= dwTimeout)
{
return FALSE;
}
}
}
PingCore
函數是 Ping 工具的核心部分,負責構建 ICMP 報文、發送報文、接收響應報文,並進行超時處理。通過循環等待接收事件,實時檢測是否有 ICMP 響應報文到達。在接收到響應後,判斷響應是否符合預期條件,如果符合則填充 pPingReply
結構體,並返回 TRUE
。
3. CalCheckSum 函數
USHORT CPing::CalCheckSum(USHORT *pBuffer, int nSize)
{
unsigned long ulCheckSum = 0;
while (nSize > 1)
{
ulCheckSum += *pBuffer++;
nSize -= sizeof(USHORT);
}
if (nSize)
{
ulCheckSum += *(UCHAR*)pBuffer;
}
ulCheckSum = (ulCheckSum >> 16) + (ulCheckSum & 0xffff);
ulCheckSum += (ulCheckSum >> 16);
return (USHORT)(~ulCheckSum);
}
CalCheckSum
函數用於計算 ICMP 報文的校驗和。校驗和的計算採用了累加和的方法,最後對累加和進行溢出處理。計算完成後,返回取反後的校驗和。
4. GetTickCountCalibrate 函數
ULONG CPing::GetTickCountCalibrate()
{
// ...(省略其他代碼)
return s_ulFirstCallTick + (ULONG)(llCurrentTimeMS - s_ullFirstCallTickMS);
}
GetTickCountCalibrate
函數用於獲取經過調校的系統時間。通過計算系統時間相對於 Ping 工具啓動時的時間差,實現對系統時間的校準。這樣做是爲了處理系統時間溢出的情況。
5. Ping 函數
BOOL CPing::Ping(DWORD dwDestIP, PingReply *pPingReply, DWORD dwTimeout)
{
return PingCore(dwDestIP, pPingReply, dwTimeout);
}
BOOL CPing::Ping(char *szDestIP, PingReply *pPingReply, DWORD dwTimeout)
{
if (NULL != szDestIP)
{
return PingCore(inet_addr(szDestIP), pPingReply, dwTimeout);
}
return FALSE;
}
Ping
函數是對 PingCore
函數的封裝,根據目標 IP 地址調用 PingCore
進行 Ping
最後的MyPing.cpp
完整實現如下所示;
#include "MyPing.h"
USHORT CPing::s_usPacketSeq = 0;
// 構造函數
CPing::CPing() :m_szICMPData(NULL), m_bIsInitSucc(FALSE)
{
WSADATA WSAData;
if (WSAStartup(MAKEWORD(1, 1), &WSAData) != 0)
{
// 如果初始化不成功則返回
return;
}
m_event = WSACreateEvent();
m_usCurrentProcID = (USHORT)GetCurrentProcessId();
m_sockRaw = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP, NULL, 0, 0);
if (m_sockRaw == INVALID_SOCKET)
{
// 10013 以一種訪問權限不允許的方式做了一個訪問套接字的嘗試
return;
}
else
{
WSAEventSelect(m_sockRaw, m_event, FD_READ);
m_bIsInitSucc = TRUE;
m_szICMPData = (char*)malloc(DEF_PACKET_SIZE + sizeof(ICMPHeader));
if (m_szICMPData == NULL)
{
m_bIsInitSucc = FALSE;
}
}
}
// 析構函數
CPing::~CPing()
{
WSACleanup();
if (NULL != m_szICMPData)
{
free(m_szICMPData);
m_szICMPData = NULL;
}
}
// Ping 方法,傳入目標 IP 地址或域名、PingReply 結構體和超時時間
BOOL CPing::Ping(DWORD dwDestIP, PingReply *pPingReply, DWORD dwTimeout)
{
return PingCore(dwDestIP, pPingReply, dwTimeout);
}
// Ping 方法,傳入目標 IP 地址或域名、PingReply 結構體和超時時間
BOOL CPing::Ping(char *szDestIP, PingReply *pPingReply, DWORD dwTimeout)
{
if (NULL != szDestIP)
{
return PingCore(inet_addr(szDestIP), pPingReply, dwTimeout);
}
return FALSE;
}
// Ping 核心方法,傳入目標 IP 地址、PingReply 結構體和超時時間
BOOL CPing::PingCore(DWORD dwDestIP, PingReply *pPingReply, DWORD dwTimeout)
{
// 判斷初始化是否成功
if (!m_bIsInitSucc)
{
return FALSE;
}
// 配置 SOCKET
sockaddr_in sockaddrDest;
sockaddrDest.sin_family = AF_INET;
sockaddrDest.sin_addr.s_addr = dwDestIP;
int nSockaddrDestSize = sizeof(sockaddrDest);
// 構建 ICMP 包
int nICMPDataSize = DEF_PACKET_SIZE + sizeof(ICMPHeader);
ULONG ulSendTimestamp = GetTickCountCalibrate();
USHORT usSeq = ++s_usPacketSeq;
memset(m_szICMPData, 0, nICMPDataSize);
ICMPHeader *pICMPHeader = (ICMPHeader*)m_szICMPData;
pICMPHeader->m_byType = ECHO_REQUEST;
pICMPHeader->m_byCode = 0;
pICMPHeader->m_usID = m_usCurrentProcID;
pICMPHeader->m_usSeq = usSeq;
pICMPHeader->m_ulTimeStamp = ulSendTimestamp;
pICMPHeader->m_usChecksum = CalCheckSum((USHORT*)m_szICMPData, nICMPDataSize);
// 發送 ICMP 報文
if (sendto(m_sockRaw, m_szICMPData, nICMPDataSize, 0, (struct sockaddr*)&sockaddrDest, nSockaddrDestSize) == SOCKET_ERROR)
{
return FALSE;
}
// 判斷是否需要接收相應報文
if (pPingReply == NULL)
{
return TRUE;
}
char recvbuf[256] = { "\0" };
while (TRUE)
{
// 接收響應報文
if (WSAWaitForMultipleEvents(1, &m_event, FALSE, 100, FALSE) != WSA_WAIT_TIMEOUT)
{
WSANETWORKEVENTS netEvent;
WSAEnumNetworkEvents(m_sockRaw, m_event, &netEvent);
if (netEvent.lNetworkEvents & FD_READ)
{
ULONG nRecvTimestamp = GetTickCountCalibrate();
int nPacketSize = recvfrom(m_sockRaw, recvbuf, 256, 0, (struct sockaddr*)&sockaddrDest, &nSockaddrDestSize);
if (nPacketSize != SOCKET_ERROR)
{
IPHeader *pIPHeader = (IPHeader*)recvbuf;
USHORT usIPHeaderLen = (USHORT)((pIPHeader->m_byVerHLen & 0x0f) * 4);
ICMPHeader *pICMPHeader = (ICMPHeader*)(recvbuf + usIPHeaderLen);
if (pICMPHeader->m_usID == m_usCurrentProcID // 是當前進程發出的報文
&& pICMPHeader->m_byType == ECHO_REPLY // 是 ICMP 響應報文
&& pICMPHeader->m_usSeq == usSeq // 是本次請求報文的響應報文
)
{
pPingReply->m_usSeq = usSeq;
pPingReply->m_dwRoundTripTime = nRecvTimestamp - pICMPHeader->m_ulTimeStamp;
pPingReply->m_dwBytes = nPacketSize - usIPHeaderLen - sizeof(ICMPHeader);
pPingReply->m_dwTTL = pIPHeader->m_byTTL;
return TRUE;
}
}
}
}
// 超時
if (GetTickCountCalibrate() - ulSendTimestamp >= dwTimeout)
{
return FALSE;
}
}
}
// 計算檢驗和的方法
USHORT CPing::CalCheckSum(USHORT *pBuffer, int nSize)
{
unsigned long ulCheckSum = 0;
while (nSize > 1)
{
ulCheckSum += *pBuffer++;
nSize -= sizeof(USHORT);
}
if (nSize)
{
ulCheckSum += *(UCHAR*)pBuffer;
}
ulCheckSum = (ulCheckSum >> 16) + (ulCheckSum & 0xffff);
ulCheckSum += (ulCheckSum >> 16);
return (USHORT)(~ulCheckSum);
}
// 獲取時鐘計時器的校準值
ULONG CPing::GetTickCountCalibrate()
{
static ULONG s_ulFirstCallTick = 0;
static LONGLONG s_ullFirstCallTickMS = 0;
SYSTEMTIME systemtime;
FILETIME filetime;
GetLocalTime(&systemtime);
SystemTimeToFileTime(&systemtime, &filetime);
LARGE_INTEGER liCurrentTime;
liCurrentTime.HighPart = filetime.dwHighDateTime;
liCurrentTime.LowPart = filetime.dwLowDateTime;
LONGLONG llCurrentTimeMS = liCurrentTime.QuadPart / 10000;
if (s_ulFirstCallTick == 0)
{
s_ulFirstCallTick = GetTickCount();
}
if (s_ullFirstCallTickMS == 0)
{
s_ullFirstCallTickMS = llCurrentTimeMS;
}
return s_ulFirstCallTick + (ULONG)(llCurrentTimeMS - s_ullFirstCallTickMS);
}
如何使用
在主程序中直接引入頭文件MyPing.h
,並在main()
函數中直接調用CPing
類即可實現探測主機是否存活。
探測主機是否存活
#include "MyPing.h"
#include <iostream>
// 探測主機是否存活
bool TestPing(char *szIP)
{
CPing objPing;
PingReply reply;
objPing.Ping(szIP, &reply);
if (reply.m_dwTTL >= 10 && reply.m_dwTTL <= 255)
{
return true;
}
return false;
}
int main(int argc, char *argv[])
{
bool is_open = TestPing("202.89.233.100");
std::cout << "本機是否存活: " << is_open << std::endl;
system("pause");
return 0;
}
運行效果如下所示;
模擬系統Ping測試
#include "MyPing.h"
#include <iostream>
// 模擬系統Ping測試
void SystemPing(char *szIP, int szCount)
{
CPing objPing;
PingReply reply;
for (int x = 0; x < szCount; x++)
{
objPing.Ping(szIP, &reply);
std::cout << "探測主機: " << szIP << " 默認字節: " << DEF_PACKET_SIZE << " 發送長度: " << reply.m_dwBytes << " 時間: " << reply.m_dwRoundTripTime << " TTL: " << reply.m_dwTTL << std::endl;
Sleep(1000);
}
}
int main(int argc, char *argv[])
{
SystemPing("202.89.233.100", 5);
system("pause");
return 0;
}
運行效果如下所示;
參考資料
代碼的實現來源於博客園Snser博主,此處僅用於功能收錄以便於後期在項目中應用。