今天接到需求要實現ping的功能,然后網上查了一些資料,對網絡編程的一些函數熟悉了一下,雖然還有一些細節不清楚,但是慢慢積累。
要實現這樣的功能:

基礎知識
ping的過程是向目的IP發送一個type=8的ICMP響應請求報文,目標主機收到這個報文之后,會向源IP(發送方,我)回復一個type=0的ICMP響應應答報文。
那上面的字節、往訪時間、TTL之類的信息又是從哪來的呢?這取決於IP和ICMP的頭部。
IP頭部:

頭部內容有點多,我們關心的只有以下幾個:
IHL:首部長度。因為IP的頭部不是定長的,所以需要這個信息進行IP包的解析,從而找到Data字段的起始點。
另外注意這個IHL是以4個字節為單位的,所以首部實際長度是IHL*4字節。
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個字節的時間戳。
在收到應答報文時,取出這個時間戳與當前的時間對比即可。
Ping程序實現步驟
- 創建類型為SOCK_RAW的一個套接字,同時設定協議IPPROTO_ICMP。
- 創建並初始化ICMP頭。
- 調用sendto或WSASendto,將ICMP請求發給遠程主機。
- 調用recvfrom或WSARecvfrom,以接收任何ICMP響應。
ping.h
#pragma once //在默認windows.h會包含winsock.h,當你包含winsock2.h就會沖突,因此在包含windows.h前需要定義一個宏,#define WIN32_LEAN_AND_MEAN ;去除winsock.h //要么將#include <winsock2.h>放在#include<windows.h>前面或者直接去掉#include<windows.h> #include <winsock2.h> #pragma comment(lib, "WS2_32") // 鏈接到WS2_32.lib #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; DWORD m_dwRoundTripTime; DWORD m_dwBytes; DWORD m_dwTTL; }; 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; WSAEVENT m_event; USHORT m_usCurrentProcID; char *m_szICMPData; BOOL m_bIsInitSucc; private: static USHORT s_usPacketSeq; };
ping.cpp
#include "ping.h" #include <iostream> USHORT CPing::s_usPacketSeq = 0; CPing::CPing() :m_szICMPData(NULL),m_bIsInitSucc(FALSE) { WSADATA WSAData; //WSAStartup(MAKEWORD(2, 2), &WSAData); if (WSAStartup(MAKEWORD(1, 1), &WSAData) != 0) { /*如果初始化不成功則報錯,GetLastError()返回發生的錯誤信息*/ printf("WSAStartup() failed: %d\n", GetLastError()); return; } m_event = WSACreateEvent(); m_usCurrentProcID = (USHORT)GetCurrentProcessId(); //setsockopt(m_sockRaw); /*if ((m_sockRaw = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP, NULL, 0, 0)) != SOCKET_ERROR) { 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; } }*/ m_sockRaw = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP, NULL, 0, 0); if (m_sockRaw == INVALID_SOCKET) { std::cerr << "WSASocket() failed:" << WSAGetLastError ()<< std::endl; //10013 以一種訪問權限不允許的方式做了一個訪問套接字的嘗試。 } 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; } } 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; } 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); }
main.cpp
#include <winsock2.h> #include <stdio.h> #include "ping.h" int main(void) { CPing objPing; char *szDestIP = "127.0.0.1"; PingReply reply; printf("Pinging %s with %d bytes of data:\n", szDestIP, DEF_PACKET_SIZE); while (TRUE) { objPing.Ping(szDestIP, &reply); printf("Reply from %s: bytes=%d time=%ldms TTL=%ld\n", szDestIP, reply.m_dwBytes, reply.m_dwRoundTripTime, reply.m_dwTTL); Sleep(500); } return 0; }
結果:

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函數初始化失敗。。。
主要參考:
http://www.cnblogs.com/goagent/p/4078940.html
http://blog.sina.com.cn/s/blog_5cf5e7c401014fvq.html
http://blog.csdn.net/segen_jaa/article/details/7569727
