C++代碼實現(xiàn)網(wǎng)絡(luò)Ping功能
ping 127.0.0.1: 這個Ping命令被送到本地計算機的IP軟件,該命令永不退出該計算機。
localhost是個操作系統(tǒng)的網(wǎng)絡(luò)保留名,是127.0.0.1的別名。
ping www.baidu.com——對這個域名執(zhí)行Ping命令,你的計算機必須先將域名轉(zhuǎn)換成IP地址,通常是通過DNS服務(wù)器。

(一)main.cpp文件
// ConsoleApplication3.cpp : 定義控制臺應(yīng)用程序的入口點。
//
//程序應(yīng)用: ping命令是向目的主機發(fā)送ICMP報文,檢驗本地主機和遠程的目的主機是否連接
#include <winsock2.h>
#include <stdio.h>
#include "ping.h"
int main(void)
{
CPing objPing; //CPing類與對象
char *szDestIP = "127.0.0.1"; //字符IP地址 //127.0.0.1 這個Ping命令被送到本地計算機的IP軟件,該命令永不退出該計算機。localhost是個操作系統(tǒng)的網(wǎng)絡(luò)保留名,是127.0.0.1的別名。//ping www.baidu.com——對這個域名執(zhí)行Ping命令,你的計算機必須先將域名轉(zhuǎn)換成IP地址,通常是通過DNS服務(wù)器。
PingReply reply; //PingReply類與對象
printf("Pinging %s with %d bytes of data:\n", szDestIP, DEF_PACKET_SIZE); //ping 遠端IP地址,32字節(jié)的數(shù)據(jù)
while (TRUE)
{
objPing.Ping(szDestIP, &reply);//遠端IP地址不為空(NULL),就返回true表示需要響應(yīng)報文。遠端IP空時不需要響應(yīng)報文false。
printf("Reply from %s: bytes=%d time=%ldms TTL=%ld\n", szDestIP, reply.m_dwBytes, reply.m_dwRoundTripTime, reply.m_dwTTL); //字節(jié)數(shù),時間,TTL生存時間
Sleep(500);
}
return 0;
}
(二)ping.h文件
(1)IP頭結(jié)構(gòu)體:
IHL:首部長度。因為IP的頭部不是定長的,所以需要這個信息進行IP包的解析,從而找到Data字段的起始點。
另外注意這個IHL是以4個字節(jié)為單位的,所以首部實際長度是IHL*4字節(jié)。
Time to Live:生存時間,這個就是TTL了。
Data:這部分是IP包的數(shù)據(jù),也就是ICMP的報文內(nèi)容。


//1.IP頭結(jié)構(gòu)體:20字節(jié)
struct IPHeader
{
BYTE m_byVerHLen; //4位版本Version+4位首部長度IHL 1B
BYTE m_byTOS; //服務(wù)類型 1B=16b
USHORT m_usTotalLen; //總長度 2B=16b
USHORT m_usID; //標識 2B=16b
USHORT m_usFlagFragOffset; //3位標志+13位片偏移=16位 2B=16b
BYTE m_byTTL; //TTL 生存時間 1B=8b
BYTE m_byProtocol; //協(xié)議 1B=8b 為1時表示是ICMP報文
USHORT m_usHChecksum; //首部檢驗和 2B=16b
ULONG m_ulSrcIP; //源IP地址 4B=32b
ULONG m_ulDestIP; //目的IP地址 4B=32b
};
(2)ICMP頭結(jié)構(gòu)體:
類型Type、代碼Code、校驗和、標識符、序列號、ICMP數(shù)據(jù)

//ICMP報文由首部8B和數(shù)據(jù)段組成。
//首部為定長的8個字節(jié),前4個字節(jié)是通用部分(類型1B/代碼1B/校驗和2B),后4個字節(jié)隨報文類型的不同有所差異。
//2.ICMP頭結(jié)構(gòu)體 (標準ICMP頭為8字節(jié))
struct ICMPHeader
{
BYTE m_byType; //類型 1B type=8表示響應(yīng)請求報文,type=0表示響應(yīng)應(yīng)答報文。
BYTE m_byCode; //代碼 1B 與type組合,表示具體的信息
USHORT m_usChecksum; //檢驗和 2B 整個ICMP報文的檢驗和,包括Type、Code、...、Data。
USHORT m_usID; //標識符 2B=16bits 用于標識本進程
USHORT m_usSeq; //序列號 2B=16bits 用于判斷回顯應(yīng)答數(shù)據(jù)報。
ULONG m_ulTimeStamp; //時間戳(非標準ICMP頭部)4B //統(tǒng)計ping的往返時間的做法是,在ICMP報文的Data區(qū)域?qū)懭?個字節(jié)的時間戳。在收到應(yīng)答報文時,取出這個時間戳與當前的時間對比即可。
};
(3)ICMP響應(yīng)報文結(jié)構(gòu)體:
//3.ICMP回答報文結(jié)構(gòu)體
struct PingReply
{
USHORT m_usSeq; //ICMP包的序列號 2B
DWORD m_dwRoundTripTime;//時間差 4B (word是2字節(jié))
DWORD m_dwBytes; //數(shù)據(jù)所占字節(jié)數(shù) 4B
DWORD m_dwTTL; //TTL生存時間 4B
};
(4)Ping類及相關(guān)變量的定義:
//類
class CPing
{
//公共變量
public:
CPing(); //構(gòu)造函數(shù)
~CPing(); //析構(gòu)函數(shù)
BOOL Ping(DWORD dwDestIP, PingReply *pPingReply = NULL, DWORD dwTimeout = 2000);
BOOL Ping(char *szDestIP, PingReply *pPingReply = NULL, DWORD dwTimeout = 2000);
//私有變量
private:
BOOL PingCore(DWORD dwDestIP, PingReply *pPingReply, DWORD dwTimeout);
USHORT CalCheckSum(USHORT *pBuffer, int nSize);//計算檢驗和
ULONG GetTickCountCalibrate(); //計算毫秒級別的時間差
private:
SOCKET m_sockRaw; //需要監(jiān)聽的socket
WSAEVENT m_event; //網(wǎng)絡(luò)事件對象
USHORT m_usCurrentProcID; //當前進程發(fā)出的報文
char *m_szICMPData; //ICMP(Internet Control Message Protocol,網(wǎng)際控制報文協(xié)議)
BOOL m_bIsInitSucc; //初始化成功
private:
static USHORT s_usPacketSeq; //序列號++(16位=2字節(jié))
};
(三)ping.cpp文件
#include "ping.h" #include <iostream> USHORT CPing::s_usPacketSeq = 0;
(1)char *m_szICMPData; BOOL m_bIsInitSucc;
//::表示類作用域。為避免不同的類有名稱相同的成員而采用作用域的方式進行區(qū)分。
CPing::CPing() :m_szICMPData(NULL), m_bIsInitSucc(FALSE)
{
WSADATA WSAData;
//WSAStartup(MAKEWORD(2, 2), &WSAData);
if (WSAStartup(MAKEWORD(1, 1), &WSAData) != 0)
{
printf("WSAStartup() failed: %d\n", GetLastError()); /*如果初始化不成功則報錯,GetLastError()返回發(fā)生的錯誤信息*/
return;
}
m_event = WSACreateEvent(); //創(chuàng)建一個網(wǎng)絡(luò)事件對象(HANDLE m_event)。 //返回一個手工重置的事件對象句柄 (HANDLE hEventObject)
m_usCurrentProcID = (USHORT)GetCurrentProcessId(); //當前進程ID
/*ICMP必須使用原始套接字進行設(shè)計,要手動設(shè)置IP的頭部和ICMP的頭部并行校驗*/
m_sockRaw = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP, NULL, 0, 0); //創(chuàng)建一個監(jiān)聽的socket (SOCKET m_sockRaw) //當IP報頭中的協(xié)議字段值為1時,就說明這是一個ICMP報文。
if (m_sockRaw == INVALID_SOCKET) //無效套接字
{
std::cerr << "WSASocket() failed:" << WSAGetLastError() << std::endl; //10013 以一種訪問權(quán)限不允許的方式做了一個訪問套接字的嘗試。
}
else //是ICMP報文,令初始化成功,為ICMP數(shù)據(jù)分配內(nèi)存
{
WSAEventSelect(m_sockRaw, m_event, FD_READ); //調(diào)用WSAEventSelect將監(jiān)聽的socket(m_sockRaw)與該事件(m_event)進行關(guān)聯(lián)。WSAEventSelect(套接字,網(wǎng)絡(luò)事件對象,需要關(guān)注的事件)
m_bIsInitSucc = TRUE;
m_szICMPData = (char*)malloc(DEF_PACKET_SIZE + sizeof(ICMPHeader)); //為ICMPData分配內(nèi)存
//ping命令的工作原理是:向網(wǎng)絡(luò)上的另一個主機系統(tǒng)發(fā)送ICMP報文,如果指定系統(tǒng)得到了報文,它將把報文一模一樣地傳回給發(fā)送者
if (m_szICMPData == NULL)
{
m_bIsInitSucc = FALSE;
}
}
}
CPing::~CPing()
{
WSACleanup();
if (NULL != m_szICMPData)
{
free(m_szICMPData);
m_szICMPData = NULL;
}
}
(2)BOOL Ping(char *szDestIP, PingReply *pPingReply = NULL, DWORD dwTimeout = 2000);
BOOL CPing::Ping(char *szDestIP, PingReply *pPingReply, DWORD dwTimeout)
{
if (NULL != szDestIP) //遠端IP非空
{
return PingCore(inet_addr(szDestIP), pPingReply, dwTimeout); //項目 -> 屬性 -> C/C++ > SDL檢查:否。——修改VS配置,告訴它我就要用舊函數(shù)。inet_pton() or InetPton()
}
return FALSE; //遠端IP為空,false
}
(3)BOOL PingCore(DWORD dwDestIP, PingReply *pPingReply, DWORD dwTimeout);
1.//判斷初始化是否成功
2.//配置套接字SOCKET
3.//構(gòu)建ICMP包
4.//填補ICMP首部
5.//發(fā)送ICMP請求報文(ping請求)
6.//判斷是否需要接收響應(yīng)報文
7.//等待網(wǎng)絡(luò)事件接收響應(yīng)報文
BOOL CPing::PingCore(DWORD dwDestIP, PingReply *pPingReply, DWORD dwTimeout)
{
//判斷初始化是否成功
if (!m_bIsInitSucc)
{
return FALSE; //初始化沒成功
}
//配置套接字SOCKET
sockaddr_in sockaddrDest; //sockaddr_in是internet環(huán)境下的套接字地址。定義在ws2def.h中的結(jié)構(gòu)體
sockaddrDest.sin_family = AF_INET; //地址族(Address Family):網(wǎng)絡(luò)類型
sockaddrDest.sin_addr.s_addr = dwDestIP; //32位IP地址(4字節(jié))
int nSockaddrDestSize = sizeof(sockaddrDest);//大小
//構(gòu)建ICMP包
int nICMPDataSize = DEF_PACKET_SIZE + sizeof(ICMPHeader); //ICMP包長度:32+ICMP頭
ULONG ulSendTimestamp = GetTickCountCalibrate(); //發(fā)送時間戳(毫秒級)
USHORT usSeq = ++s_usPacketSeq; //2字節(jié),++0
memset(m_szICMPData, 0, nICMPDataSize); //數(shù)據(jù),長度。memset()初始化內(nèi)存:
//memset(* Dst, int Val, size_t Size):將指針變量Dst所指向的前Size字節(jié)的內(nèi)存單元用一個“整數(shù)”Val替換。
//填補ICMP首部
ICMPHeader *pICMPHeader = (ICMPHeader*)m_szICMPData;
pICMPHeader->m_byType = ECHO_REQUEST; //類型。Type:8,Code:0:表示回顯請求報文(ping請求)。 Type:0,Code:0:表示回顯回答報文(ping應(yīng)答)
pICMPHeader->m_byCode = 0; //代碼
pICMPHeader->m_usID = m_usCurrentProcID;//標識符
pICMPHeader->m_usSeq = usSeq; //本報的序列號
pICMPHeader->m_ulTimeStamp = ulSendTimestamp;//發(fā)送時間戳(非標準ICMP頭部)
pICMPHeader->m_usChecksum = CalCheckSum((USHORT*)m_szICMPData, nICMPDataSize);//計算檢驗和 //TCP/IP協(xié)議棧使用的校驗算法:對16位的數(shù)據(jù)進行累加計算,并返回計算結(jié)果
//————————————————————————————————————————————————————————————————————————————————————————————
//發(fā)送ICMP請求報文(ping請求)
if (sendto(m_sockRaw, m_szICMPData, nICMPDataSize, 0, (struct sockaddr*)&sockaddrDest, nSockaddrDestSize) == SOCKET_ERROR)
{
return FALSE; //套接字錯誤
}
//判斷是否需要接收響應(yīng)報文
if (pPingReply == NULL)
{
return TRUE;
}
char recvbuf[256] = { "\0" }; //初始化
while (TRUE) //接收響應(yīng)報文
{
//等待網(wǎng)絡(luò)事件
if (WSAWaitForMultipleEvents(1, &m_event, FALSE, 100, FALSE) != WSA_WAIT_TIMEOUT) //等待事件(m_event)。
//WSAWaitForMultipleEvents(事件對象數(shù)組里邊的個數(shù)為1,事件對象數(shù)組,等待類型為FALSE表示事件數(shù)組里至少有一個信號就返回,等待的超時時間為100,當系統(tǒng)的執(zhí)行隊列有I/O例程要執(zhí)行時不返回)
{
WSANETWORKEVENTS netEvent;
WSAEnumNetworkEvents(m_sockRaw, m_event, &netEvent); //m_event = WSACreateEvent();
//事件發(fā)生時,調(diào)用WSAEnumNetworkEvents,檢測指定的socket上的網(wǎng)絡(luò)事件,并將關(guān)聯(lián)信息保存在netEvent中。
//WSAEnumNetworkEvents(SOCKET s,WSAEVENT hEventObject,LPWSANETWORKEVENTS lpNetworkEvents)
//當調(diào)用WSAEnumNetworkEvents函數(shù)成功后,它會將我們指定的socket和事件對象所關(guān)聯(lián)的網(wǎng)絡(luò)事件的信息保存到LPWSANETWORKEVENTS這個結(jié)構(gòu)體里去,根據(jù)這個結(jié)構(gòu)體我們就可以判斷是否是我們所關(guān)注的網(wǎng)絡(luò)事件已經(jīng)發(fā)生了。
//如果是FD_READ,讀的網(wǎng)絡(luò)事件發(fā)生了,那就調(diào)用recv函數(shù)進行操作。
//若是FD_CLOSE,關(guān)閉的網(wǎng)絡(luò)事件發(fā)生了,就調(diào)用closesocket將socket關(guān)掉,在數(shù)組里將其置零等操作。
if (netEvent.lNetworkEvents & FD_READ) //有網(wǎng)絡(luò)事件,且可讀,那就recv收數(shù)啊
{
ULONG nRecvTimestamp = GetTickCountCalibrate(); //計算開始發(fā)送的時間
int nPacketSize = recvfrom(m_sockRaw, recvbuf, 256, 0, (struct sockaddr*)&sockaddrDest, &nSockaddrDestSize); //從遠端IP處收數(shù)。
if (nPacketSize != SOCKET_ERROR)
{
//ICMP協(xié)議是IP層的一個協(xié)議,但是由于差錯報告在發(fā)送給報文源發(fā)方時可能也要經(jīng)過若干子網(wǎng),因此牽涉到路由選擇等問題,所以ICMP報文需通過IP協(xié)議來發(fā)送。
//ICMP數(shù)據(jù)報的數(shù)據(jù)發(fā)送前需要兩級封裝:首先添加ICMP報頭形成ICMP報文,再添加IP報頭形成IP數(shù)據(jù)報。
//拆解封裝:IP數(shù)據(jù)報去掉IP報頭,剩下ICMP報文,再去掉ICMP報頭,就是ICMP數(shù)據(jù)報了。
IPHeader *pIPHeader = (IPHeader*)recvbuf;
USHORT usIPHeaderLen = (USHORT)((pIPHeader->m_byVerHLen & 0x0f) * 4); //(4位版本+4位首部長度)&0xf,然后乘4 //IP頭部20字節(jié)
ICMPHeader *pICMPHeader = (ICMPHeader*)(recvbuf + usIPHeaderLen);
if (pICMPHeader->m_usID == m_usCurrentProcID //是當前進程發(fā)出的報文
&& pICMPHeader->m_byType == ECHO_REPLY //是響應(yīng)報文類型(Type=0)
&& pICMPHeader->m_usSeq == usSeq //是本次請求報文的響應(yīng)報文
)
{
pPingReply->m_usSeq = usSeq; //ICMP包的序列號
pPingReply->m_dwRoundTripTime = nRecvTimestamp - pICMPHeader->m_ulTimeStamp; //當應(yīng)答返回時,用當前時間減去存放在ICMP報文中的時間值,即是往返時間。
pPingReply->m_dwBytes = nPacketSize - usIPHeaderLen - sizeof(ICMPHeader);//數(shù)據(jù)大小:整包數(shù)據(jù)-IP頭-ICMP頭
pPingReply->m_dwTTL = pIPHeader->m_byTTL; //TTL生存時間
return TRUE;
}
}
}
}
//超時
if (GetTickCountCalibrate() - ulSendTimestamp >= dwTimeout)//時間戳(非標準ICMP頭部)
{
return FALSE;
}
}
}
WSA連網(wǎng)事件流程圖

