转自:C++实现ping功能,致敬原创!

一、目标是要实现下面的功能:  

  cmd命令ping功能之C++实现_首部

二、基础知识

    ping的过程是向目的IP发送一个type=8的ICMP响应请求报文,目标主机收到这个报文之后,会向源IP(发送方,我)回复一个type=0的ICMP响应应答报文。那上面的字节、往访时间、TTL之类的信息又是从哪来的呢?这取决于IP和ICMP的头部。

1. IP头部:  

  cmd命令ping功能之C++实现_初始化_02

  头部内容有点多,我们关心的只有以下几个:

  IHL:首部长度。因为IP的头部不是定长的,所以需要这个信息进行IP包的解析,从而找到Data字段的起始点。另外注意这个IHL是以4个字节为单位的,所以首部实际长度是IHL*4字节。

  Time to Live:生存时间,这个就是TTL了。

  Data:这部分是IP包的数据,也就是ICMP的报文内容。

2. ICMP响应请求/应答报文头部:

cmd命令ping功能之C++实现_初始化_03

  Type:类型,type=8表示响应请求报文,type=0表示响应应答报文。

  Code:代码,与type组合,表示具体的信息,参考这里

  Checksum:检验和,这个是整个ICMP报文的检验和,包括Type、Code、...、Data。

  Identifier:标识符,这个一般填入本进程的标识符。

  Sequence Number:序号

  Data:数据部分

  上面是标准的ICMP报文,一般而言,统计ping的往返时间的做法是,在ICMP报文的Data区域写入4个字节的时间戳。在收到应答报文时,取出这个时间戳与当前的时间对比即可。

三、Ping程序实现步骤
  1. 创建类型为SOCK_RAW的一个套接字,同时设定协议IPPROTO_ICMP。
  2. 创建并初始化ICMP头。
  3. 调用sendto或WSASendto,将ICMP请求发给远程主机。
  4. 调用recvfrom或WSARecvfrom,以接收任何ICMP响应。

//--------------------------ping.h--------------------------方法声明部分

cmd命令ping功能之C++实现_数据_04cmd命令ping功能之C++实现_字段_05
 1 #pragma once
 2 
 3 //在默认windows.h会包含winsock.h,当你包含winsock2.h就会冲突,因此在包含windows.h前需要定义一个宏,#define WIN32_LEAN_AND_MEAN ;去除winsock.h
 4 //要么将#include <winsock2.h>放在#include<windows.h>前面或者直接去掉#include<windows.h>
 5 
 6 #include <winsock2.h>
 7 #pragma comment(lib, "WS2_32")    // 链接到WS2_32.lib
 8 
 9 #define DEF_PACKET_SIZE 32
10 #define ECHO_REQUEST 8
11 #define ECHO_REPLY 0
12 
13 struct IPHeader
14 {
15     BYTE m_byVerHLen; //4位版本+4位首部长度
16     BYTE m_byTOS; //服务类型
17     USHORT m_usTotalLen; //总长度
18     USHORT m_usID; //标识
19     USHORT m_usFlagFragOffset; //3位标志+13位片偏移
20     BYTE m_byTTL; //TTL
21     BYTE m_byProtocol; //协议
22     USHORT m_usHChecksum; //首部检验和
23     ULONG m_ulSrcIP; //源IP地址
24     ULONG m_ulDestIP; //目的IP地址
25 };
26 
27 struct ICMPHeader
28 {
29     BYTE m_byType; //类型
30     BYTE m_byCode; //代码
31     USHORT m_usChecksum; //检验和 
32     USHORT m_usID; //标识符
33     USHORT m_usSeq; //序号
34     ULONG m_ulTimeStamp; //时间戳(非标准ICMP头部)
35 };
36 
37 struct PingReply
38 {
39     USHORT m_usSeq;
40     DWORD m_dwRoundTripTime;
41     DWORD m_dwBytes;
42     DWORD m_dwTTL;
43 };
44 
45 class CPing
46 {
47 public:
48     CPing();
49     ~CPing();
50     BOOL Ping(DWORD dwDestIP, PingReply *pPingReply = NULL, DWORD dwTimeout = 2000);
51     BOOL Ping(char *szDestIP, PingReply *pPingReply = NULL, DWORD dwTimeout = 2000);
52 private:
53     BOOL PingCore(DWORD dwDestIP, PingReply *pPingReply, DWORD dwTimeout);
54     USHORT CalCheckSum(USHORT *pBuffer, int nSize);
55     ULONG GetTickCountCalibrate();
56 private:
57     SOCKET m_sockRaw;
58     WSAEVENT m_event;
59     USHORT m_usCurrentProcID;
60     char *m_szICMPData;
61     BOOL m_bIsInitSucc;
62 private:
63     static USHORT s_usPacketSeq;
64 };
View Code

//--------------------------ping.cpp--------------------------实现部分

