Socket编程之ping程序的实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/huangqiang1363/article/details/50471146

大家都知道,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 的错误。

//写的不好的话,还请大家多给点建议。

猜你喜欢

转载自blog.csdn.net/huangqiang1363/article/details/50471146
今日推荐