C++代码实现网络Ping功能
时间:2022-02-04 08:53:59|栏目:C代码|点击: 次
ping 127.0.0.1: 这个Ping命令被送到本地计算机的IP软件,该命令永不退出该计算机。
localhost是个操作系统的网络保留名,是127.0.0.1的别名。
ping www.baidu.com――对这个域名执行Ping命令,你的计算机必须先将域名转换成IP地址,通常是通过DNS服务器。
(一)main.cpp文件
// ConsoleApplication3.cpp : 定义控制台应用程序的入口点。 // //程序应用: ping命令是向目的主机发送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是个操作系统的网络保留名,是127.0.0.1的别名。//ping www.baidu.com――对这个域名执行Ping命令,你的计算机必须先将域名转换成IP地址,通常是通过DNS服务器。 PingReply reply; //PingReply类与对象 printf("Pinging %s with %d bytes of data:\n", szDestIP, DEF_PACKET_SIZE); //ping 远端IP地址,32字节的数据 while (TRUE) { objPing.Ping(szDestIP, &reply);//远端IP地址不为空(NULL),就返回true表示需要响应报文。远端IP空时不需要响应报文false。 printf("Reply from %s: bytes=%d time=%ldms TTL=%ld\n", szDestIP, reply.m_dwBytes, reply.m_dwRoundTripTime, reply.m_dwTTL); //字节数,时间,TTL生存时间 Sleep(500); } return 0; }
(二)ping.h文件
(1)IP头结构体:
IHL:首部长度。因为IP的头部不是定长的,所以需要这个信息进行IP包的解析,从而找到Data字段的起始点。
另外注意这个IHL是以4个字节为单位的,所以首部实际长度是IHL*4字节。
Time to Live:生存时间,这个就是TTL了。
Data:这部分是IP包的数据,也就是ICMP的报文内容。
//1.IP头结构体:20字节 struct IPHeader { BYTE m_byVerHLen; //4位版本Version+4位首部长度IHL 1B BYTE m_byTOS; //服务类型 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; //协议 1B=8b 为1时表示是ICMP报文 USHORT m_usHChecksum; //首部检验和 2B=16b ULONG m_ulSrcIP; //源IP地址 4B=32b ULONG m_ulDestIP; //目的IP地址 4B=32b };
(2)ICMP头结构体:
类型Type、代码Code、校验和、标识符、序列号、ICMP数据
//ICMP报文由首部8B和数据段组成。 //首部为定长的8个字节,前4个字节是通用部分(类型1B/代码1B/校验和2B),后4个字节随报文类型的不同有所差异。 //2.ICMP头结构体 (标准ICMP头为8字节) struct ICMPHeader { BYTE m_byType; //类型 1B type=8表示响应请求报文,type=0表示响应应答报文。 BYTE m_byCode; //代码 1B 与type组合,表示具体的信息 USHORT m_usChecksum; //检验和 2B 整个ICMP报文的检验和,包括Type、Code、...、Data。 USHORT m_usID; //标识符 2B=16bits 用于标识本进程 USHORT m_usSeq; //序列号 2B=16bits 用于判断回显应答数据报。 ULONG m_ulTimeStamp; //时间戳(非标准ICMP头部)4B //统计ping的往返时间的做法是,在ICMP报文的Data区域写入4个字节的时间戳。在收到应答报文时,取出这个时间戳与当前的时间对比即可。 };
(3)ICMP响应报文结构体:
//3.ICMP回答报文结构体 struct PingReply { USHORT m_usSeq; //ICMP包的序列号 2B DWORD m_dwRoundTripTime;//时间差 4B (word是2字节) DWORD m_dwBytes; //数据所占字节数 4B DWORD m_dwTTL; //TTL生存时间 4B };
(4)Ping类及相关变量的定义:
//类 class CPing { //公共变量 public: CPing(); //构造函数 ~CPing(); //析构函数 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; //需要监听的socket WSAEVENT m_event; //网络事件对象 USHORT m_usCurrentProcID; //当前进程发出的报文 char *m_szICMPData; //ICMP(Internet Control Message Protocol,网际控制报文协议) BOOL m_bIsInitSucc; //初始化成功 private: static USHORT s_usPacketSeq; //序列号++(16位=2字节) };
(三)ping.cpp文件
#include "ping.h" #include <iostream> USHORT CPing::s_usPacketSeq = 0;
(1)char *m_szICMPData; BOOL m_bIsInitSucc;
//::表示类作用域。为避免不同的类有名称相同的成员而采用作用域的方式进行区分。 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()返回发生的错误信息*/ return; } m_event = WSACreateEvent(); //创建一个网络事件对象(HANDLE m_event)。 //返回一个手工重置的事件对象句柄 (HANDLE hEventObject) m_usCurrentProcID = (USHORT)GetCurrentProcessId(); //当前进程ID /*ICMP必须使用原始套接字进行设计,要手动设置IP的头部和ICMP的头部并行校验*/ m_sockRaw = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP, NULL, 0, 0); //创建一个监听的socket (SOCKET m_sockRaw) //当IP报头中的协议字段值为1时,就说明这是一个ICMP报文。 if (m_sockRaw == INVALID_SOCKET) //无效套接字 { std::cerr << "WSASocket() failed:" << WSAGetLastError() << std::endl; //10013 以一种访问权限不允许的方式做了一个访问套接字的尝试。 } else //是ICMP报文,令初始化成功,为ICMP数据分配内存 { WSAEventSelect(m_sockRaw, m_event, FD_READ); //调用WSAEventSelect将监听的socket(m_sockRaw)与该事件(m_event)进行关联。WSAEventSelect(套接字,网络事件对象,需要关注的事件) m_bIsInitSucc = TRUE; m_szICMPData = (char*)malloc(DEF_PACKET_SIZE + sizeof(ICMPHeader)); //为ICMPData分配内存 //ping命令的工作原理是:向网络上的另一个主机系统发送ICMP报文,如果指定系统得到了报文,它将把报文一模一样地传回给发送者 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配置,告诉它我就要用旧函数。inet_pton() or InetPton() } return FALSE; //远端IP为空,false }
(3)BOOL PingCore(DWORD dwDestIP, PingReply *pPingReply, DWORD dwTimeout);
1.//判断初始化是否成功
2.//配置套接字SOCKET
3.//构建ICMP包
4.//填补ICMP首部
5.//发送ICMP请求报文(ping请求)
6.//判断是否需要接收响应报文
7.//等待网络事件接收响应报文
BOOL CPing::PingCore(DWORD dwDestIP, PingReply *pPingReply, DWORD dwTimeout) { //判断初始化是否成功 if (!m_bIsInitSucc) { return FALSE; //初始化没成功 } //配置套接字SOCKET sockaddr_in sockaddrDest; //sockaddr_in是internet环境下的套接字地址。定义在ws2def.h中的结构体 sockaddrDest.sin_family = AF_INET; //地址族(Address Family):网络类型 sockaddrDest.sin_addr.s_addr = dwDestIP; //32位IP地址(4字节) int nSockaddrDestSize = sizeof(sockaddrDest);//大小 //构建ICMP包 int nICMPDataSize = DEF_PACKET_SIZE + sizeof(ICMPHeader); //ICMP包长度:32+ICMP头 ULONG ulSendTimestamp = GetTickCountCalibrate(); //发送时间戳(毫秒级) USHORT usSeq = ++s_usPacketSeq; //2字节,++0 memset(m_szICMPData, 0, nICMPDataSize); //数据,长度。memset()初始化内存: //memset(* Dst, int Val, size_t Size):将指针变量Dst所指向的前Size字节的内存单元用一个“整数”Val替换。 //填补ICMP首部 ICMPHeader *pICMPHeader = (ICMPHeader*)m_szICMPData; pICMPHeader->m_byType = ECHO_REQUEST; //类型。Type:8,Code:0:表示回显请求报文(ping请求)。 Type:0,Code:0:表示回显回答报文(ping应答) pICMPHeader->m_byCode = 0; //代码 pICMPHeader->m_usID = m_usCurrentProcID;//标识符 pICMPHeader->m_usSeq = usSeq; //本报的序列号 pICMPHeader->m_ulTimeStamp = ulSendTimestamp;//发送时间戳(非标准ICMP头部) pICMPHeader->m_usChecksum = CalCheckSum((USHORT*)m_szICMPData, nICMPDataSize);//计算检验和 //TCP/IP协议栈使用的校验算法:对16位的数据进行累加计算,并返回计算结果 //―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― //发送ICMP请求报文(ping请求) 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) //等待事件(m_event)。 //WSAWaitForMultipleEvents(事件对象数组里边的个数为1,事件对象数组,等待类型为FALSE表示事件数组里至少有一个信号就返回,等待的超时时间为100,当系统的执行队列有I/O例程要执行时不返回) { WSANETWORKEVENTS netEvent; WSAEnumNetworkEvents(m_sockRaw, m_event, &netEvent); //m_event = WSACreateEvent(); //事件发生时,调用WSAEnumNetworkEvents,检测指定的socket上的网络事件,并将关联信息保存在netEvent中。 //WSAEnumNetworkEvents(SOCKET s,WSAEVENT hEventObject,LPWSANETWORKEVENTS lpNetworkEvents) //当调用WSAEnumNetworkEvents函数成功后,它会将我们指定的socket和事件对象所关联的网络事件的信息保存到LPWSANETWORKEVENTS这个结构体里去,根据这个结构体我们就可以判断是否是我们所关注的网络事件已经发生了。 //如果是FD_READ,读的网络事件发生了,那就调用recv函数进行操作。 //若是FD_CLOSE,关闭的网络事件发生了,就调用closesocket将socket关掉,在数组里将其置零等操作。 if (netEvent.lNetworkEvents & FD_READ) //有网络事件,且可读,那就recv收数啊 { ULONG nRecvTimestamp = GetTickCountCalibrate(); //计算开始发送的时间 int nPacketSize = recvfrom(m_sockRaw, recvbuf, 256, 0, (struct sockaddr*)&sockaddrDest, &nSockaddrDestSize); //从远端IP处收数。 if (nPacketSize != SOCKET_ERROR) { //ICMP协议是IP层的一个协议,但是由于差错报告在发送给报文源发方时可能也要经过若干子网,因此牵涉到路由选择等问题,所以ICMP报文需通过IP协议来发送。 //ICMP数据报的数据发送前需要两级封装:首先添加ICMP报头形成ICMP报文,再添加IP报头形成IP数据报。 //拆解封装:IP数据报去掉IP报头,剩下ICMP报文,再去掉ICMP报头,就是ICMP数据报了。 IPHeader *pIPHeader = (IPHeader*)recvbuf; USHORT usIPHeaderLen = (USHORT)((pIPHeader->m_byVerHLen & 0x0f) * 4); //(4位版本+4位首部长度)&0xf,然后乘4 //IP头部20字节 ICMPHeader *pICMPHeader = (ICMPHeader*)(recvbuf + usIPHeaderLen); if (pICMPHeader->m_usID == m_usCurrentProcID //是当前进程发出的报文 && pICMPHeader->m_byType == ECHO_REPLY //是响应报文类型(Type=0) && pICMPHeader->m_usSeq == usSeq //是本次请求报文的响应报文 ) { pPingReply->m_usSeq = usSeq; //ICMP包的序列号 pPingReply->m_dwRoundTripTime = nRecvTimestamp - pICMPHeader->m_ulTimeStamp; //当应答返回时,用当前时间减去存放在ICMP报文中的时间值,即是往返时间。 pPingReply->m_dwBytes = nPacketSize - usIPHeaderLen - sizeof(ICMPHeader);//数据大小:整包数据-IP头-ICMP头 pPingReply->m_dwTTL = pIPHeader->m_byTTL; //TTL生存时间 return TRUE; } } } } //超时 if (GetTickCountCalibrate() - ulSendTimestamp >= dwTimeout)//时间戳(非标准ICMP头部) { return FALSE; } } }
WSA连网事件流程图
(4)计算检验和
//计算检验和 // TCP/IP协议栈使用的校验算法是比较经典的,对16位的数据进行累加计算,并返回计算结果 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++; //数值相加 //(2)把需校验的数据看成以16位为单位的数字组成,依次进行求和,并存到32位的整型中 nSize -= sizeof(USHORT); //长度相减 } if (nSize) { ulCheckSum += *(UCHAR*)pBuffer; } ulCheckSum = (ulCheckSum >> 16) + (ulCheckSum & 0xffff); //高位相加 //(3)把求和结果中的高16位(进位)加到低16位上,如果还有进位,重复 ulCheckSum += (ulCheckSum >> 16); //将溢出位加入 return (USHORT)(~ulCheckSum); //返回值:取反 //(4)将这个32位的整型按位取反,并强制转换为16位整型(截断)后返回 } /* 附录:如何计算检验和 ICMP中检验和的计算算法为: 1、将检验和字段置为0 2、把需校验的数据看成以16位为单位的数字组成,依次进行二进制反码求和 3、把得到的结果存入检验和字段中 所谓二进制反码求和,就是: 1、将源数据转成反码 2、0 + 0 = 0 0 + 1 = 1 1 + 1 = 0进1 3、若最高位相加后产生进位,则最后得到的结果要加1 在实际实现的过程中,比较常见的代码写法是: 1、将检验和字段置为0 2、把需校验的数据看成以16位为单位的数字组成,依次进行求和,并存到32位的整型中 3、把求和结果中的高16位(进位)加到低16位上,如果还有进位,重复第3步[实际上,这一步最多会执行2次] 4、将这个32位的整型按位取反,并强制转换为16位整型(截断)后返回 */
(5)计算毫秒级别的时间差
//计算毫秒级别的时间差。返回值是unsigned long级别的 ULONG CPing::GetTickCountCalibrate() { static ULONG s_ulFirstCallTick = 0; static LONGLONG s_ullFirstCallTickMS = 0; SYSTEMTIME systemtime; //系统时间SYSTEMTIME与tm类似,不过多了一项wMilliseconds。 FILETIME filetime; //文件时间FILETIME与time_t类似,是64位整型,不过FILETIME是以100纳秒(ns)为单位。 GetLocalTime(&systemtime); //GetLocalTime获得当前的本地时间,GetSystemTime函数获得当前的UTC时间,两个时间存在着时差。 SystemTimeToFileTime(&systemtime, &filetime);//UTC的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(); //返回(retrieve)从操作系统启动所经过(elapsed)的毫秒数。用GetTickCount()计算毫秒级的时间差是不靠谱的! } if (s_ullFirstCallTickMS == 0) { s_ullFirstCallTickMS = llCurrentTimeMS; } return s_ulFirstCallTick + (ULONG)(llCurrentTimeMS - s_ullFirstCallTickMS);// 当前时间ms - 第一次回应时间ms }