最近在看些Windows下网络编程问题。看到原始套接字的使用,于是看了Ping程序的功能实现。
大部分人用ping命令只是作为查看另一个系统的网络连接是否正常的一种简单方法。这里我介绍下在Windows下实现ping程序的两种方法。
一是使用原始套接字的方法:
代码如下:
- #include <Winsock2.h>
- #pragma comment(lib, "ws2_32.lib")
- #pragma comment(lib, "Iphlpapi.lib")
- typedef struct icmp_hdr
- {
- unsigned char icmp_type;
- unsigned char icmp_code;
- unsigned short icmp_checksum;
- //回显头
- unsigned short icmp_id;
- unsigned short icmp_sequence;
- unsigned long icmp_timestamp;
- }ICMP_HDR,*PICMP_HDR;
- typedef struct _IPHeader // 20字节的IP头
- {
- UCHAR iphVerLen; // 版本号和头长度(各占4位)
- UCHAR ipTOS; // 服务类型
- USHORT ipLength; // 封包总长度,即整个IP报的长度
- USHORT ipID; // 封包标识,惟一标识发送的每一个数据报
- USHORT ipFlags; // 标志
- UCHAR ipTTL; // 生存时间,就是TTL
- UCHAR ipProtocol; // 协议,可能是TCP、UDP、ICMP等
- USHORT ipChecksum; // 校验和
- ULONG ipSource; // 源IP地址
- ULONG ipDestination; // 目标IP地址
- } IPHeader, *PIPHeader;
- USHORT checksum(USHORT* buffer, int size)
- {
- unsigned long cksum = 0;
- while(size > 1 )
- {
- cksum += *buffer++;
- size -=sizeof(USHORT);
- }
- if(size)
- {
- cksum += *(UCHAR*)buffer;
- }
- cksum = (cksum>>16)+(cksum & 0xffff);
- cksum += (cksum >> 16);
- return (USHORT)(~cksum);
- }
- BOOL SetTimeout(SOCKET s, int nTime, BOOL bRecv)
- {
- int ret = ::setsockopt(s, SOL_SOCKET,
- bRecv ? SO_RCVTIMEO : SO_SNDTIMEO, (char*)&nTime, sizeof(nTime));
- return ret != SOCKET_ERROR;
- }
- int main(int argc, char* argv[])
- {
- if ( argc < 2)
- {
- printf("Usage: Ping IP address./n");
- return -1;
- }
- char szDestIp[20];
- strcpy_s(szDestIp, argv[1]);
- //初始化套接字
- WSADATA wsaData;
- int wsaset = WSAStartup(0x101, &wsaData);
- SOCKET sRaw = ::socket(AF_INET, SOCK_RAW,IPPROTO_ICMP);
- int a = ::GetLastError();
- SetTimeout(sRaw, 1000, TRUE);
- SOCKADDR_IN dest;
- dest.sin_family = AF_INET;
- dest.sin_port = htons(0);
- dest.sin_addr.S_un.S_addr = inet_addr(szDestIp);
- //创建ICMP封包
- char buff[sizeof(ICMP_HDR)+32];
- ICMP_HDR* pIcmp = (ICMP_HDR*)buff;
- //填写ICMP包
- pIcmp->icmp_type = 8;
- pIcmp->icmp_code = 0;
- pIcmp->icmp_id = (USHORT)::GetCurrentProcessId();
- pIcmp->icmp_checksum = 0;
- pIcmp->icmp_sequence = 0;
- //填充数据
- memset(&buff[sizeof(ICMP_HDR)],'E', 32);
- //开始发送和接收ICMP封包
- USHORT nSeq = 0;
- char recvBuf[1024];
- SOCKADDR_IN from;
- int nLen = sizeof(from);
- while( TRUE )
- {
- static int nCount = 0;
- int nRet;
- if(nCount++ == 4)
- break;
- pIcmp->icmp_checksum = 0;
- pIcmp->icmp_timestamp = ::GetTickCount();
- pIcmp->icmp_sequence = nSeq++;
- pIcmp->icmp_checksum = checksum((USHORT*)buff, sizeof(ICMP_HDR) + 32);
- //发送
- nRet = ::sendto(sRaw, buff, sizeof(ICMP_HDR) + 32, 0, (SOCKADDR *)&dest, sizeof(dest));
- if(nRet == SOCKET_ERROR)
- {
- printf(" sendto() failed: %d /n", ::WSAGetLastError());
- return -1;
- }
- nRet = ::recvfrom(sRaw, recvBuf, 1024, 0, (sockaddr*)&from, &nLen);
- if(nRet == SOCKET_ERROR)
- {
- if(::WSAGetLastError() == WSAETIMEDOUT)
- {
- printf(" timed out/n");
- continue;
- }
- printf(" recvfrom() failed: %d/n", ::WSAGetLastError());
- return -1;
- }
- // 下面开始解析接收到的ICMP封包
- int nTick = ::GetTickCount();
- if(nRet < sizeof(IPHeader) + sizeof(ICMP_HDR))
- {
- printf(" Too few bytes from %s /n", ::inet_ntoa(from.sin_addr));
- }
- // 接收到的数据中包含IP头,IP头大小为20个字节,所以加20得到ICMP头
- ICMP_HDR* pRecvIcmp = (ICMP_HDR*)(recvBuf + 20); // (ICMP_HDR*)(recvBuf + sizeof(IPHeader));
- if(pRecvIcmp->icmp_type != 0) // 回显
- {
- printf(" nonecho type %d recvd /n", pRecvIcmp->icmp_type);
- return -1;
- }
- if(pRecvIcmp->icmp_id != ::GetCurrentProcessId())
- {
- printf(" someone else's packet! /n");
- return -1;
- }
- printf(" %d bytes from %s:", nRet, inet_ntoa(from.sin_addr));
- printf(" icmp_seq = %d. ", pRecvIcmp->icmp_sequence);
- printf(" time: %d ms", nTick - pRecvIcmp->icmp_timestamp);
- printf(" /n");
- ::Sleep(1000);
- }
- system("pause");
- return 0;
- }
这是使用了原始套接字,实现发送ICMP数据包,实现ping程序。
在Linux中的Ping使用了这种方式。
这里是一个比较完整的关于Ping程序介绍的文章:http://www.ibm.com/developerworks/cn/linux/network/ping/index.html
这是Windows下实现的Ping程序:http://blog.youkuaiyun.com/tancfjob/archive/2008/05/07/2408978.aspx
但是在使用Windows下实现Ping的时候发现使用原始套接字需要管理员权限,但是Windows下使用ping程序并不需要使用管理员权限。感谢shadowstar 提示了我使用IcmpSendEcho来实现功能。
下面是使用IcmpSendEcho的示例代码:
- #pragma comment(lib, "ws2_32.lib")
- #pragma comment(lib, "Iphlpapi.lib")
- int __cdecl main(int argc, char **argv) {
- // Declare and initialize variables
- HANDLE hIcmpFile;
- unsigned long ipaddr = INADDR_NONE;
- DWORD dwRetVal = 0;
- char SendData[] = "Data Buffer";
- LPVOID ReplyBuffer = NULL;
- DWORD ReplySize = 0;
- // Validate the parameters
- if (argc != 2) {
- printf("usage: %s IP address/n", argv[0]);
- return 1;
- }
- ipaddr = inet_addr(argv[1]);
- if (ipaddr == INADDR_NONE) {
- printf("usage: %s IP address/n", argv[0]);
- return 1;
- }
- hIcmpFile = IcmpCreateFile();
- if (hIcmpFile == INVALID_HANDLE_VALUE) {
- printf("/tUnable to open handle./n");
- printf("IcmpCreatefile returned error: %ld/n", GetLastError() );
- return 1;
- }
- ReplySize = sizeof(ICMP_ECHO_REPLY) + sizeof(SendData);
- ReplyBuffer = (VOID*) malloc(ReplySize);
- if (ReplyBuffer == NULL) {
- printf("/tUnable to allocate memory/n");
- return 1;
- }
- dwRetVal = IcmpSendEcho(hIcmpFile, ipaddr, SendData, sizeof(SendData),
- NULL, ReplyBuffer, ReplySize, 1000);
- if (dwRetVal != 0) {
- PICMP_ECHO_REPLY pEchoReply = (PICMP_ECHO_REPLY)ReplyBuffer;
- struct in_addr ReplyAddr;
- ReplyAddr.S_un.S_addr = pEchoReply->Address;
- printf("/tSent icmp message to %s/n", argv[1]);
- if (dwRetVal > 1) {
- printf("/tReceived %ld icmp message responses/n", dwRetVal);
- printf("/tInformation from the first response:/n");
- }
- else {
- printf("/tReceived %ld icmp message response/n", dwRetVal);
- printf("/tInformation from this response:/n");
- }
- printf("/t Received from %s/n", inet_ntoa( ReplyAddr ) );
- printf("/t Status = %ld/n",
- pEchoReply->Status);
- printf("/t Roundtrip time = %ld milliseconds/n",
- pEchoReply->RoundTripTime);
- }
- else {
- printf("/tCall to IcmpSendEcho failed./n");
- printf("/tIcmpSendEcho returned error: %ld/n", GetLastError() );
- return 1;
- }
- return 0;
- }
此段代码需要ws2_32.lib和Iphlpapi.lib,包含头文件iphlpapi.h和icmpapi.h