01Ping程序的设计
1.Ping程序设计具体设计任务
1.1 实验目的
PING程序是我们使用的比较多的用于测试网络连通性的程序。PING程序基于ICMP,使用ICMP的回送请求和回送应答来工作。由计算机网络课程知道,ICMP是基于IP的一个协议,ICMP包通过IP的封装之后传递。
课程设计中选取PING程序的设计,其目的是希望同学们通过PING程序的设计,能初步掌握TCP/IP网络协议的基本实现方法,对网络的实现机制有进一步的认识。
1.2 实验内容和要求
1.2.1 RAW模式的SOCKET编程
PING程序是面向用户的应用程序,该程序使用ICMP的封装机制,通过IP协议来工作。为了实现直接对IP和ICMP包进行操作,实验中使用RAW模式的SOCKET编程。
熟悉SOCKET的编程,包括基本的系统调用如SOCKET、BIND等;
1.2.2 具体内容
1) 定义数据结构
需要定义好IP数据报、ICMP包等相关的数据结构;
2) 程序实现
在WINDOWS环境下实现PING程序;
3) 程序要求
在命令提示符下输入:
PING ΧΧΧ.ΧΧΧ.ΧΧΧ.ΧΧΧ
其中ΧΧΧ为目的主机的IP地址,不要求支持域名,对是否带有开关变量也不做要求。不带开关变量时,要求返回4次响应。
返回信息的格式:
REPLY FROM ΧΧΧ.ΧΧΧ.ΧΧΧ.ΧΧΧ
或
REQUEST TimeOut (无法PING通的情况)。
2.程序设计方案
2.1 原理流程框图
2.2 设计主要函数分析
本程序的步骤包括创建套接字、设置接收和发送超时值、分配内存、创建ICMP报文、发送ICMP请求报文、接收ICMP应答报文和解读ICMP报文等。当中调用多个函数从而实现主机间相互Ping通。
(1)int socketInit(char *par_host)初始化操作
通过此函数建立WSADATA结构,并存放windows socket初始化信息。并通过调用函数实现对socket的连接和启用,并记录本地及目的地数据。
(2)ICMP检查和封装算法
ICMP报文格式如下:
8位类型 |
8位代码 |
16位校验和 |
(不同类型和代码有不同的内容) |
通过调用unsigned short cal_chksum(unsigned short *addr, int len) 此函数,实现对ICMP包的检查并返回有效字符对ICMP包调用int pack(int pack_no)实行封装。
(3)IP数据包解析函数
IP数据报格式如下:
4位版本 |
4位首部长度 |
8位区分服务 |
16位总长度 |
|
16位标识 |
3位标志 |
13位片偏移 |
||
8位生存时间 |
8位协议 |
16位首部检验和 |
||
32位源地址 |
||||
32位目的地址 |
||||
选项(如果有) |
||||
数据部分 |
通过int unpack(char *buf, int len) 函数对IP数据包有效字节数据进行存储,实现对数据基于ICMP包上再进行封装,并实现数据字节调用输出。
(4)main()函数
主程序中通过调用各函数,实现对socket接口的初始化和对主机间进行连通性测试。并通过函数实现对输入地址中是否包含“-t”标示从而是否无限ping对方。
2.3 程序源代码
#include<Winsock2.h> //WINSOCK API的头文件 #include<process.h> #include<iostream> #include<string> #include<math.h> using namespace std; #define SEND_SIZE 32 //定义包的大小 #define PACKET_SIZE 4096 #define ICMP_ECHO 8 #define ICMP_ECHOREPLY 0 //静态加入一个lib文件 #pragma comment(lib,"Ws2_32.lib") struct icmp { unsigned char icmp_type; //类型 unsigned char icmp_code; //编码 unsigned short icmp_chksum; //校验和 unsigned short icmp_id; //标示符 unsigned short icmp_seq; //顺序号 unsigned long icmp_data; //数据 }; struct ip { unsigned char ip_hl : 4; //报头长度 unsigned char ip_v : 4; //版本号 unsigned char ip_tos; //服务类型 unsigned short ip_len; //总长度 unsigned short ip_id; //标识 unsigned short ip_off; //标志 unsigned char ip_ttl; //生存时间 unsigned char ip_p; //协议号 unsigned short ip_sum; //报头校验和 unsigned long ip_src; //源IP地址 unsigned long ip_dst; //目的IP地址 }; //发送包 char sendpacket[PACKET_SIZE]; //接受包 char recvpacket[PACKET_SIZE]; //网络地址 struct sockaddr_in dest_addr; struct sockaddr_in from_addr; int sockfd; //Socket状态变量 int pid; //程序标志位,取得进程识别码,在process.h下 int socketInit(char *par_host); unsigned short cal_chksum(unsigned short *addr, int len); int pack(int pack_no); int unpack(unsigned char *buf, int len); void sendPacket(void); void recvPacket(void); int main() { int i = 0; char *par_host; char m_Input[100]; while(1) { cout << "ping "; cin.getline(m_Input,40); //输入一个可含空格的字符数组 //如果是输入了-t if (m_Input[0] == '-'&&m_Input[1] == 't' && m_Input[2] == ' ') { char *a = m_Input; par_host = a + 3; } else par_host = m_Input; socketInit(par_host); //初始化socket以及检查socket有效性 pid = _getpid(); //程序随机数标志 //如果为'-t '则循环4次,否则无限循环 if(m_Input[0] == '-'&&m_Input[1] == 't' &&m_Input[2] == ' ') { while(1) { sendPacket(); recvPacket(); Sleep(1000); } } else { for(i=0;i < 4;i++) { sendPacket(); recvPacket(); Sleep(1000); } } } system("pause"); return 0; } int socketInit(char *par_host) { struct hostent *host; struct protoent *protocol; int timeout = 1000; //设置发送超时1000ms WORD wVersionRequested; //定义类型,word类型:16位短整数 WSADATA wsaData; //建立WSADATA结构,存放windows socket初始化信息 int err; //用来接收函数的返回值,即启动socket时是否出现错误 wVersionRequested = MAKEWORD(2, 2); //宏,高字节为2,低字节为2。514 err = WSAStartup(wVersionRequested, &wsaData); //启动Socket,WSAStartup函数是连接应用程序与winsock.dll的第一个调用。第一个参数是WINSOCK版本号,第二个参数指向WSADATA指针,该函数返回一个int值 //wsaData用来存储系统传回的关于WINSOCK的资料 if (err){ exit(1); }; if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) //如果16进制最低那个字节内容不等于2或最高那个字节不等于2 //字段wVersion:Windows Sockets DLL期望调用者使用的Windows Sockets规范的版本,为WORD类型 { WSACleanup(); //终止Winsock 2 DLL(Ws2_32.dll)使用 return 0; } if ((protocol = getprotobyname("icmp")) == NULL) //返回对应于给定协议名的包含名字和协议号 { cout << "getprotobyname error" << endl; exit(1); } //生成使用ICMP的原始套接字,这种套接字只有root才能生成 if ((sockfd = socket(AF_INET, SOCK_RAW, protocol->p_proto))<0) //初始化socket套接口,异常时返回-1 { cout << "socket error" << endl; exit(1); } //回收root权限,设置当前用户权限 if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout))<0) //设置套接口的选项,设置接收超时时间 cout << "failed to set recv timeout: " << endl << WSAGetLastError(); //输出并返回上次网络错误 if (setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, (char*)&timeout, sizeof(timeout))<0) //设置套接口的选项,设置发送超时时间 cout << "failed to set send timeout: " << endl << WSAGetLastError(); //输出并返回上次网络错误 memset(&dest_addr, 0, sizeof(dest_addr)); //设置目标主机初始化,即清零 dest_addr.sin_family = AF_INET; //设置地址族 if (host = gethostbyname(par_host)) // 返回对应于给定主机名的主机信息,par_host是ping的目的主机的Ip { //取指针地址所指的变量,放入函数 memcpy中计算得到结果 memcpy((char *)&dest_addr.sin_addr, host->h_addr, host->h_length); //将获取到的IP值赋给目的地址中相应字段 if (host = gethostbyaddr(host->h_addr, 4, PF_INET)) par_host = host->h_name; //将ip解析为主机名 } else if (dest_addr.sin_addr.s_addr = inet_addr(par_host) == INADDR_NONE) //查找目标IP地址失败 { cout << "Unkown host " << endl << par_host; exit(1); } } unsigned short cal_chksum(unsigned short *addr, int len) //ICMP检查算法 { int nleft = len; int sum = 0; unsigned short *w = addr; unsigned short answer = 0; /*把ICMP报头二进制数据以2字节为单位累加起来*/ while (nleft>1) { sum += *w++; nleft -= 2; } if (nleft == 1) //处理ICMP报头为奇数个字节时累加最后一个 { /*若ICMP报头为奇数个字节,会剩下最后一字节。把最后一个字节视为一个2字节数据的高字节,这个2字节数据的低字节为0,继续累加*/ *(unsigned char *)(&answer) = *(unsigned char *)w; sum += answer; } /*校验和是以16位为单位进行求和计算的,sum是32位的,sum&0xffff是低16位,sum>>16是右移16位,取到的是高16位,相加就是高16位和低16位的和。*/ sum = (sum >> 16) + (sum & 0xffff); /*这一步是有可能上面加的时候有进位到高16位的,再把高16位加进来。*/ sum += (sum >> 16); /*上一步肯定不会再有进位了,即使上面sum高16位非0也不要紧,此处sum只能把低字节数赋值给answer,因为answer为16位*/ answer = ~sum; return answer; } int pack(int pack_no) //封装ICMP包 { int packsize; struct icmp *icmp; packsize = 8 + SEND_SIZE; //数据报大小为64字节 icmp = (struct icmp*)sendpacket; icmp->icmp_type = ICMP_ECHO; icmp->icmp_code = 0; icmp->icmp_chksum = 0; icmp->icmp_seq = pack_no; //发送的数据报编号 icmp->icmp_id = pid; icmp->icmp_data = GetTickCount(); //记录发送时间 icmp->icmp_chksum = cal_chksum((unsigned short *)icmp, packsize); //校验算法 return packsize; } int unpack(char *buf, int len) //解析IP包 { struct ip *ip; struct icmp *icmp; double rtt; int iphdrlen; ip = (struct ip *)buf; iphdrlen = ip->ip_hl * 4; /*求ip报头长度,即ip报头的长度标志乘4*/ icmp = (struct icmp *)(buf + iphdrlen); /*越过ip报头,指向ICMP报头*/ /*确保所接收的是我所发的的ICMP的回应*/ if ((icmp->icmp_type == ICMP_ECHOREPLY) && (icmp->icmp_id == pid)) { len = len - iphdrlen - 8; rtt = GetTickCount() - icmp->icmp_data; cout << "Reply from " << inet_ntoa(from_addr.sin_addr) << ": bytes=" << len << " time=" << rtt << "ms TTL= " << fabs((double)ip->ip_ttl) << endl; return 1; } return 0; } void sendPacket() //发送ICMP包 { int packetsize; //设置icmp报头 static int pack_no = 0; packetsize = pack(pack_no++); //发送数据报 if (sendto(sockfd, sendpacket, packetsize, 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr))<0) cout << "Destination host unreachable." << endl; } void recvPacket() //接收IP包 { int n, fromlen; int success; fromlen = sizeof(from_addr); do { if ((n = recvfrom(sockfd, recvpacket, sizeof(recvpacket), 0, (struct sockaddr *)&from_addr, &fromlen)) >= 0) success = unpack(recvpacket, n); //剥去ICMP报头 else if (WSAGetLastError() == WSAETIMEDOUT) { cout << "Request timed out." << endl; return; } } while (!success); }
3.实验结果
不带参数连接时,如图所示:
带参数-t时,如图所示:
4.思考题
1.本题目只要求实现PING的一些简单功能,在Windows命令行模式下,输入“Ping”回车,查看PING的所有功能,考虑如何实现这些功能。
首先Ping命令会构建一个固定格式的ICMP请求数据包,然后由ICMP协议将这个数据包连同地址“10.10.165.140”一起交给IP层协议并以此作为目的IP地址,本机IP地址作为源地址,再加上其他数据信息,构建一个IP数据包。通过在映射表中查找出IP地址10.10.165.140所对应的物理地址,一并交给数据链路层。数据链路层在上一层传递下来的数据包封装构建一个数据帧,并加上必要控制信息传递到物理层,再根据相应协议将它们传送出去。基于此去设计实现ping各种功能。
2.如果一台主机能ping通自己但网络不通,可能是什么原因?
(1)Windows服务器的网络服务功能还没启动
(2)计算机的TCP/IP协议没有与网卡有效的绑定;
(3)可能计算机的网卡安装不正确,也可能是没连通
3.考虑Netstat、Traceroute、ipconfig等网络测试应用程序的工作原理以及使用。
Netstat的工作原理及使用:
(1)显示本地或与之相连的远程机器的连接状态,包括TCP、IP、UDP、ICMP协议的使用情况,了解本地机开放的端口情况.
(2)检查网络接口是否已正确安装,如果在用netstat这个命令后仍不能显示某些网络接口的信息,则说明这个网络接口没有正确连接,需要重新查找原因。
(3)通过加入“-r”参数查询与本机相连的路由器地址分配情况。
(4)检查一些常见的木马等黑客程序,因为任何黑客程序都需要通过打开一个端口来达到与其服务器进行通信的目的。
Traceroute程序的设计是利用ICMP及IP header的TTL。首先,traceroute送出一个TTL是1的IP 数据报到目的地,当路径上的第一个路由器收到这个数据报时,它将TTL减1。此时,TTL变为0了,所以该路由器会将此数据报丢掉,并送回一个「ICMP time exceeded」消息,traceroute 收到这个消息后,便知道这个路由器存在于这个路径上,接着traceroute 再送出另一个TTL是2 的datagram,发现第2 个路由器...... traceroute 每次将送出的数据报的TTL 加1来发现另一个路由器,这个重复的动作一直持续到某个数据报抵达目的地。当数据报到达目的地后,该主机并不会送回ICMP time exceeded消息,因为它已是目的地了。
Ipconfig的工作原理及使用:
(1)查找目标主机的IP地址及其它有关TCP/IP协议的信息。
(2)当用户的网络中设置的是DHCP(动态IP地址配置协议)时,利用Ipconfig可以让用户很方便地了解到所用IPconfig机的IP地址的实际配置情况。它可侦查到本机上所有网络适配的IP地址分配情况,运行”Ipconfig”命令后,窗口中显示了主机名、DNS服务器、节点类型以及主机的相关信息如网卡类型、MAC地址、IP地址、子网掩码以及默认网关等。配置不正确的IP地址或子网掩码是接口配置的常见故障。
其中配置不正确的IP地址有两种情情况:
(1)网号部分不正确。
(2)主机部分不正确。