最近在做计算机网络的课程设计,整理了一点资料。
ping (Packet Internet Groper)是一种因特网包探索器,用于测试网络连接量的程序。Ping是工作在TCP/IP网络体系结构中应用层的一个服务命令, 主要是向特定的目的主机发送ICMP(Internet Control Message Protocol 因特网报文控制协议)请求报文,测试目的站是否可达及了解其有关状态 。
常见的ICMP报文可分为差错报文和查询报文,Ping主要是利用了查询报文中的回送请求或回答报文。回送请求和回送回答报文是为诊断网络而设计的,我们使用这对报文来发现网络的问题,回送请求和回送回答报文组合起来就可以确定两个网络设备之间彼此是否能够通信。回收请求和回送回答报文可以直接确定两台主机的IP协议是否能够正常通信,这是因为ICMP报文是被封装在IP数据报中发送的,发送请求的主机能收到回答报文,证明两台主机之间能够使用IP协议进行通信,同时也能证明源主机与目的主机之间的所有路由器的接收、处理、转发功能正常。
在写代码前,我们必须先了解一个很重要的库:<winsock.h> ,最好能掌握其中关键的一些函数https://baike.baidu.com/item/Winsock/6154208?fr=aladdin
要想具体了解Ping的实现,首先我们先要了解一点计算机网络的知识,Ping是工作在TCP/IP体系结构下的,同时运用的是IP网际层的ICMP协议,所以我们先要了解IP数据报和ICMP数据报的格式(特别是首部的格式)。
由此可写出IP首部和ICMP的首部(其中ICMP首部中的标识和代码可根据ICMP报文类型决定是否需要)
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;
到了这里,Ping功能的实现可以归结为发送请求和接受请求这两个主要部分,但是由于发生的数据报需要根据相应的协议进行填充和解析,所以,接下来分为计算校验和、发送回应请求函数、接收应答回复并进行解析、等待回应答复四个部分(函数)
计算检验和:
- 把校验和字段置为0;
- 对IP头部中的每16bit进行二进制求和;
- 如果和的高16bit不为0,则将和的高16bit和低16bit反复相加,直到和的高16bit为0,从而获得一个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; //每16位相加,因此字节数-2
}
//补全奇数位
if (nleft == 1)
{
u_short u = 0;
*(u_char *)(&u) = *(u_char*)w; //unsigned short转为unsigned char 16位转为8位
sum += u; //字节数为奇数
}
//将反馈的 16 位从高位移到低位
sum = (sum >> 16) + (sum & 0xffff); //高16位和低16位相加,sum & 0xffff将高16位置0
sum += (sum >> 16); //将低16位与溢出值相加
answer = ~sum; //取反码
return (answer);
}
发送回应请求函数:在这个函数中,需要填充ICMP协议首部的信息,其中数据部分没有具体规定,检验和要先置零,利用上一个函数计算出具体数值后再赋值,最后发送
int SendEchoRequest(SOCKET s, struct sockaddr_in *lpstToAddr)
{
static ECHOREQUEST echoReq;
//static int nId = 1;
//static int 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(); //GetTickcount函数:它返回从操作系统启动到当前所经过的毫秒数;最小精度为18ms
//计算回应请求的校验和
echoReq.icmpHdr.Checksum = checksum((u_short*)&echoReq, sizeof(ECHOREQUEST));
//发送回应请求
nRet = sendto(s, (char*)&echoReq, sizeof(ECHOREQUEST), 0, (struct sockaddr*)lpstToAddr, sizeof(SOCKADDR_IN));
if (nRet == SOCKET_ERROR)
{
printf("send to() error:%d\n", WSAGetLastError());
}
return (nRet);
}
接收应答回复并进行解析:接收应答回复用的是recvfromn()函数,而不是recv()函数,可以将目的IP的相关信息赋值给lpasFrom
DWORD RecvEchoReply(SOCKET s, LPSOCKADDR_IN lpsaFrom, u_char *pTTL)
{
ECHOREPLY echoReply;
int nRet;
int nAddrLen = sizeof(struct sockaddr_in);
//接收应答回复
nRet = recvfrom(s, (char*)&echoReply, sizeof(ECHOREPLY), 0, (LPSOCKADDR)lpsaFrom, &nAddrLen);
//检验接收结果
if (nRet == SOCKET_ERROR)
{
printf("recvfrom() error:%d\n", WSAGetLastError());
}
//记录返回的 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; //socket的数组
timeout.tv_sec = 1;
timeout.tv_usec = 0; //设计时间为1s,超过1s为超时
return(select(1, &readfds, NULL, NULL, &timeout)); //0:超时;大于0的值时,表示SOCKET数;失败时返回SOCKET_ERROR
}
最后,就是Ping的实现了:logic用来设置Ping4次还是无限次。其中,GetTickcount函数:它返回从操作系统启动到当前所经过的毫秒数,根据公式:往返时间=接收时刻-发送时刻,可用于计算往返时间
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; //ttl 生存时间
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); //仅支持AF_INET格式,也就是说ARPA Internet地址格式; SOCK_RAW 为原始套接字,由于自定义ip
// 第二个注释函数 socket
if (rawSocket == SOCKET_ERROR) //检验error
{
printf("socket() error:%d\n", WSAGetLastError());
return;
}
//检测目标主机
lpHost = gethostbyname(pstrHost);
if (lpHost == NULL)
{
printf("Host not found:%s\n", pstrHost);
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", pstrHost, inet_ntoa(destIP.sin_addr), REQ_DATASIZE);
//发起多次 PING 测试
for (nLoop = 0; nLoop < k; nLoop++) {
if (logic) k = k + 1;
//发送 ICMP 回应请求
SendEchoRequest(rawSocket, &destIP);
//等待回复的数据
nRet = WaitForEchoReply(rawSocket);
if (nRet == SOCKET_ERROR)
{
printf("select() error:%d\n", WSAGetLastError());
break;//退出循环
}
if (!nRet)
{
lost++;
printf("\nRequest time out.");
continue;//进入下一次
}
//接收回复
dwTimeSent = RecvEchoReply(rawSocket, &srcIP, &cTTL);
reveived++;
//计算花费的时间
dwElapsed = GetTickCount() - dwTimeSent;
if (dwElapsed > maximum) maximum = dwElapsed;
if (dwElapsed < minimum) minimum = dwElapsed;
average += dwElapsed;
printf("\nReply from %s: bytes = %d time = %ldms TTL = %d",
inet_ntoa(srcIP.sin_addr), REQ_DATASIZE, dwElapsed, cTTL);
if (_kbhit()) /* Use _getch to throw key away. */
{
if ((c = _getch()) == 0x2) //crrl -b
break;
}
else
Sleep(1000);
}
/*printf("\n\n");
printf("Ping statistics for %s:\n", inet_ntoa(srcIP.sin_addr));
printf(" Packets: Sent = %d, Received = %d, Lost = %d (%.f%% loss),\n",
sent, reveived, lost, (float)(lost*1.0 / sent) * 100);
if (lost == 0)
{
printf("Approximate round trip times in milli-seconds:\n");
printf(" Minimum = %dms, Maximum = %dms, Average = %dms\n", minimum, maximum, average / sent);
}*/
printf("\n\n");
nRet = closesocket(rawSocket);
if (nRet == SOCKET_ERROR)
{
printf("closesocket() error:%d\n", WSAGetLastError());
}
}
结果演示: