Ping(Packet Internet Gopher分组网间网探测器)利用了ICMP(Internet Control Message Protocol互联网控制报文协议)协议的“回响”功能来实现主机/服务器是否有应答的测试。ICMP为路由器和主机提供了正常情况以外的通信,它是IP的一个完整的组成部分。ICMP包括降低传送速率的源站抑制报文、请求主机改变选路表的重定向报文以及主机可用来决定目的站是否可达的回送请求/回答报文。ICMP报文在IP数据报的数据区中传送。当主机/服务器接收到具有回响类型的ICMP报文时,就响应1个“回响应答”报文。本地机器收到该报文并确认之后即可认为该主机/服务器处于活动状态,从而本机与远程主机/服务器之间能够连通,也可以互相通信。 仿照DOS下的ping命令并根据51单片机资源现状,我实现了一个简单的ping功能。它的使用方法如下: (1)单片机-->PC机 在Shell里使用“ping XXX.XXX.XXX.XXX<cr>”,如果连通,显示“Reply from XXX.XXX.XXX.XXX: bytes=32 TTL=XXX”,否则,显示“Request timed out.(XXX.XXX.XXX.XXX)”。 (2)PC机-->单片机 按照DOS里的常规操作即可 每个ping命令重复测试8次,即显示8次信息。 注意到显示内容与PC机上稍有不同,这是由于此处ping工作在多任务单窗口环境下,为了区分响应发送源,有必要增加源IP地址信息。另外,由于51资源限制,取消了time参数(time是本机与对方主机往返一次所用时间)显示。具体简化内容如下: (1)只支持“ping+IP地址”命令格式,域名方式和其他可选项均不可用 (2)固定32字节测试包 (3)不计算本机与对方主机往返一次所用时间,测试用时为1到2秒 总之,经过简化的ping能够完成最基本的连通测试功能。 0 8 16 31 ------------------------------------------------ | 类型(8或0) | 代码(0) | 校验和 | ------------------------------------------------ | 标识符 | 序号 | ------------------------------------------------ | 可选数据 | ------------------------------------------------ | 。。。 | ------------------------------------------------ 图1 ICMP回送请求或回答报文格式 PingCycle | 定时操作 V PingCmd ---------------- -------------- -------->| PingRequest|----------->| | 命令 | | 请求 | | | | | | | A | | B | | | | | <--------|PingEcho |<-----------|PingAnswer | 回显 ---------------- 应答 -------------- 图2 A Ping B 过程(全双工操作,反过来亦可,未画出反向过程) 图1所示为ICMP回送请求/回答报文格式(即Ping包格式),在实现网卡驱动程序后,Ping的实现简化为填写报文(详见伪代码清单)。如图2所示,完整ping过程我主要用4个函数实现。Ping请求(PingRequest)、Ping应答(PingAnswer)、收到应答后回显(PingEcho)、定时操作(PingCycle)。 PingRequest完成Ping请求。当输入Ping命令后,调用此函数,发出请求包。 PingAnswer完成Ping应答。它工作在后台,每当收到发给自己IP的请求包就自动应答。 PingEcho显示应答信息。每当收到应答就立即显示相关信息。 PingCycle定时操作pingbuf记录缓冲区。每秒钟扫描一次,并根据当前情况和状态进行状态变迁。 - ---------------------------------------------------- | | 状态(status) | 次数(times) | 内存句柄(memhandle) | | ---------------------------------------------------- N个| 。。。 | | ---------------------------------------------------- | | 状态(status) | 次数(times) | 内存句柄(memhandle) | - ---------------------------------------------------- N=MaxLenPingBuf 图3 pingbuf记录缓冲区格式 IP地址要先转换成MAC地址才能在以太网上传输,如果ARP缓存里没有对应项,则需要较长时间查找(网络传输时间相对于CPU时间)。为了不阻塞进程,我设计了一个pingbuf记录缓冲区,如图3所示。此表1秒钟被查询一次,根据当前情况改变状态。它包括状态、次数、内存句柄三个域。“次数”字段指示测试剩余数,为0表示测完,同时改变状态为0以表明此记录项现在空闲。“内存句柄”字段保存ICMP报文缓冲区首址指针。“状态”字段记录状态号,含义如下: 0---空闲 1---已发出但无应答 2---已发出且应答 3---等ARP 4---第一次准备发(用于同步1秒时钟) 状态变迁图如图4所示。 图4略,详见伪代码清单PingCycle,请自行画出状态图。 这个实现里还使用了ping命令处理、IP地址转换、校验和计算等辅助程序,详见伪代码清单。IP协议使用统一的CheckSum算法计算和验证数据报的首部校验和。将首部视为一个16位的整数序列,并定义校验和是首部中所有16位整数的和的二进制反码。同样和数及补码也被定义使用二进制反码算法。所有TCP/IP协议的校验和计算和数据包的校验均由CheckSum子程序完成。不过需要注意的是TCP和UDP的校验需要加上伪头部。需要首部校验和计算及验证的包为:IP、ICMP、UDP、TCP。相互间的差别仅在于求和数据不同,算法都采用CheckSum。详见源程序清单。(提示:IP包头从版本号、首部长度、服务类型到目的站IP地址(如果不含IP选项)共20字节;ICMP校验和只覆盖ICMP报文。对比UDP、TCP伪首部和IP首部相似点,可以不必单独分配伪首部缓冲区,而直接利用IP缓冲区计算校验和。观察知IP头邻接数据,采取一定措施可实现直接计算。即先将IP寿命字段清0,协议字段赋相应值,校验和赋UDP/TCP包长度值,并加上12,表示伪首部的3长字长度,完成计算后向IP包首部添入正确寿命、校验和值,见图5。) 0 8 16 31 0 8 16 31 ---------------------------- --------------------------- | 寿命 | 协议 | 首部校验和 | | 源站IP地址 | ---------------------------- --------------------------- | 源站IP地址 | | 目的站IP地址 | ---------------------------- --------------------------- | 目的站IP地址 | | 零 | 协议 | UDP/TCP长度 | ---------------------------- --------------------------- | 数据 | | UDP/TCP包数据 | ---------------------------- --------------------------- IP包格式(局部) UDP、TCP伪首部+数据格式 图5 IP包格式(局部)和UDP、TCP伪首部格式对比图 伪代码清单: PingRequest(IP地址) //Ping请求 { //申请小号内存 pICMP=OSMemGet(); //填写以太网帧 目的MAC地址=ping命令传入的IP地址解析后得到的物理地址 源MAC地址=本节点MAC地址 类型=0x0800 //IP包 //填写IP帧 版本号&首部长度=0x45 服务类型=0 总长度=60 标识符=GlobalID++ //全局变量16位GlobalID加1 标志&分片偏移量=0x0000 寿命=0x80 协议=0x0001 //icmp 首部校验和=0x0000 源IP地址=本节点IP地址 目的IP地址=ping命令传入的IP地址 首部校验和=CheckSum(IP首部和长度) //计算IP包首部校验和 //填写ICMP帧 类型=8 //8 请求 0 应答 代码=0 校验和=0x0 标识符=0x0300 序号=GlobalID 校验和=CheckSum(ICMP首部和长度) //计算ICMP包首部校验和 //将ICMP包登记在PingBuf中 for(i=0;i<MaxLenPingBuf;i++){ if(PingBuf[i].status==0){ PingBuf[i].times=0x8; //测试8次 PingBuf[i].ip=ping命令传入的IP地址; PingBuf[i].memhandle=内存句柄; PingBuf[i].status=4; //第一次准备发(用于同步1秒时钟) break; } } if(i==MaxLenPingBuf) 释放内存; } PingAnswer(*输入包缓冲区首址pBUF) //Ping应答 { //改写以太网帧 目的MAC地址=源MAC地址 源MAC地址=本节点MAC地址 //改写IP帧 寿命=寿命-1 目的IP地址=源IP地址 源IP地址=本节点IP地址 首部校验和=0x0000 首部校验和=CheckSum(IP首部和长度) //计算IP包首部校验和 //改写ICMP帧 类型=0 //8 请求 0 应答 校验和=0x0 校验和=CheckSum(ICMP首部和长度) //计算ICMP包首部校验和 //直接发送ICMP包至TxQFIFO缓存 OSQSend(QID,*pBUF); } PingEcho(*输入包缓冲区首址pBUF) //收到应答后回显 { //打印ping响应,因为51定时器较慢,time参数省略(time是本机与对方主机往返一次所用时间)。 PrintStr("/n/tReply from IP="); PrintStr(输入包之源IP地址); PrintStr(": bytes=32"); PrintStr(" TTL="); PrintByte(输入包之TTL); PrintStr("/n"); //处理PingBuf的记录 for(i=0;i<MaxLenPingBuf;i++){ if(PingBuf[i].status==1){ if(PingBuf[i].ip==pBUF.ipframe.ip){ PingBuf[i].status=2; //已发出且应答 break; } } } } PingCycle() //定时操作,放在1秒循环任务中 { for(;;){ taskDelay(1秒); for(i=0;i<MaxLenPingBuf;i++){ switch(PingBuf[i].status) case 0: //空闲 break; { case 1: //已发出但无应答 //打印超时信息 PrintStr("/n/tRequest timed out.("); PrintStr(PingBuf[i].ip); PrintStr(")/n"); case 2: //已发出且应答 //状态变迁 PingBuf[i].times=PingBuf[i].times-1; if(PingBuf[i].times==0) PingBuf[i].status=0; else{ case 4: //第一次准备发(用于同步1秒时钟) //查ARP缓存 if(ARP缓存有对应项){ //直接发送ICMP包至TxQFIFO缓存 OSQSend(QID,*pBUF); PingBuf[i].status=1; //已发出但无应答 } else PingBuf[i].status=3; //等ARP } break; } case 3: //等ARP { //查ARP缓存 if(ARP缓存有对应项){ //直接发送ICMP包至TxQFIFO缓存 OSQSend(QID,*pBUF); } PingBuf[i].status=1; //已发出但无应答 } default: //其他状态,错误 PingBuf[i].status=0; } } } void PingCommand(WORDTABLE *WordTable)//PING命令处理 { if(WordTable->Num==1) PrintStr("/nPlease input IP address!/n/n"); else{ if(IPadrToHEX(WordTable->wt[1].Str,&ping_ip_address)==0){ PrintStr("/nIP address error!/n/n");return; } else PingRequest(ping_ip_address); } } INT16U CheckSum(INT16U *buf,INT16U length) //校验和计算 { INT16U len; INT32U sum; len=length>>1; for(sum=0;len>0;len--) sum+=*buf++; if(length&1) sum+=(*buf&0xFF00); sum=(sum>>16)+(sum&0xFFFF); sum+=(sum>>16); return(~sum); } union ip_address_type{ //ip地址数据结构 unsigned long dwords; unsigned int words[2]; unsigned char bytes[4]; }; bit IPadrToHEX(unsigned char *Str,union ip_address_type *ip) //IP字符串转换到IP地址值 { unsigned char i,j,ch,x; ch=*Str++; for(j=0;j<3;j++){ x=0; for(i=0;i<4;i++){ if(ch=='.') {ch=*Str++;break;} else if(i==3) return 0; else if(ch<0&&ch>9) return 0; else x=10*x+(ch-'0'); ch=*Str++; } ip->bytes[j]=x; } x=0; for(i=0;i<4;i++){ if(ch=='/0') {ch=*Str++;break;} else if(i==3) return 0; else if(ch<0&&ch>9) return 0; else x=10*x+(ch-'0'); ch=*Str++; } ip->bytes[3]=x; return 1; } void HEXToIPadr(unsigned char *Str,union ip_address_type *ip) //IP地址值转换到IP字符串 { unsigned char i; unsigned char x,y; for(i=0;i<4;i++){ x=ip->bytes[i]; if(x>99){ y=x/100;*Str++=y+'0'; x=x-100*y;y=x/10;*Str++=y+'0'; x=x-10*y;*Str++=x+'0'; if(i==3) *Str++='/0'; else *Str++='.'; } else if(x>9){ y=x/10;*Str++=y+'0'; x=x-10*y;*Str++=x+'0'; if(i==3) *Str++='/0'; else *Str++='.'; } else{ *Str++=x+'0'; if(i==3) *Str++='/0'; else *Str++='.'; } } } 参考文献: 1。《用TCP/IP进行网际互连》(第3版)第一、二、三卷 DOUGLAS E.COMER著 电子工业出版社 作者<asdjf@163.com>
|