C++ 實現的Ping類的封裝

Ping 使用 Internet 控制消息協議(ICMP)來測試主機之間的連接。當用戶發送一個 ping 請求時,則對應的發送一個 ICMP Echo 請求消息到目標主機,並等待目標主機回覆一個 ICMP Echo 迴應消息。如果目標主機接收到請求並且網絡連接正常,則會返回一個迴應消息,表示主機之間的網絡連接是正常的。如果目標主機沒有收到請求消息或網絡連接不正常,則不會有迴應消息返回。

編譯報錯問題解決

Windows環境下編程不可避免的會用到windows.hwinsock.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 包的序列號
};

下面是對每個結構體成員的簡要說明:

  1. 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地址。
  2. ICMPHeader 結構體:
    • m_byType: ICMP類型。
    • m_byCode: ICMP代碼。
    • m_usChecksum: 檢驗和。
    • m_usID: 標識符。
    • m_usSeq: 序號。
    • m_ulTimeStamp: 時間戳(非標準ICMP頭部)。
  3. 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博主,此處僅用於功能收錄以便於後期在項目中應用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章