(4)計算檢驗和
//計算檢驗和 // TCP/IP協(xié)議棧使用的校驗算法是比較經(jīng)典的,對16位的數(shù)據(jù)進行累加計算,并返回計算結(jié)果
USHORT CPing::CalCheckSum(USHORT *pBuffer, int nSize) //CalCheckSum((USHORT*)m_szICMPData, nICMPDataSize);
{
unsigned long ulCheckSum = 0; //4B=32b //(1)將檢驗和字段置為0
while (nSize > 1)
{
ulCheckSum += *pBuffer++; //數(shù)值相加 //(2)把需校驗的數(shù)據(jù)看成以16位為單位的數(shù)字組成,依次進行求和,并存到32位的整型中
nSize -= sizeof(USHORT); //長度相減
}
if (nSize)
{
ulCheckSum += *(UCHAR*)pBuffer;
}
ulCheckSum = (ulCheckSum >> 16) + (ulCheckSum & 0xffff); //高位相加 //(3)把求和結(jié)果中的高16位(進位)加到低16位上,如果還有進位,重復(fù)
ulCheckSum += (ulCheckSum >> 16); //將溢出位加入
return (USHORT)(~ulCheckSum); //返回值:取反 //(4)將這個32位的整型按位取反,并強制轉(zhuǎn)換為16位整型(截斷)后返回
}
/*
附錄:如何計算檢驗和
ICMP中檢驗和的計算算法為:
1、將檢驗和字段置為0
2、把需校驗的數(shù)據(jù)看成以16位為單位的數(shù)字組成,依次進行二進制反碼求和
3、把得到的結(jié)果存入檢驗和字段中
所謂二進制反碼求和,就是:
1、將源數(shù)據(jù)轉(zhuǎn)成反碼
2、0 + 0 = 0 0 + 1 = 1 1 + 1 = 0進1
3、若最高位相加后產(chǎn)生進位,則最后得到的結(jié)果要加1
在實際實現(xiàn)的過程中,比較常見的代碼寫法是:
1、將檢驗和字段置為0
2、把需校驗的數(shù)據(jù)看成以16位為單位的數(shù)字組成,依次進行求和,并存到32位的整型中
3、把求和結(jié)果中的高16位(進位)加到低16位上,如果還有進位,重復(fù)第3步[實際上,這一步最多會執(zhí)行2次]
4、將這個32位的整型按位取反,并強制轉(zhuǎn)換為16位整型(截斷)后返回
*/
(5)計算毫秒級別的時間差
//計算毫秒級別的時間差。返回值是unsigned long級別的
ULONG CPing::GetTickCountCalibrate()
{
static ULONG s_ulFirstCallTick = 0;
static LONGLONG s_ullFirstCallTickMS = 0;
SYSTEMTIME systemtime; //系統(tǒng)時間SYSTEMTIME與tm類似,不過多了一項wMilliseconds。
FILETIME filetime; //文件時間FILETIME與time_t類似,是64位整型,不過FILETIME是以100納秒(ns)為單位。
GetLocalTime(&systemtime); //GetLocalTime獲得當前的本地時間,GetSystemTime函數(shù)獲得當前的UTC時間,兩個時間存在著時差。
SystemTimeToFileTime(&systemtime, &filetime);//UTC的SYSTEMTIME時間 轉(zhuǎn)換為對應(yīng)的 本地的FILETIME時間
LARGE_INTEGER liCurrentTime;
liCurrentTime.HighPart = filetime.dwHighDateTime;
liCurrentTime.LowPart = filetime.dwLowDateTime;
LONGLONG llCurrentTimeMS = liCurrentTime.QuadPart / 10000;
if (s_ulFirstCallTick == 0)
{
s_ulFirstCallTick = GetTickCount(); //返回(retrieve)從操作系統(tǒng)啟動所經(jīng)過(elapsed)的毫秒數(shù)。用GetTickCount()計算毫秒級的時間差是不靠譜的!
}
if (s_ullFirstCallTickMS == 0)
{
s_ullFirstCallTickMS = llCurrentTimeMS;
}
return s_ulFirstCallTick + (ULONG)(llCurrentTimeMS - s_ullFirstCallTickMS);// 當前時間ms - 第一次回應(yīng)時間ms
}
到此這篇關(guān)于C++代碼實現(xiàn)網(wǎng)絡(luò)Ping功能的文章就介紹到這了,更多相關(guān)C++網(wǎng)絡(luò)Ping內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Qt使用QSoundEffect類實現(xiàn)播放音效或音樂
這篇文章主要為大家詳細介紹了Qt如何使用QSoundEffect類實現(xiàn)播放音效或音樂功能,文中的示例代碼講解詳細,有需要的小伙伴可以參考一下2024-12-12
VSCode 配置C++開發(fā)環(huán)境的方法步驟
這篇文章主要介紹了VSCode 配置C++開發(fā)環(huán)境的方法步驟,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-03-03
Ubuntu系統(tǒng)下如何在VScode配置OpenCV(C++)環(huán)境(.json文件)
這篇文章主要介紹了如何在VSCode中配置和運行C++程序,包括創(chuàng)建test.cpp文件、配置launch.json、tasks.json和c_cpp_properties.json文件,以及重啟VSCode以解決可能的報錯問題,需要的朋友可以參考下2025-02-02

