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
原创粉丝点击