這是一個老話題了,但是我剛學會...
我們的目的是實現這麼個東西:
之所以用紅框框一下是因為,從baidu.com到123.125.114.144的過程是DNS解析,我們暫時先實現ping的部分。
基礎知識
ping的過程是向目的IP發送一個type=8的ICMP響應請求報文,目標主機收到這個報文之後,會向源IP(發送方,我)回復一個type=0的ICMP響應應答報文。
那上面的字節、往訪時間、TTL之類的信息又是從哪來的呢?這取決於IP和ICMP的頭部。
IP頭部:
頭部內容有點多,我們關心的只有以下幾個:
IHL:首部長度。因為IP的頭部不是定長的,所以需要這個信息進行IP包的解析,從而找到Data字段的起始點。
Time to Live:生存時間,這個就是TTL了。
Data:這部分是IP包的數據,也就是ICMP的報文內容。
ICMP響應請求/應答報文頭部:
Type:類型,type=8表示響應請求報文,type=0表示響應應答報文。
Code:代碼,與type組合,表示具體的信息,參考這裡。
Checksum:檢驗和,這個是整個ICMP報文的檢驗和,包括Type、Code、...、Data。
Identifier:標識符,這個一般填入本進程的標識符。
Sequence Number:序號
Data:數據部分
上面是標准的ICMP報文,一般而言,統計ping的往返時間的做法是,在ICMP報文的Data區域寫入4個字節的時間戳。
在收到應答報文時,取出這個時間戳與當前的時間對比即可。
代碼實現
1 #pragma once 2 3 #include <windows.h> 4 5 //這裡需要導入庫 Ws2_32.lib,在不同的IDE下可能不太一樣 6 //#pragma comment(lib, "Ws2_32.lib") 7 8 #define DEF_PACKET_SIZE 32 9 #define ECHO_REQUEST 8 10 #define ECHO_REPLY 0 11 12 struct IPHeader 13 { 14 BYTE m_byVerHLen; //4位版本+4位首部長度 15 BYTE m_byTOS; //服務類型 16 USHORT m_usTotalLen; //總長度 17 USHORT m_usID; //標識 18 USHORT m_usFlagFragOffset; //3位標志+13位片偏移 19 BYTE m_byTTL; //TTL 20 BYTE m_byProtocol; //協議 21 USHORT m_usHChecksum; //首部檢驗和 22 ULONG m_ulSrcIP; //源IP地址 23 ULONG m_ulDestIP; //目的IP地址 24 }; 25 26 struct ICMPHeader 27 { 28 BYTE m_byType; //類型 29 BYTE m_byCode; //代碼 30 USHORT m_usChecksum; //檢驗和 31 USHORT m_usID; //標識符 32 USHORT m_usSeq; //序號 33 ULONG m_ulTimeStamp; //時間戳(非標准ICMP頭部) 34 }; 35 36 struct PingReply 37 { 38 USHORT m_usSeq; 39 DWORD m_dwRoundTripTime; 40 DWORD m_dwBytes; 41 DWORD m_dwTTL; 42 }; 43 44 class CPing 45 { 46 public: 47 CPing(); 48 ~CPing(); 49 BOOL Ping(DWORD dwDestIP, PingReply *pPingReply = NULL, DWORD dwTimeout = 2000); 50 BOOL Ping(char *szDestIP, PingReply *pPingReply = NULL, DWORD dwTimeout = 2000); 51 private: 52 BOOL PingCore(DWORD dwDestIP, PingReply *pPingReply, DWORD dwTimeout); 53 USHORT CalCheckSum(USHORT *pBuffer, int nSize); 54 ULONG GetTickCountCalibrate(); 55 private: 56 SOCKET m_sockRaw; 57 WSAEVENT m_event; 58 USHORT m_usCurrentProcID; 59 char *m_szICMPData; 60 BOOL m_bIsInitSucc; 61 private: 62 static USHORT s_usPacketSeq; 63 }; View Code [ping.h] 1 #include "ping.h" 2 3 USHORT CPing::s_usPacketSeq = 0; 4 5 CPing::CPing() : 6 m_szICMPData(NULL), 7 m_bIsInitSucc(FALSE) 8 { 9 WSADATA WSAData; 10 WSAStartup(MAKEWORD(1, 1), &WSAData); 11 12 m_event = WSACreateEvent(); 13 m_usCurrentProcID = (USHORT)GetCurrentProcessId(); 14 15 if ((m_sockRaw = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP, NULL, 0, 0)) != SOCKET_ERROR) 16 { 17 WSAEventSelect(m_sockRaw, m_event, FD_READ); 18 m_bIsInitSucc = TRUE; 19 20 m_szICMPData = (char*)malloc(DEF_PACKET_SIZE + sizeof(ICMPHeader)); 21 22 if (m_szICMPData == NULL) 23 { 24 m_bIsInitSucc = FALSE; 25 } 26 } 27 } 28 29 CPing::~CPing() 30 { 31 WSACleanup(); 32 33 if (NULL != m_szICMPData) 34 { 35 free(m_szICMPData); 36 m_szICMPData = NULL; 37 } 38 } 39 40 BOOL CPing::Ping(DWORD dwDestIP, PingReply *pPingReply, DWORD dwTimeout) 41 { 42 return PingCore(dwDestIP, pPingReply, dwTimeout); 43 } 44 45 BOOL CPing::Ping(char *szDestIP, PingReply *pPingReply, DWORD dwTimeout) 46 { 47 if (NULL != szDestIP) 48 { 49 return PingCore(inet_addr(szDestIP), pPingReply, dwTimeout); 50 } 51 return FALSE; 52 } 53 54 BOOL CPing::PingCore(DWORD dwDestIP, PingReply *pPingReply, DWORD dwTimeout) 55 { 56 //判斷初始化是否成功 57 if (!m_bIsInitSucc) 58 { 59 return FALSE; 60 } 61 62 //配置SOCKET 63 sockaddr_in sockaddrDest; 64 sockaddrDest.sin_family = AF_INET; 65 sockaddrDest.sin_addr.s_addr = dwDestIP; 66 int nSockaddrDestSize = sizeof(sockaddrDest); 67 68 //構建ICMP包 69 int nICMPDataSize = DEF_PACKET_SIZE + sizeof(ICMPHeader); 70 ULONG ulSendTimestamp = GetTickCountCalibrate(); 71 USHORT usSeq = ++s_usPacketSeq; 72 memset(m_szICMPData, 0, nICMPDataSize); 73 ICMPHeader *pICMPHeader = (ICMPHeader*)m_szICMPData; 74 pICMPHeader->m_byType = ECHO_REQUEST; 75 pICMPHeader->m_byCode = 0; 76 pICMPHeader->m_usID = m_usCurrentProcID; 77 pICMPHeader->m_usSeq = usSeq; 78 pICMPHeader->m_ulTimeStamp = ulSendTimestamp; 79 pICMPHeader->m_usChecksum = CalCheckSum((USHORT*)m_szICMPData, nICMPDataSize); 80 81 //發送ICMP報文 82 if (sendto(m_sockRaw, m_szICMPData, nICMPDataSize, 0, (struct sockaddr*)&sockaddrDest, nSockaddrDestSize) == SOCKET_ERROR) 83 { 84 return FALSE; 85 } 86 87 //判斷是否需要接收相應報文 88 if (pPingReply == NULL) 89 { 90 return TRUE; 91 } 92 93 char recvbuf[256] = {"\0"}; 94 while (TRUE) 95 { 96 //接收響應報文 97 if (WSAWaitForMultipleEvents(1, &m_event, FALSE, 100, FALSE) != WSA_WAIT_TIMEOUT) 98 { 99 WSANETWORKEVENTS netEvent; 100 WSAEnumNetworkEvents(m_sockRaw, m_event, &netEvent); 101 102 if (netEvent.lNetworkEvents & FD_READ) 103 { 104 ULONG nRecvTimestamp = GetTickCountCalibrate(); 105 int nPacketSize = recvfrom(m_sockRaw, recvbuf, 256, 0, (struct sockaddr*)&sockaddrDest, &nSockaddrDestSize); 106 if (nPacketSize != SOCKET_ERROR) 107 { 108 IPHeader *pIPHeader = (IPHeader*)recvbuf; 109 USHORT usIPHeaderLen = (USHORT)((pIPHeader->m_byVerHLen & 0x0f) * 4); 110 ICMPHeader *pICMPHeader = (ICMPHeader*)(recvbuf + usIPHeaderLen); 111 112 if (pICMPHeader->m_usID == m_usCurrentProcID //是當前進程發出的報文 113 && pICMPHeader->m_byType == ECHO_REPLY //是ICMP響應報文 114 && pICMPHeader->m_usSeq == usSeq //是本次請求報文的響應報文 115 ) 116 { 117 pPingReply->m_usSeq = usSeq; 118 pPingReply->m_dwRoundTripTime = nRecvTimestamp - pICMPHeader->m_ulTimeStamp; 119 pPingReply->m_dwBytes = nPacketSize - usIPHeaderLen - sizeof(ICMPHeader); 120 pPingReply->m_dwTTL = pIPHeader->m_byTTL; 121 return TRUE; 122 } 123 } 124 } 125 } 126 //超時 127 if (GetTickCountCalibrate() - ulSendTimestamp >= dwTimeout) 128 { 129 return FALSE; 130 } 131 } 132 } 133 134 USHORT CPing::CalCheckSum(USHORT *pBuffer, int nSize) 135 { 136 unsigned long ulCheckSum=0; 137 while(nSize > 1) 138 { 139 ulCheckSum += *pBuffer++; 140 nSize -= sizeof(USHORT); 141 } 142 if(nSize ) 143 { 144 ulCheckSum += *(UCHAR*)pBuffer; 145 } 146 147 ulCheckSum = (ulCheckSum >> 16) + (ulCheckSum & 0xffff); 148 ulCheckSum += (ulCheckSum >>16); 149 150 return (USHORT)(~ulCheckSum); 151 } 152 153 ULONG CPing::GetTickCountCalibrate() 154 { 155 static ULONG s_ulFirstCallTick = 0; 156 static LONGLONG s_ullFirstCallTickMS = 0; 157 158 SYSTEMTIME systemtime; 159 FILETIME filetime; 160 GetLocalTime(&systemtime); 161 SystemTimeToFileTime(&systemtime, &filetime); 162 LARGE_INTEGER liCurrentTime; 163 liCurrentTime.HighPart = filetime.dwHighDateTime; 164 liCurrentTime.LowPart = filetime.dwLowDateTime; 165 LONGLONG llCurrentTimeMS = liCurrentTime.QuadPart / 10000; 166 167 if (s_ulFirstCallTick == 0) 168 { 169 s_ulFirstCallTick = GetTickCount(); 170 } 171 if (s_ullFirstCallTickMS == 0) 172 { 173 s_ullFirstCallTickMS = llCurrentTimeMS; 174 } 175 176 return s_ulFirstCallTick + (ULONG)(llCurrentTimeMS - s_ullFirstCallTickMS); 177 } View Code [ping.cpp]1 #include <windows.h> 2 #include <stdio.h> 3 #include "ping.h" 4 5 int main(void) 6 { 7 CPing objPing; 8 9 char *szDestIP = "123.125.114.144"; 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=%ld time=%ldms TTL=%ld\n", szDestIP, reply.m_dwBytes, reply.m_dwRoundTripTime, reply.m_dwTTL); 17 Sleep(500); 18 } 19 20 return 0; 21 }
執行結果
附錄:如何計算校驗和
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位整型(截斷)後返回