cmd命令ping功能之C++实现_数据_04cmd命令ping功能之C++实现_字段_05
  1 #include "ping.h"
  2 #include <iostream>
  3 USHORT CPing::s_usPacketSeq = 0;
  4 
  5 CPing::CPing() :m_szICMPData(NULL),m_bIsInitSucc(FALSE)
  6 {
  7     WSADATA WSAData;
  8     //WSAStartup(MAKEWORD(2, 2), &WSAData);
  9     if (WSAStartup(MAKEWORD(1, 1), &WSAData) != 0)
 10     {
 11         /*如果初始化不成功则报错,GetLastError()返回发生的错误信息*/
 12         printf("WSAStartup() failed: %d\n", GetLastError());
 13         return;
 14     }
 15     m_event = WSACreateEvent();
 16     m_usCurrentProcID = (USHORT)GetCurrentProcessId();
 17     //setsockopt(m_sockRaw);
 18     /*if ((m_sockRaw = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP, NULL, 0, 0)) != SOCKET_ERROR)
 19     {
 20         WSAEventSelect(m_sockRaw, m_event, FD_READ);
 21         m_bIsInitSucc = TRUE;
 22 
 23         m_szICMPData = (char*)malloc(DEF_PACKET_SIZE + sizeof(ICMPHeader));
 24 
 25         if (m_szICMPData == NULL)
 26         {
 27             m_bIsInitSucc = FALSE;
 28         }
 29     }*/
 30     m_sockRaw = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP, NULL, 0, 0);
 31     if (m_sockRaw == INVALID_SOCKET)
 32     {
 33         std::cerr << "WSASocket() failed:" << WSAGetLastError ()<< std::endl;  //10013 以一种访问权限不允许的方式做了一个访问套接字的尝试。
 34     }
 35     else
 36     {
 37         WSAEventSelect(m_sockRaw, m_event, FD_READ);
 38         m_bIsInitSucc = TRUE;
 39 
 40         m_szICMPData = (char*)malloc(DEF_PACKET_SIZE + sizeof(ICMPHeader));
 41 
 42         if (m_szICMPData == NULL)
 43         {
 44             m_bIsInitSucc = FALSE;
 45         }
 46     }
 47 }
 48 
 49 CPing::~CPing()
 50 {
 51     WSACleanup();
 52 
 53     if (NULL != m_szICMPData)
 54     {
 55         free(m_szICMPData);
 56         m_szICMPData = NULL;
 57     }
 58 }
 59 
 60 BOOL CPing::Ping(DWORD dwDestIP, PingReply *pPingReply, DWORD dwTimeout)
 61 {
 62     return PingCore(dwDestIP, pPingReply, dwTimeout);
 63 }
 64 
 65 BOOL CPing::Ping(char *szDestIP, PingReply *pPingReply, DWORD dwTimeout)
 66 {
 67     if (NULL != szDestIP)
 68     {
 69         return PingCore(inet_addr(szDestIP), pPingReply, dwTimeout);
 70     }
 71     return FALSE;
 72 }
 73 
 74 BOOL CPing::PingCore(DWORD dwDestIP, PingReply *pPingReply, DWORD dwTimeout)
 75 {
 76     //判断初始化是否成功
 77     if (!m_bIsInitSucc)
 78     {
 79         return FALSE;
 80     }
 81 
 82     //配置SOCKET
 83     sockaddr_in sockaddrDest;
 84     sockaddrDest.sin_family = AF_INET;
 85     sockaddrDest.sin_addr.s_addr = dwDestIP;
 86     int nSockaddrDestSize = sizeof(sockaddrDest);
 87 
 88     //构建ICMP包
 89     int nICMPDataSize = DEF_PACKET_SIZE + sizeof(ICMPHeader);
 90     ULONG ulSendTimestamp = GetTickCountCalibrate();
 91     USHORT usSeq = ++s_usPacketSeq;
 92     memset(m_szICMPData, 0, nICMPDataSize);
 93     ICMPHeader *pICMPHeader = (ICMPHeader*)m_szICMPData;
 94     pICMPHeader->m_byType = ECHO_REQUEST;
 95     pICMPHeader->m_byCode = 0;
 96     pICMPHeader->m_usID = m_usCurrentProcID;
 97     pICMPHeader->m_usSeq = usSeq;
 98     pICMPHeader->m_ulTimeStamp = ulSendTimestamp;
 99     pICMPHeader->m_usChecksum = CalCheckSum((USHORT*)m_szICMPData, nICMPDataSize);
100 
101     //发送ICMP报文
102     if (sendto(m_sockRaw, m_szICMPData, nICMPDataSize, 0, (struct sockaddr*)&sockaddrDest, nSockaddrDestSize) == SOCKET_ERROR)
103     {
104         return FALSE;
105     }
106 
107     //判断是否需要接收相应报文
108     if (pPingReply == NULL)
109     {
110         return TRUE;
111     }
112 
113     char recvbuf[256] = { "\0" };
114     while (TRUE)
115     {
116         //接收响应报文
117         if (WSAWaitForMultipleEvents(1, &m_event, FALSE, 100, FALSE) != WSA_WAIT_TIMEOUT)
118         {
119             WSANETWORKEVENTS netEvent;
120             WSAEnumNetworkEvents(m_sockRaw, m_event, &netEvent);
121 
122             if (netEvent.lNetworkEvents & FD_READ)
123             {
124                 ULONG nRecvTimestamp = GetTickCountCalibrate();
125                 int nPacketSize = recvfrom(m_sockRaw, recvbuf, 256, 0, (struct sockaddr*)&sockaddrDest, &nSockaddrDestSize);
126                 if (nPacketSize != SOCKET_ERROR)
127                 {
128                     IPHeader *pIPHeader = (IPHeader*)recvbuf;
129                     USHORT usIPHeaderLen = (USHORT)((pIPHeader->m_byVerHLen & 0x0f) * 4);
130                     ICMPHeader *pICMPHeader = (ICMPHeader*)(recvbuf + usIPHeaderLen);
131 
132                     if (pICMPHeader->m_usID == m_usCurrentProcID //是当前进程发出的报文
133                         && pICMPHeader->m_byType == ECHO_REPLY //是ICMP响应报文
134                         && pICMPHeader->m_usSeq == usSeq //是本次请求报文的响应报文
135                         )
136                     {
137                         pPingReply->m_usSeq = usSeq;
138                         pPingReply->m_dwRoundTripTime = nRecvTimestamp - pICMPHeader->m_ulTimeStamp;
139                         pPingReply->m_dwBytes = nPacketSize - usIPHeaderLen - sizeof(ICMPHeader);
140                         pPingReply->m_dwTTL = pIPHeader->m_byTTL;
141                         return TRUE;
142                     }
143                 }
144             }
145         }
146         //超时
147         if (GetTickCountCalibrate() - ulSendTimestamp >= dwTimeout)
148         {
149             return FALSE;
150         }
151     }
152 }
153 
154 USHORT CPing::CalCheckSum(USHORT *pBuffer, int nSize)
155 {
156     unsigned long ulCheckSum = 0;
157     while (nSize > 1)
158     {
159         ulCheckSum += *pBuffer++;
160         nSize -= sizeof(USHORT);
161     }
162     if (nSize)
163     {
164         ulCheckSum += *(UCHAR*)pBuffer;
165     }
166 
167     ulCheckSum = (ulCheckSum >> 16) + (ulCheckSum & 0xffff);
168     ulCheckSum += (ulCheckSum >> 16);
169 
170     return (USHORT)(~ulCheckSum);
171 }
172 
173 ULONG CPing::GetTickCountCalibrate()
174 {
175     static ULONG s_ulFirstCallTick = 0;
176     static LONGLONG s_ullFirstCallTickMS = 0;
177 
178     SYSTEMTIME systemtime;
179     FILETIME filetime;
180     GetLocalTime(&systemtime);
181     SystemTimeToFileTime(&systemtime, &filetime);
182     LARGE_INTEGER liCurrentTime;
183     liCurrentTime.HighPart = filetime.dwHighDateTime;
184     liCurrentTime.LowPart = filetime.dwLowDateTime;
185     LONGLONG llCurrentTimeMS = liCurrentTime.QuadPart / 10000;
186 
187     if (s_ulFirstCallTick == 0)
188     {
189         s_ulFirstCallTick = GetTickCount();
190     }
191     if (s_ullFirstCallTickMS == 0)
192     {
193         s_ullFirstCallTickMS = llCurrentTimeMS;
194     }
195 
196     return s_ulFirstCallTick + (ULONG)(llCurrentTimeMS - s_ullFirstCallTickMS);
197 }
View Code

//-------------------------main.cpp--------------------------测试代码

 1 #include <winsock2.h>
 2 #include <stdio.h>
 3 #include "ping.h"
 4 
 5 int main(void)
 6 {
 7     CPing objPing;
 8 
 9     char *szDestIP = "127.0.0.1";
10     PingReply reply;
11 
12     printf("Pinging %s with %d bytes of data:\n", szDestIP, DEF_PACKET_SIZE);
13     while (TRUE)
14     {
15         objPing.Ping(szDestIP, &reply);
16         printf("Reply from %s: bytes=%d time=%ldms TTL=%ld\n", szDestIP, reply.m_dwBytes, reply.m_dwRoundTripTime, reply.m_dwTTL);
17         Sleep(500);
18     }
19 
20     return 0;
21 }

  结果如下图所示:

  cmd命令ping功能之C++实现_#include_08

四、附录-如何计算检验和

  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位整型(截断)后返回

  其中也遇到了很多问题,头文件包含,WSAStartup函数初始化失败。。。

主要参考:

javascript:void(0)

http://blog.sina.com.cn/s/blog_5cf5e7c401014fvq.html

javascript:void(0)

没有坚守就没有事业,没有执着就没有未来!