Socket编程之ping程序的实现
来源:互联网 发布:java开源社区聚类 编辑:程序博客网 时间:2024/05/03 21:39
大家都知道,ping程序是基于ICMP回显请求和应答报文的,通过IP数据报的选项字段可以达到记录路由的效果,老实说,在刚刚拿到这个课题时,毫无头绪,根本不知道如何下手,因为之前根本就没有从事过Socket网络编程,但是这又是必须的,没办法,凭借之前对孙鑫老师MFC视频教程里的网络编程的基础知识,以及计算机网络、TCP/IP详解等课本的知识,最终结合网上的参考代码,加上自己的理解和调试,解决众多BUG问题,可以说是对网上的代码进行了一定层次的优化。下面附上代码,希望对大家有所帮助,也希望能够和我交流思想。
头文件部分:/*导入库文件*/#pragma comment( lib, "ws2_32.lib" )/*加载头文件*/#include <Winsock2.h>//创建套接字头文件#include <ws2tcpip.h>#include <cmath>#include <cstdio>#include <cstdlib>#include <iostream>#include <iomanip>#include <cstring>using namespace std;/*定义常量*//*表示要记录路由*/#define IP_RECORD_ROUTE 0x7/*默认数据报大小*/#define DEF_PACKET_SIZE 32 /*最大的ICMP数据报大小*/#define MAX_PACKET 1024 /*最大IP头长度*/#define MAX_IP_HDR_SIZE 60 /*ICMP报文类型,回显请求*/ #define ICMP_ECHO 8/*ICMP报文类型,回显应答*/ #define ICMP_ECHOREPLY 0/*最小的ICMP数据报大小*/#define ICMP_MIN 8/*自定义函数原型*/void Init_Ping();void UserHelp();void GetArgments(int argc, char** argv); USHORT CheckSum(USHORT *buffer, int size);void FillICMPData(char *icmp_data, int datasize);void FreeRes();void DecodeIPOptions(char *buf, int bytes); //ip报文各字段解析void DecodeICMPHeader(char *buf, int bytes, SOCKADDR_IN* from); //icmp报文首部解析翻译void PingTest(int timeout); //ping命令测试程序void PingInput(int &argc,char *argv[]);/*IP报头字段数据结构*/typedef struct _iphdr //声明定义一个结构体,用来表示ip首部{ unsigned int h_len:4; /*IP报头长度 位域为4(虽然为int32位的,但只用4位)*/ unsigned int version:4; /*IP的版本号 4*/ unsigned char tos; /*服务类型 8*/ unsigned short total_len; /*数据报总长度 16*/ unsigned short ident; /*惟一的标识符 16*/ unsigned short frag_flags; /*分段标志 16*/ unsigned char ttl; /*生存期 8*/ unsigned char proto; /*协议类型(TCP、UDP等) 8*/ unsigned short checksum; /*校验和 16*/ unsigned int sourceIP; /*源IP地址 32*/ unsigned int destIP; /*目的IP地址 32*/} IpHeader; /*ICMP报头字段数据结构*/typedef struct _icmphdr //同上,这里表示icmp报文的首部:用数据结构表示{ BYTE i_type; /*ICMP报文类型 8位*/ BYTE i_code; /*该类型中的代码号 8位*/ USHORT i_cksum; /*校验和 16位*/ USHORT i_id; /*惟一的标识符 16位*/ USHORT i_seq; /*序列号 16位*/ ULONG timestamp; /*时间戳 32位*/} IcmpHeader;/*IP选项头字段数据结构*/typedef struct _ipoptionhdr //表示ip首部中的可选字段{ unsigned char code; /*选项类型 8位*/ unsigned char len; /*选项头长度 8位*/ unsigned char ptr; /*地址偏移长度 8位*/ unsigned long addr[9]; /*记录的IP地址列表 32位*/ } IpOptionHeader;SOCKET m_socket; IpOptionHeader IpOption; SOCKADDR_IN DestAddr; SOCKADDR_IN SourceAddr; char *icmp_data; char *recvbuf; USHORT seq_no ; //typedef unsigned short USHORT;char *lpdest; //lpdest表示目的地址的指针int datasize; bool RecordFlag; double PacketNum; bool InputTrue;int Rcount;
源文件部分:#include"ping.h" /*初始化变量*/ void Init_Ping() { WSADATA wsaData; icmp_data = NULL; seq_no = 0; recvbuf = NULL; RecordFlag = FALSE; //记录标志为false lpdest = NULL; datasize = DEF_PACKET_SIZE; PacketNum = 4; InputTrue = FALSE; Rcount=9; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) //加载套接字库,进行套接字库的版本协商,确定哪一个版本的套接字库 { cout<<"WSAStartup() failed: "<<GetLastError()<<endl; return ; } m_socket = INVALID_SOCKET; //初始化套接字描述符为无效的}//成功加载winsock dll动态链接库后//WSA data结构 lpwsadata 指向wsa data结构的指针//高位字节--副版本,低位字节--主版本//makeword()这个宏来得到WSA version 的word值,/*显示帮助信息*/void UserHelp(){ cout<<"UserHelp: ping [-r -n|-n] <host/ip> [data size]"<<endl; cout<<" -r [count|0-9] record route"<<endl; cout<<" -n record amount"<<endl; cout<<" host remote machine to ping"<<endl; cout<<" datasize can be up to 1KB"<<endl;}/*获取ping选项、参数信息*/void GetArgments(int argc,char* argv[]) { int i,j; int exp,len,m; if(argc == 1) /*如果没有指定目的地地址和任何选项*/ { cout<<endl<<"Please specify the destination IP address and the ping option as follow!"<<endl; InputTrue=true;UserHelp(); //请指明目的ip地址和如下的选项字段 -->调用用户使用帮助return ; } for(i = 1; i < argc; i++) //参数的个数>1,即不止一个"ping"字符串 { len = strlen(argv[i]); if (argv[i][0] == '-') //是否为选项字段 { if(isdigit(argv[i][1])) { PacketNum = 0; for(j=len-1,exp=0;j>=1;j--,exp++) //将输入的记录条数转为10进制数 PacketNum += ((double)(argv[i][j]-48))*pow(10,exp); } else { switch (tolower(argv[i][1])) //转为小写字母 { case 'r': //表明要记录路由 RecordFlag = TRUE; break; default: //输入不符合规范InputTrue=true; UserHelp();return ; } } } /*参数是数据报大小或者IP地址*/ else if (isdigit(argv[i][0])) //判断是否为数字 { for(m=1;m<len;m++) //这里会有一个问题,就是指定icmp报文数据部分<10时的问题 { if(!(isdigit(argv[i][m]))) { lpdest = argv[i]; //只要数字中存在不是数字的,就说明是ip地址,而不是数据报大小 break; } if(m==len-1&&i==argc-1) //光有数字,代表用户指明数据报大小 datasize = atoi(argv[i]); }if(RecordFlag&&i<argc-1)Rcount=atoi(argv[i]); } else //上述情况都不满足就是主机名了 {lpdest = argv[i]; } }}//inet_addr()函数可以将一个点分十进制的ip地址转为u_long类型以适合分配给S_addr//inet_ntoa()函数接收一个in_addr结构体类型的参数并返回一个点分十进制表示的ip地址//从主机字节序转换为TCP/IP网络字节序/*求校验和*/USHORT CheckSum(USHORT *buffer, int size) //模拟二进制反码求和(先取反再按二进制直接相加)的整个过程{ unsigned long cksum=0; //注意buffer指针的基类型 while (size > 1) { cksum += *buffer++; size -= sizeof(USHORT); //无符号短整型占两个字节,即16位 } if (size) { cksum += *(UCHAR*)buffer; }while(cksum>>16) cksum = (cksum>>16) + (cksum & 0xffff); //两次就够了,就能保证高16位为全0 return (USHORT)(~cksum); //最后的二进制反码和再取反赋值给检验和字段}/*填充ICMP数据报字段*/void FillICMPData(char *icmp_data, int datasize){ IcmpHeader *icmp_hdr = NULL; char *datapart = NULL; icmp_hdr = (IcmpHeader*)icmp_data; icmp_hdr->i_type = ICMP_ECHO; //为数字8,代表回显请求 icmp_hdr->i_code = 0; icmp_hdr->i_id = (USHORT)GetCurrentProcessId(); //获取当前进程的PID作为标识符 icmp_hdr->i_cksum = 0; icmp_hdr->i_seq = 0; datapart = icmp_data + sizeof(IcmpHeader); //icmp数据包的数据部分 memset(datapart,'0',datasize-sizeof(IcmpHeader)); //字符填充或者是0比特填充,此处0比特填充}/*释放资源*/void FreeRes(){ if (m_socket != INVALID_SOCKET) //只要套接字不等于无效的套接字,就关闭套接字,以释放为套接字分配的资源 closesocket(m_socket); HeapFree(GetProcessHeap(), 0, recvbuf); //释放堆分配的内存 HeapFree(GetProcessHeap(), 0, icmp_data); WSACleanup(); //终止对winsocket套接字库的使用 return ;}/*解读IP选项头*/void DecodeIPOptions(char *buf, int bytes) { IpOptionHeader *ipopt = NULL; IN_ADDR inaddr; int i; HOSTENT *host = NULL; /*获取路由信息的地址入口*/ ipopt = (IpOptionHeader *)(buf + 19); //因为如果设置了ip首部选项字段的话,buf是指向ip数据报首字节的 //移动20字节,不久指向了ip option吗 cout<<"路由: "; int temp=(Rcount<=(ipopt->ptr/4))?Rcount:(ipopt->ptr/4); for(i = 0; i < temp; i++) //ptr是地址偏移长度,4字节一移动,因为ip地址为4个字节 { inaddr.S_un.S_addr = ipopt->addr[i]; //选项字段的ip地址赋给了地址结构体变量 if (i != 0) cout<<" "; /*根据IP地址获取主机名*/ host = gethostbyaddr((char *)&inaddr.S_un.S_addr,sizeof(inaddr.S_un.S_addr), AF_INET); if (host) //获取成功 cout<<"("<<setw(-15)<<inet_ntoa(inaddr)<<")"<<" "<<host->h_name;//inet_ntoa通过接收in_addr结构体返回点分十进制的ip地址 host结构体存储了主机的信息 else //获取失败 cout<<"("<<setw(-15)<<inet_ntoa(inaddr)<<") ";if(i!=temp-1)cout<<"->";cout<<endl; } return;} /*解读ICMP报头*/int icmpcount; //icmp报文的个数void DecodeICMPHeader(char *buf, int bytes, SOCKADDR_IN *from){ IpHeader *iphdr = NULL; //ip首部的指针 IcmpHeader *icmphdr = NULL; //icmp首部的指针 unsigned short iphdrlen; //ip首部的长度 DWORD tick; iphdr = (IpHeader *)buf; //指针的基类型转换-强制类型转换 iphdrlen = iphdr->h_len * 4; //ip的首部长度字段乘以4个字节就是ip首部的总长度 tick = GetTickCount(); /*如果IP报头的长度为最大长度(基本长度是20字节),则认为有IP选项,需要解读IP选项*/ if ((iphdrlen == MAX_IP_HDR_SIZE) && (!icmpcount)) //ip首部的最大长度为60字节,15*4=60 DecodeIPOptions(buf, bytes); //解读ip首部选项字段(路由信息) if (bytes < iphdrlen + ICMP_MIN) /*如果读取的数据太小 iphdrlen=20 icmp_min=8*/ { cout<<"Too few bytes from "<<inet_ntoa(from->sin_addr)<<endl; //打印输出对方ip发过来的数据太小了 } icmphdr = (IcmpHeader*)(buf + iphdrlen); //icmp首部指针由空指针指向了icmp首部,只需要指向ip首部的指针移动ip首部的长度就可以了/*如果收到的不是回显应答报文则报错*/ if (icmphdr->i_type != ICMP_ECHOREPLY) { printf("non-echo type of %d is recvd!!!\n",icmphdr->i_type); return; } /*核实收到的ID号和发送的是否一致*/ if (icmphdr->i_id != (USHORT)GetCurrentProcessId()) //GetCurrentProcessId返回当前进程的pid { cout<<"someone else's packet!"<<endl; //如果pid不一致,代表不是我这个进程的icmp响应报文 return ; } cout<<bytes<<" bytes from "<<inet_ntoa(from->sin_addr)<<":"; //字节数和ip地址 cout<<" icmp_seq = "<<icmphdr->i_seq<<". "; //序号 cout<<" time: "<<tick - icmphdr->timestamp<<" ms"<<endl; //花费的时间 icmpcount++; return;}/*开始ping测试*/void PingTest(int timeout) { int ret; int readNum; int fromlen;int smiss=PacketNum,rmiss=PacketNum; struct hostent *hp = NULL; //hostent结构用于存储一个给定主机的信息:例如主机名,ip地址 /*创建原始套接字,该套接字用于ICMP协议*/ m_socket = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP, NULL, 0,WSA_FLAG_OVERLAPPED); /*如果套接字创建不成功,即创建的是无效的套接字*/ if (m_socket == INVALID_SOCKET) { cout<<"WSASocket() failed: "<<WSAGetLastError()<<endl; return ; } if (RecordFlag) //若要记录路由,进行ip选项字段的填充 { ZeroMemory(&IpOption, sizeof(IpOption)); //zeromemory函数用0填充一块内存 /*为每个ICMP包设置路由选项*/ IpOption.code = IP_RECORD_ROUTE; IpOption.ptr = 4; IpOption.len = 39; //4*9+3=39 /*设置套结字的选项函数 setsockopt()*/ ret = setsockopt(m_socket, IPPROTO_IP, IP_OPTIONS,(char *)&IpOption, sizeof(IpOption)); if (ret == SOCKET_ERROR) { cout<<"setsockopt(IP_OPTIONS) failed: "<<WSAGetLastError()<<endl; } } /*设置接收的超时值*/ readNum = setsockopt(m_socket, SOL_SOCKET, SO_RCVTIMEO,(char*)&timeout, sizeof(timeout)); if(readNum == SOCKET_ERROR) { cout<<"setsockopt(SO_RCVTIMEO) failed: "<<WSAGetLastError()<<endl; return ; } /*设置发送的超时值*/ timeout = 1000; //设置发送超时 readNum = setsockopt(m_socket, SOL_SOCKET, SO_SNDTIMEO,(char*)&timeout, sizeof(timeout)); if (readNum == SOCKET_ERROR) { cout<<"setsockopt(SO_SNDTIMEO) failed: "<<WSAGetLastError()<<endl; return ; } memset(&DestAddr, 0, sizeof(DestAddr)); //用0初始化目的地地址 DestAddr.sin_family = AF_INET; //TCP/IP中此字段必须为AF_INET if ((DestAddr.sin_addr.s_addr = inet_addr(lpdest)) == INADDR_NONE) //如果是0xffffffff { if ((hp = gethostbyname(lpdest)) != NULL) //名字解析,根据主机名获取IP地址 { /*将获取到的IP值赋给目的地地址中的相应字段*/ memcpy(&(DestAddr.sin_addr), hp->h_addr, hp->h_length); /*将获取到的地址族值赋给目的地地址中的相应字段*/ DestAddr.sin_family = hp->h_addrtype; cout<<"DestAddr.sin_addr = "<<inet_ntoa(DestAddr.sin_addr)<<endl; } else //获取不成功 { cout<<"gethostbyname() failed: "<<WSAGetLastError()<<endl; return ; } } datasize += sizeof(IcmpHeader); //icmp数据部分大小+ICMP首部组成icmp报文长度 /*根据默认堆句柄,从堆中分配MAX_PACKET内存块,新分配内存的内容将被初始化为0*/ icmp_data =(char*) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY,MAX_PACKET); //堆分配函数,调用成功返回指向该堆的指针 recvbuf =(char*) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY,MAX_PACKET); //堆分配接收缓存空间 if (!icmp_data) { //堆分配失败:打印错误原因 cout<<"HeapAlloc() failed: "<<GetLastError()<<endl; return ; } /*构建ICMP报文*/ memset(icmp_data,0,MAX_PACKET); //先进行0 FillICMPData(icmp_data,datasize); //数据填充,以获得一个完整的icmp报文int nCount = 0;icmpcount=0; while(1) { int writeNum; if (nCount++ == PacketNum) //控制输出的记录个数 break; /*计算校验和前要把校验和字段设置为0*/ ((IcmpHeader*)icmp_data)->i_cksum = 0; /*获取操作系统启动到现在所经过的毫秒数,设置时间戳*/ ((IcmpHeader*)icmp_data)->timestamp = GetTickCount(); /*设置序列号*/ ((IcmpHeader*)icmp_data)->i_seq = seq_no++; /*计算校验和*/ ((IcmpHeader*)icmp_data)->i_cksum = CheckSum((USHORT*)icmp_data, datasize); /*基于UDP开始发送ICMP请求*/ //设定目的套接字的地址信息,地址结构体的长度 writeNum = sendto(m_socket, icmp_data, datasize, 0,(struct sockaddr*)&DestAddr, sizeof(DestAddr)); //设置0那个位置的值,将会影响sendto函数调用的行为/*如果发送不成功*/ if (writeNum == SOCKET_ERROR) { if (WSAGetLastError() == WSAETIMEDOUT) //发送超时 { cout<<"timed out"<<endl; smiss--; continue; } //其他发送失败原因 cout<<"sendto() failed: "<<WSAGetLastError()<<endl; return ; } /*基于UDP开始接收ICMP应答 */ fromlen = sizeof(SourceAddr); readNum = recvfrom(m_socket, recvbuf, MAX_PACKET, 0,(struct sockaddr*)&SourceAddr, &fromlen); if (readNum == SOCKET_ERROR) //如果接收失败 { if (WSAGetLastError() == WSAETIMEDOUT) //接收超时 { cout<<"timed out"<<endl; rmiss--;continue; } //其他接收失败原因 cout<<"recvfrom() failed: "<<WSAGetLastError()<<endl; return ; } /*解读接收到的ICMP数据报*/ DecodeICMPHeader(recvbuf, readNum, &SourceAddr); }if(rmiss>0){cout<<endl<<inet_ntoa(SourceAddr.sin_addr)<<"的ping统计信息:"<<endl;cout<<" 数据包:已发送 = "<<smiss<<", 已接收 = "<<rmiss<<", 丢失 = "<<PacketNum-rmiss<<"("<<(PacketNum-rmiss)*100/PacketNum<<"% 丢失)"<<endl;}else{cout<<endl<<inet_ntoa(DestAddr.sin_addr)<<"的ping统计信息:"<<endl;cout<<" 数据包:已发送 = "<<smiss<<", 已接收 = "<<rmiss<<", 丢失 = "<<PacketNum-rmiss<<"("<<(PacketNum-rmiss)*100/PacketNum<<"% 丢失)"<<endl;}}char a[200]; void PingInput(int &argc,char *argv[]){int i=0,j=0;memset(a,'\0',sizeof(a));gets(a);while(a[i]){if(a[i]==' '){while(a[++i]==' '&&a[i]);}if(a[i]!=' '){argv[j++]=&a[i];while(a[++i]!=' '&&a[i]);}a[i++]='\0';}argc=j;return ;}int main() {UserHelp();while(1){int argc;char* argv[20];cout<<">";Init_Ping(); PingInput(argc,argv);GetArgments(argc, argv); if(InputTrue)continue; if(!lpdest){cout<<"没有指定目的地址,请重新输入!!!"<<endl;continue;}PingTest(1000);Sleep(1000); FreeRes(); } return 0;}
程序运行测试:
测试路由功能:
测试路由和ping功能:
补充:部分网络环境可能禁止 记录路由功能,同时,我们的程序是 基于Visual C++ 6.0开发环境的,其他开发环境需要做相应头文件包括的更改;程序的运行 需要提供 "管理员权限" ;否则会报 1013 的错误。
//写的不好的话,还请大家多给点建议。
1 0
- Socket编程之ping程序的实现
- 【网络编程】之十三、ping程序实现
- 【网络编程】之十三、ping程序实现
- 网络程序之ping指令的实现
- 转载:Linux编程之PING的实现
- Linux编程之PING的实现
- ping程序的实现
- ping程序的实现
- 使用原始套接字编程实现简单的ping程序
- 网络编程- ping程序简单实现
- ping程序的C#实现
- 实现ping的C程序
- PC 和ARM9和虚拟机linux的互ping通,以及socket编程实现
- Socket API实现PING
- socket实现ping功能
- socket实现Ping命令
- 【Socket编程】Python用udp实现简易ping
- 扯谈网络编程之自己实现ping
- sql按某字段过滤重复,且只保留某字段排序后最大或最小的记录
- VC++读取注册表失败之64位惹得祸
- Oracle权限、用户、和角色管理
- 18. 4Sum
- 抽象方法
- Socket编程之ping程序的实现
- Javascript异步编程的4种方法
- 反射
- NYOJ37最大回文字符串(LCS)
- 面试技巧
- SSRS:Reporting Services报表制作技巧
- 管理数据库和表
- Echarts示例总结
- MP4文件格式的解析,以及MP4文件的分割算法