ping程序的设计与实现
滁州学院
课程设计报告
课程名称: 计算机网络课程设计
设计题目: ping程序的设计与实现 系 别: 计算机与信息工程学院 专 业: 计算科学与技术 组 别: 第五小组 起止日期: 2011年12月1日~2011年12月8日 指导教师:
计算机科学与技术系二○一一年制
课程设计题目 组长 Ping程序的设计与实现 学号 专业 2011220125 班级 计专(2)班 系别 计算机与信息工程学院 组员 指导教师 计算机科学与技术 课程设计目的 通过设计Ping程序,理解Ping程序的实现原理,并初步讲解了c语言网络编程技术。本章涉及很多网络编程函数和编程技巧,包括库文件的导入;winsock的初始化、注销;socket的创建、关闭;设置socket选项;根据主机名获取IP地址; 从堆中分配一定数量的空间、释放从堆中分配的空间;数据报的发送;数据报的接等。 课程设计所需环境 课程设计任务要求 WindowsXP+Visual C++6.0 实现ping的基本功能,实现ping -t 课程设计工作进度计划 序号 起止日期 01 02 03 指导教师签字: 年 月 日 工 作 内 容 分工情况 展开思路讨论工作并搜2011-12-1~2011-12-2 集相关资料 具体制作,编写相关代2011-12-3~2011-12-6 码,制作相关窗口并实 现,美化界面 编写并完成课程设计报2011-12-7~2011-12-8 告 。
教研室审核意见: 教研室主任签字: 年 月 日 课程设计任务书
一.Ping 程序运行原理
在网络层, 除了IP协议之外, 还有一些控制协议, 如ICMP, ARP, DHCP等。
1. ping的基础知识
原始套接字
原始套接字是允许访问底层传输协议的一种套接字类型。使用原始套接字操作IP数据报, 可以进行路由跟踪, Ping等。另外, 使用原始套接字需要知道许多下层协议结构的知识,所以下面讨论ICMP,IP, UDP, TCP格式。
原始套接字有两种类型, 第一种类型是在IP头种使用预定义的协议, 如ICMP;第二种类型是在IP头种使用自定义的协议。下面使用创建原始套接字的方法。
创建套接字的函数是socket()或者WSASocket(),只不过要将套接字类型指定为SOCK_RAW,代码如下:
SOCKET sraw = ::socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
创建原始套接字时socket函数的第三个参数protocol值将成为IP头中得协议域的值。IPPROTO_ICMP指定要使用ICMP。
原始套解释提供管理下层传输的能力。他们可能会被恶意利用, 因此, 仅Administrator组的成员能够创建SOCK_RAW类型的套接字。 任何人在Windows NT下都可以创建原始套接字,但是没有Administrator权限的人不能用它来做任何事情, 因为bind函数将会失败, 出错码WSAEACCES..
在上面的套接字创建代码种,我们使用ICMP,也可以使用IIGMP, UDP, IP或者原始IP,对应的宏定义分别是IPPROTO_IGMP, IPROTO_UDP, IPPROTO_IP或者IPPROTO_RAW。 其中协议标志IPPROTO_UDP, IPPROTO_IP, 和IPPROTO_RAW 需要启动IP_HDRINCL选项。
使用恰当的协议标志创建原始套接字之后,便可以在发送和接受调用种使用此套接字句柄了。无论IP_HDRINCL选项是否设置, 在原始套接字上接收到的数据种都会将包含IP头。
2. ICMP协议与校验和的计算
互联网上得操作由路由器紧紧地监控着。当有异常饭送时候,具体事件通过ICMP报道,如目的不可到达,TTL超时等。这个协议也用来测试互联网。
每个ICMP消息都封装在IP封包中, 所以使用IP寻址,
ICMP消息的格式如下:首8位表示ICMP的类型,通常可以分为请求消息和错误报告消息两类。接下来的八位表示ICMP代码,这个域进一步定义了请求或者是消息的类型。接下来八位表示icmp的校验和。它提供了ICMP头和他的实际数据。 3.校验和的计算
发送ICMP报文时, 必须由程序自己计算校验和,并将它填入ICMP头部的对应域中。校验和的计算方法是:将数据以资为单位累加到一个双字中,如果数据长度为奇数,最后一个字节将被扩展到字, 累加的结果是一个双字,最后将这个双字的高16bit和低16bit相加后取反,便得到了校验和。
u_short checksum(u_short *buffer, int len) {
register int nleft = len;
register u_short *w = buffer; register u_short answer; register int sum = 0;
//使用32位累加器,进行16位的反馈计算 while ( nleft > 1 ) {
sum += *w++; nleft -= 2; }
//补全奇数位 if ( nleft == 1 ) {
u_short u = 0;
*(u_char *)(&u) = *(u_char*)w; sum += u; }
//将反馈的16位从高位移到低位 sum = (sum >> 16) + (sum & 0xffff); sum += (sum >> 16); answer = ~sum; return (answer); }
3.Ping程序设计思路:
要实现ping程序,需要实现以下步骤:
(1) 创建协议类型为IPPROTO_ICMP的原始套接字,设置套接字属性。 (2) 创建并初始化ICMP封包。
(3) 调用sendto函数向远程主机发送ICMP请求。 (4) 调用recfrom函数接受ICMP响应。 初始化ICMP头时先初始化消息类型和代码域, 之后是回显请求头。程序首先定义了ICMP头的数据结构IMCP_HDR.。ICMP_HDR的定义如下: typedef struct _ICMPHeader
{
u_char Type; //类型 u_char Code; //代码
u_short Checksum; //首部校验和 u_short ID; //标识 u_short Seq; //序列号 char Data; //数据
}ICMPHDR, *PICMPHDR;
4.编程时,需要用到一些windows函数,说明如下:
(1).int WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData); 函数说明:
为了在应用程序当中调用任何一个Winsock API函数,首先第一件事情就是必须通过WSAStartup函数完成对Winsock服务的初始化,因此需要调用WSAStartup函数。使用Socket的程序在使用Socket之前必须调用WSAStartup函数。
该函数的第一个参数指明程序请求使用的Socket版本,其中高位字节指明副版本、低位字节指明主版本;
操作系统利用第二个参数返回请求的Socket的版本信息。当一个应用程序调用WSAStartup函数时,
操作系统根据请求的Socket版本来搜索相应的Socket库,然后绑定找到的Socket库到该应用程序中。
以后应用程序就可以调用所请求的Socket库中的其它Socket函数了。 (2).SOCKET socket( int af, int type, int protocol ); 函数说明:
应用程序调用socket函数来创建一个能够进行网络通信的套接字。
第一个参数指定应用程序使用的通信协议的协议族,对于TCP/IP协议族,该参数置AF_INET;
第二个参数指定要创建的套接字类型,流套接字类型为SOCK_STREAM、数据报套接字类型为SOCK_DGRAM、
原始套接字SOCK_RAW(WinSock接口并不适用某种特定的协议去封装它,而是由程序自行处理数据包以 及协议首部);
第三个参数指定应用程序所使用的通信协议。
(3).int sendto( SOCKET s, const char FAR *buf, int len, int flags, const struct sockaddr FAR *to, int tolen); 函数说明:
返回值:实际发送数据的长度。 parameter : s 套接字 buff 待发送数据的缓冲区 size 缓冲区长度
Flags 调用方式标志位, 一般为0, 改变Flags,将会改变Sendto发送的形式
addr (可选)指针,指向目的套接字的地址
len addr所指地址的长度
(4)int recvfrom(SOCKET s, char FAR* buf, int len, int flags,
struct sockaddr FAR *from, int FAR *fromlen );
函数说明:recvfrom( )用来接收远程主机经指定的socket传来的数据,并把数据
传到由参数buf指向的内存空间,参数len为可接收数据的最大长度.参数flags一般设0,其他数值定义参考recv().参数from用来指定欲传送的网络地址,结构sockaddr请参考bind()函数.参数fromlen为sockaddr的结构长度.
二.程序的流程图和源码
Ping程序的设计与实现大致可分为四个模块(见图1-1),分别是:初始化模块、功能控制模块、ping模块、mian测试模块。
Ping程序的设计与实现 初始化模块 功能控制模块 Ping模块 main测试模块
图1-1
1.初始化模块:该模块用于定义及初始化各个全局变量,为winsock加载winsock体。
(见图1-2)主要包括定义IP首部格式、定义ICMP首部格式、定义ICMP回应请求、定义ICMP回应答复。
初始化模块 定义IP首部格式 定义ICMP首部格式 图1-2
定义ICMP回应请求 定义ICMP回应答复
2.功能控制模块:该模块是被其他模块调用,其功能包括计算校验和、发送回应请求
函数、接收应答回复并进行解析、等待回应答复(主是要select模型)。(见图1-3,1-4,1-5,1-6) 计算校验和函数源码:
Checksum()开始 定义初始化一些变量 Nleft>1 是(使用32位累加器,进行16位的反馈计算) sum += *w++; nleft -= 2; Nleft=1 是(补全奇数位) sum += u; 否 (将反馈的16位从高位移到低位) sum = (sum >> 16) + (sum & 0xffff); sum += (sum >> 16); 返回answer 结束 图1-3
SendEchoRequest()开始 定义初始化一些变量 填充要发送的数据 存储发送的时间 计算回应请求的校验和 发送回应请求 是 返回nRet 调用发送错误函数 结束 图1-4
RecvEchoReply ()开始 WaitForEchoReply ()开始 定义初始化一些变量 定义初始化一些变量 接收应答回复 返回timeout 检验接收结果 结束 图1-6
是 返回应答时间
调用发送错误函数 结束 图1-5
3.Ping模块功能模块:该模块是本程序的核心模块,调用其他模块实现其功
能,进而实现Ping的功能。
开始 定义初始化各个局部变量 s
判断WSAGetLastError()是否调用成是 检测目标主机是否为NULL 否 设置目标主机的IP地址,开始ping 发起四次ping测试 发送ICMP回应请求 等待回复的数据 清除残余 接收回复 结束 否 计算花费时间 Loop是否为0 是 输出ping结果 输出平均次数
4.main()函数模块:向指定的域名或IP地址发送Echo 请求报文;根据响应报
文显示出Ping的结果;程序仅支持-t选项即可。
开始 WSAStartup() 是否成功 加载失败 否 截取后三位字符 为了实现-t 调用ping() 释放资源 结束
图1-8
三.运行操作及结果
在VC中运行程序后会出现如图4.1所示,提示你输入IP或网址;
2、我们先输入校园网机房主机命令,看能否ping通
3.试着使用ping –t命令,如下:
4、再输入外部网主机命令,看能否ping通,
上图为网络ping不通的情形。
源代码
源代码如下: #include #pragma comment(lib, \"ws2_32.lib\")//导入库文件 #define ICMP_ECHOREPLY 0 //ICMP回应答复 #define ICMP_ECHOREQ 8 //ICMP回应请求 #define REQ_DATASIZE 32 //请求数据报大小 #include //定义IP首部格式 typedef struct _IPHeader { u_char VIHL; //版本和首部长度 u_char ToS; //服务类型 u_short TotalLen; //总长度 u_short ID; //标识号 u_short Frag_Flags; //片偏移量 u_char TTL; //生存时间 u_char Protocol; //协议 u_short Checksum; //首部校验和 struct in_addr SrcIP; //源IP地址 struct in_addr DestIP; //目的地址 }IPHDR, *PIPHDR; //定义ICMP首部格式 typedef struct _ICMPHeader { u_char Type; //类型 u_char Code; //代码 u_short Checksum; //首部校验和 u_short ID; //标识 u_short Seq; //序列号 char Data; //数据 }ICMPHDR, *PICMPHDR; //定义ICMP回应请求 typedef struct _ECHOREQUEST { ICMPHDR icmpHdr; DWORD dwTime; char cData[REQ_DATASIZE]; }ECHOREQUEST, *PECHOREQUEST; //定义ICMP回应答复 typedef struct _ECHOREPLY { IPHDR ipHdr; ECHOREQUEST echoRequest; char cFiller[256]; }ECHOREPLY, *PECHOREPLY; /**********************************************************************************/ //计算校验和 u_short checksum(u_short *buffer, int len) { register int nleft = len; register u_short *w = buffer; register u_short answer; register int sum = 0; //使用32位累加器,进行16位的反馈计算 while ( nleft > 1 ) { sum += *w++; nleft -= 2; } //补全奇数位 if ( nleft == 1 ) { u_short u = 0; *(u_char *)(&u) = *(u_char*)w; sum += u; } //将反馈的16位从高位移到低位 sum = (sum >> 16) + (sum & 0xffff); sum += (sum >> 16); answer = ~sum; return (answer); } /**********************************************************************************************/ //发送回应请求函数 int SendEchoRequest(SOCKET s, struct sockaddr_in *lpstToAddr) { static ECHOREQUEST echoReq; static nId = 1; static nSeq = 1; int nRet; //填充回应请求消息 echoReq.icmpHdr.Type = ICMP_ECHOREQ; echoReq.icmpHdr.Code = 0; echoReq.icmpHdr.Checksum = 0; echoReq.icmpHdr.ID = nId++; echoReq.icmpHdr.Seq = nSeq++; //填充要发送的数据 for (nRet = 0; nRet < REQ_DATASIZE; nRet++) { echoReq.cData[nRet] = '1' + nRet; } //存储发送的时间 echoReq.dwTime = GetTickCount(); //计算回应请求的校验和 echoReq.icmpHdr.Checksum = checksum((u_short*)&echoReq, sizeof(ECHOREQUEST)); //发送回应请求 nRet = sendto(s,(LPSTR)&echoReq,sizeof(ECHOREQUEST), 0,(struct sockaddr*)lpstToAddr,sizeof(SOCKADDR_IN)); if (nRet == SOCKET_ERROR) { printf(\"send to() error:%d\\n\ } return (nRet); } /*******************************************************************************/ //接收应答回复并进行解析 DWORD RecvEchoReply(SOCKET s, LPSOCKADDR_IN lpsaFrom, u_char *pTTL) { ECHOREPLY echoReply; int nRet; int nAddrLen = sizeof(struct sockaddr_in); //接收应答回复 nRet = recvfrom(s,(LPSTR)&echoReply,sizeof(ECHOREPLY), 0,(LPSOCKADDR)lpsaFrom,&nAddrLen); //检验接收结果 if (nRet == SOCKET_ERROR) { printf(\"recvfrom() error:%d\\n\ } //记录返回的TTL *pTTL = echoReply.ipHdr.TTL; //返回应答时间 return(echoReply.echoRequest.dwTime); } /********************************************************************************/ //等待回应答复,使用select模型 int WaitForEchoReply(SOCKET s) { struct timeval timeout; fd_set readfds; readfds.fd_count = 1; readfds.fd_array[0] = s; timeout.tv_sec = 1; timeout.tv_usec = 0; return(select(1, &readfds, NULL, NULL, &timeout)); } /******************************************************************************/ //PING功能实现 void Ping(char *pstrHost,bool logic) { char c; SOCKET rawSocket; LPHOSTENT lpHost; struct sockaddr_in destIP; struct sockaddr_in srcIP; DWORD dwTimeSent; DWORD dwElapsed; u_char cTTL; int nLoop,k=4; int nRet,minimum=100000,maximum=0,average=0; int sent=4,reveived=0,lost=0; //创建原始套接字,ICMP类型 rawSocket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);//第二个注释函数socket if (rawSocket == SOCKET_ERROR) { printf(\"socket() error:%d\\n\ return; } //检测目标主机 lpHost = gethostbyname(pstrHost); if (lpHost==NULL) { printf(\"Host not found:%s\\n\ return; } //设置目标机地址 destIP.sin_addr.s_addr = *((u_long FAR*)(lpHost->h_addr)); //设置目标IP destIP.sin_family = AF_INET; //地址规格 destIP.sin_port = 0; //提示开始进行PING printf(\"\\nPinging %s [%s] with %d bytes of data:\\n\TASIZE); //发起多次PING测试 for (nLoop=0; nLoop Average printf(\"closesocket() error:%d\\n\ } } //主程序 void main() { printf(\"Welcome to the Ping Test\\n\"); while(1) { WSADATA wsd;//检测输入的参数 //初始化Winsock if (WSAStartup(MAKEWORD(1, 1), &wsd) != 0){//第一个函数说明 WSAStartup() printf(\"加载 Winsock 失败!\\n\"); } char opt1[100]; char *ptr=opt1; bool log=false; printf(\"Ping \"); cin.getline(opt1,100,'\\n');//ping的地址 字符串 if(strstr(opt1, \"-t\")!=NULL) { log=true; strncpy(ptr,opt1+0,strlen(opt1)-3);//把原字符串的最后三位截取 ptr[strlen(opt1)-2]=0; //printf(\"%s\ } //开始PING Ping(ptr,log); //程序释放 Winsock 资源 WSACleanup(); } } /*函数说明 为了在应用程序当中调用任何一个Winsock API函数, 首先第一件事情就是必须通过WSAStartup函数完成对Winsock服务的初始化, 因此需要调用WSAStartup函数。使用Socket的程序在使用Socket之前必须调用WSAStartup函数。 该函数的第一个参数指明程序请求使用的Socket版本,其中高位字节指明副版本、低位字节指明主版本; 操作系统利用第二个参数返回请求的Socket的版本信息。当一个应用程序调用WSAStartup函数时, 操作系统根据请求的Socket版本来搜索相应的Socket库,然后绑定找到的Socket库到该应用程序中。 以后应用程序就可以调用所请求的Socket库中的其它Socket函数了。 */ /*函数说明 SOCKET socket( int af, int type, int protocol ); 应用程序调用socket函数来创建一个能够进行网络通信的套接字。 第一个参数指定应用程序使用的通信协议的协议族,对于TCP/IP协议族,该参数置AF_INET; 第二个参数指定要创建的套接字类型,流套接字类型为SOCK_STREAM、数据报套接字类型为SOCK_DGRAM、 原始套接字SOCK_RAW(WinSock接口并不适用某种特定的协议去封装它,而是由程序自行处理数据包以 及协议首部); 第三个参数指定应用程序所使用的通信协议。 心得体会 通过仔细阅读程序代码,查找相关资料,我大概弄懂了程序的基本过程。程序通过发送一个ICMP回显请求报文到目的地主机,如果有IP选项途中的主机通过报文记录各自的IP地址,目的地主机回发一个回显应答报文,然后发送主机通过解析回显应答报文,查看通过路由地址,并计算发送回收报文所用的传送时间,以确定回显报文是否超时。 程序中很多算法值得我们借鉴,例如检验和算法先把两数通过取反相加,然后再次取反以防止上次取反数组溢出而出现错误。我们从中要学会代码的重复使用,程序中usage(char *progname)反复使用来输出信息,而不用每次输出相关的信息的时候都需要重新写实现代码,这样既节省程序编译运行所需的时间和内存的开销又易于以后程序的升级和添加其他功能,符合编程的发展趋势。 在发现程序良好的算法和优良的编程思想的同时我也发现很多是我没见过的函数例如WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)、socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)、ZeroMemory(&ipopt, sizeof(ipopt))。 在以前的学习中没有接触到这类的函数,通过上网查找才知道是操作系统的类库函数和一些套接字函数。而且是一些基本的常见的函数,这使我发现自己知识的匮乏,在以后的学习过程中得要好好的努力,多阅读一些复杂的程序,了解一个基本的函数、算法和精良的编程思想,更要多动手写写一些有一定难度的程序,我们不应该怕写程序出错,应该大胆的写出自己的想法,出现错误去解决错误发现自己的错误就能找出自己知识的漏洞和模糊点。我们还可以通过阅读别人错误的程序,试着帮别人查找错误,这样既能夯实头脑中语言的规则还能发现一些初学者易犯的错误,使自己能少走一些初学者容易走的误区。 通过整个于都程度和编写文档过程,感觉自己的能力远远没有达到老师要求的层面上,仔细反省了下,觉得主要有一下几点没做好。一:开始学一门语言时没有真正的把学习当做一个重要的事情,疏忽了基础知识的学习根基薄弱。二:阅读的程序太少或者说看的程序方面的书籍太少,即使是阅读了部分的代码也都是些简单的程序代码,遇到复杂的代码就没有继续读下去,没有去好好分析程序程序的流程和内存分析。三:写的代码太少,写的太简单没有用到复杂的函数,所用的算法过于简单没有深度。四:写程序出错后不愿意去查出错误没有正确对待错误是从哪里出现的,更没有修改正确运行处正确结果来。 最后觉得通过这次的作业收获不少,发现了自己不足的地方。找到了一部分自己应该努力的地方,通过横向比较发现自己和别人有一定差距,有一些是通过学习就能补上的,有一些需要一定的实际联系才能追的上别人。同时通过这次作业的要求让我认识到一份文档的要 因篇幅问题不能全部显示,请点此查看更多更全内容