c语言实现ping程序
基础知识
1、ICMP协议
互联网控制信息协议(Internet CONTROLM essage Protocol),用于错误报告和调试。该协议是TCP/IP 协议集中的一个子协议,属于网络层协议,主要用于在主机与路由器之间传递控制信息,包括报告错误、交换受限控制和状态信息等。当遇到IP 数据无法访问目标、IP 路由器无法按当前的传输速率转发数据包等情况时,会自动发送ICMP消息。我们可以通过Ping 命令发送ICMP回应请求消息并记录收到ICMP回应回复消息,通过这些消息来对网络或主机的故障提供参考依据。常见ICMP报文有响应请求报文,目标不可到达、源抑制和超时报文,此外还有时间戳报文。
2、Ping 工作原理
Ping的原理就是首先建立通道,然后发送包,对方接受后返回信息,这个包至少包括以下内容:发送的时候,包的内容包括对方的ip地址和自己的地址,还有序列数;回送的时候包括双方地址,还有时间等,主要是接受方在都是在操作系统内核里做好的,时刻在监听。Ping程序生成一个icmp“回送请求”,将其发送给目的主机。通过检测是否可以收到目标主机的应答,便可以知道网络的连通性。
3.ICMP报文格式
4.IP报文格式
5、WinSock原始套接字的使用方法与API
函数Winsock 原始套接字编程过程中,服务器端/ 客户端的编程都按照以下步骤:
初始化套接字(WSAStartup)
创建套接字(socket 或WSASocket)
向服务器通信(sendto/recvfrom)
关闭套接字(closesocket)
结束使用套接字(WSACleanup)
6、三种WinSock地址结构
① 用的Winsock 地址结构sockaddr ,针对各种通信域的套接字,存储它们的地址信息。
② 专门针对Internet 通信域的Winsock 地址结构sockaddr_in
③ 专用于存储IP 地址的结构in_addr
7、编程时,需要用到一些windows 函数,说明如下:
(1)int WSAStartup(WORD wVersionRequested,LPWSADA TA 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接口并不适用某种特定的协议去封装它,而是由程序自行处理数据包以及协议首部);第三个参数指定应用程序所使用的通信协议。
(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) 创建协议类型为IPPROTO_ICMP 的原始套接字,设置套接字属性。
(2) 创建并初始化ICMP 封包。
(3) 调用sendto 函数向远程主机发送ICMP 请求。
(4) 调用recfrom 函数接受ICMP 响应。
Ping程序的设计与实现大致可分为四个模块,分别是:初始化模块、功能控制模块、ping 模块、mian 测试模块。
1.初始化模块:该模块用于定义及初始化各个全局变量,为winsock加载winsock 体。主要包括定义IP 首部格式、定义ICMP首部格式、定义ICMP回应请求、定义ICMP回应答复。
2.功能控制模块: 该模块是被其他模块调用,其功能包括计算校验和、发送回应请求函数、接收应答回复并进行解析、等待回应答复。
计算校验和
发送请求
接收应答回复并进行解析
等待回应答复
3.Ping 模块功能模块 该模块是本程序的核心模块,调用其他模块实现其功
能,进而实现Ping 的功能。
4.main()函数模块:向指定的域名或IP 地址发送Echo请求报文;根据响应报文显示出Ping 的结果
代码
#include <stdio.h>
#include <stdlib.h>
#include <winsock.h>
#include<conio.h>
#pragma comment(lib, "ws2_32.lib")// 导入库文件
#define ICMP_ECHOREPLY 0 //ICMP 回应答复
#define ICMP_ECHOREQ 8 //ICMP 回应请求
#define REQ_DATASIZE 32 // 请求数据报大小
#include <iostream>
using namespace std;
//定义 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 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();
//计算回应请求的校验和
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", WSAGetLastError());
}
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",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;
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", 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());
}
}
//主程序
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", ptr);
}
//开始 PING
Ping(ptr,log);
//程序释放 Winsock 资源
WSACleanup();
}
}