大家都知道,ping程序是基于ICMP回显请求和应答报文的,通过IP数据报的选项字段可以达到记录路由的效果,老实说,在刚刚拿到这个课题时,毫无头绪,根本不知道如何下手,因为之前根本就没有从事过Socket网络编程,但是这又是必须的,没办法,凭借之前对孙鑫老师MFC视频教程里的网络编程的基础知识,以及计算机网络、TCP/IP详解等课本的知识,最终结合网上的参考代码,加上自己的理解和调试,解决众多BUG问题,可以说是对网上的代码进行了一定层次的优化。下面附上代码,希望对大家有所帮助,也希望能够和我交流思想。
头文件部分:
/*导入库文件*/
#pragma comment( lib, "ws2_32.lib" )
/*加载头文件*/
#include //创建套接字头文件
#include
#include
#include
#include
#include
#include
#include
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: "< [data size]"<调用用户使用帮助
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 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<<"("<h_name;//inet_ntoa通过接收in_addr结构体返回点分十进制的ip地址 host结构体存储了主机的信息
else //获取失败
cout<<"("<";
cout<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 "<sin_addr)<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!"<sin_addr)<<":"; //字节数和ip地址
cout<<" icmp_seq = "<i_seq<<". "; //序号
cout<<" time: "<timestamp<<" ms"<h_addr, hp->h_length);
/*将获取到的地址族值赋给目的地地址中的相应字段*/
DestAddr.sin_family = hp->h_addrtype;
cout<<"DestAddr.sin_addr = "<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"<0)
{
cout<";
Init_Ping();
PingInput(argc,argv);
GetArgments(argc, argv);
if(InputTrue)
continue;
if(!lpdest)
{
cout<<"没有指定目的地址,请重新输入!!!"<
程序运行测试:
测试路由功能:
测试路由和ping功能:
补充:部分网络环境可能禁止 记录路由功能,同时,我们的程序是 基于Visual C++ 6.0开发环境的,其他开发环境需要做相应头文件包括的更改;程序的运行 需要提供 "管理员权限" ;否则会报 1013 的错误。
//写的不好的话,还请大家多给点建议。