Not Only Algorithm,不仅仅是算法,关注数学、算法、数据结构、程序员笔试面试以及一切涉及计算机编程之美的内容 。。
你的位置:NoAlGo博客 » 网络 » 

PING程序实现

PING是一个我们非常熟悉的网络命令了,可以检查网络是否通畅或者网络连接速度,使用起来非常方便。但PING是怎么实现的呢?本文将介绍PING的内部原理并实现一个简单的PING程序。

PING

PING(Packet Internet Groper,因特网包探索器),是用于测试网络连接量的程序。Ping发送一个ICMP(Internet Control Messages Protocol,因特网信报控制协议)回声请求消息给目的地并报告是否收到所希望的ICMP echo (ICMP回声应答)。它利用网络上机器IP地址的唯一性,给目标IP地址发送一个数据包,再要求对方返回一个同样大小的数据包来确定两台网络机器是否连接相通,并通过发送与接收的时差确定两者的延时。

下面是一个在Debian机器上使用PING命令的结果:

noalgo@AdMin:~$ ping noalgo.info
PING noalgo.info (59.188.71.230) 56(84) bytes of data.
64 bytes from 59.188.71.230: icmp_req=1 ttl=47 time=7.63 ms
64 bytes from 59.188.71.230: icmp_req=2 ttl=47 time=8.18 ms
^C64 bytes from 59.188.71.230: icmp_req=3 ttl=47 time=9.04 ms

--- noalgo.info ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 10022ms
rtt min/avg/max/mdev = 7.639/8.287/9.042/0.582 ms

输出结果显示了被测试系统主机名和相应IP地址、返回给当前主机的ICMP报文顺序号、ttl生存时间和往返时间rtt。
TTL(time to live)表示ping程序发送的icmp数据包的生存周期,每经过一个网段,TTL的值减1,当其值被减为0时,该数据包将被丢弃,但该数据包的源地址将会被告知情况,以重新发送该数据包。另外,不同操作系统的TTL值是不相同的,linux操作系统是64。

IP

IP(Internet Protocol,网络之间互连的协议)是为计算机网络相互连接进行通信而设计的协议。由于PING程序使用了ICMP协议,而ICMP协议是IP层的一个协议,所以PING程序的数据需要经过两级封装,先添加ICMP报头形成ICMP报文,再添加IP报头形成IP数据报。
本节将简单介绍IP的包头数据格式,如下图所示:

| Ver | Head Len |      TOS      |        Total Length            |  
|          Identifier            |     Flags    |   Frag Offset   |
|      TTL       |    Protocol   |        Header Checksum         |
|                          Source Address                         |
|                       Destination Address                       |
|                  Options                      |     padding     |

其中各字段含义如下:

  • 版本号(Version):长度4位。标识采用的IP协议版本号,一般值为0100(IPv4),0110(IPv6)。
  • IP包头长度(Header Length):长度4位。描述IP包头的长度,因为在IP包头中有变长的可选部分。IP包头最小长度为20字节,最长为15*4=60字节。
  • 服务类型(Type of Service):长度8位。表示服务的类型,如包的优先级。
  • IP包总长(Total Length):长度16位。IP包的长度(包括头部和数据),以字节为单位,最大长度65535字节。
  • 标识符(Identifier):长度16位。当路由器将一个较大的上层数据包进行分段拆分后,所有小包被标记相同的值,以便目的端设备能够区分哪个包属于被拆分开的包的一部分。
  • 标记(Flags):长度3位。第一位不使用。第二位是DF(Don’t Fragment)位,表明路由器是否对该上层数据包分段。第三位是MF(More Fragments)位,表明后面是否还有属于同一个IP包的分段。
  • 片偏移(Fragment Offset):长度13位。表示该IP包在该组分片包中位置,接收端靠此来组装还原IP包。
  • 生存时间(TTL):长度8位。IP包每经过一个沿途的路由器时,路由器会将其TTL值减1。如果TTL减少为0,则该IP包会被丢弃。
  • 协议(Protocol):长度8位。标识所使用的协议,如1表示ICMP,6表示TCP,17表示UDP。
  • 头部校验(Header Checksum):长度16位。检测IP头部的正确性,不包含数据部分。因为每个路由器要改变TTL的值,所以路由器会为每个通过的数据包重新计算这个值。
  • 起源和目标地址(Source and Destination Addresses):长度32位。标识了IP包的起始和目标地址。
  • 可选项(Options):可变长的字段,可以包含如松散源路由 、严格源路由、路由记录、时间戳等内容。
  • 填充(Padding):IP包头的长度必须为32位的整数倍,用来填充未使用的位。

在接收到一个PING的应答之后,我们可以根据其IP包中的长度字段得到ICMP包头的位置,然后再从ICMP包头中读取相应的数据。

ICMP

ICMP(Internet Control Message Protocol)协议TCP/IP协议族的一个子协议,它是一种面向无连接的协议,用于在IP主机、路由器之间传递控制消息。控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用。

ICMP有很多报文,PING命令只使用其中的两种:

  • ICMP_ECHO:请求回送
  • ICMP_ECHOREPLY:请求回应

这两种报文的格式如下:

| Type | code |   Checksum   |
|      ID     |    Seq No    |
|           Data             |

其中各字段的含义如下:

  • 类型(Type):长度8位,表示报文类型,这里为ICMP_ECHO或ICMP_ECHOREPLY。
  • 编码(Code):长度8位,置0,不使用。
  • 校验和(ICMP Header Checksum):长度16位,ICMP数据按16位累加,不足16位的直接加上,结果取反即为校验和。
  • 标识符(Identifier):长度16位,唯一标识ICMP报文,可以帮助ECHO请求匹配其对应的应答。
  • 顺序号(Sequence number):长度16,表示ICMP报文发送顺心,可以帮助ECHO请求匹配其对应的应答。
  • 数据(Data):可变长度,实现的具体数据。

在实现代码的sendRequest函数中将会对这里涉及到的内容进行填充,把进程ID赋给标识符字段进行识别,并把发送时间放到数据字段。这样当接收到应答时,可以直接从数据字段中读取发送的时间,然后计算接收时间和发送时间的时间差。

另外还有一个问题,当PING自己的时候(如localhost),源主机即为目的主机,则目的主机第一个接收到的数据包即为自己发送的请求包,这时我们的PING程序应该忽略这个包,而是接收内核稍后自动返回的应答数据包,所以程序的recvReply函数中使用了循环的模式,只有成功接收或者发生错误时才退出循环。

PING实现

PING的原理非常直接,源主机向目标主机发送ICMP数据包,然后接受目标主机返回的应答,根据应答统计rtt等信息。本文PING的实现将按照这个思路进行,主程序每隔一秒就发送一个数据包,然后接收对应的应答,直到用户按下Ctrl+C才结束数据的发送并进行结果统计。另外使用了套接字选项来解决超时的问题。

本程序编译后的可执行文件为Ping(自带的PING程序为ping,首字母不同),由于要root用户才能创建原始套接字,因此本程序需要使用sudo进行运行,下面是本程序的样例输出结果,可以看到,本程序尽量模仿Debian自带的输出模式进行结果显示。

noalgo@AdMin:~$ sudo ./Ping noalgo.info
PING noalgo.info (59.188.71.230) 56(84) bytes of data.
64 byte from 59.188.71.230: icmp_req=1, ttl=47, time=7.8 ms
64 byte from 59.188.71.230: icmp_req=2, ttl=47, time=8.7 ms
64 byte from 59.188.71.230: icmp_req=3, ttl=47, time=8.0 ms
^C
--- noalgo.info ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2506ms
rtt min/avg/max/mdev = 7.805/8.150/8.659/0.367 ms

最后是本程序的完整代码。

#include <stdio.h>
#include <netdb.h>
#include <errno.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/time.h>
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>

#include <cmath>
#include <vector>
#include <numeric>
#include <algorithm>

const size_t MXLEN = 1500;
const size_t DATALEN = 56;
const size_t REQLEN = ICMP_MINLEN + DATALEN;
const size_t NAMELEN = 50;

char dstIP[NAMELEN], dstName[NAMELEN]; //目标IP,目标主机名
char sendBuf[REQLEN], recvBuf[MXLEN];  //发送缓存,接收缓冲

sockaddr_in dstAddr;        //目标地址
uint32_t nrSend, nrRecv;    //发送包数量,接收包数量
std::vector<double> rtts;   //记录每次的rtt

timeval startTime, endTime; //开始时间,结束时间

//计算校验和
uint16_t calChecksum(uint16_t *buf, size_t len)
{
	uint32_t sum = 0;
	while (len > 1) sum += *buf++, len -= 2;
	if (len == 1)
		sum += (*(uint8_t *)buf) << 4 ;
		
	sum = (sum >> 16) + (sum & 0xFFFF);
	sum += sum >> 16;
	return (uint16_t)~sum;
}

//输出统计信息
void calStat(int signo)
{
	//计时结束,计算花费的时间
	gettimeofday(&endTime, NULL);
	int elapse = 1000 * (endTime.tv_sec - startTime.tv_sec) + (endTime.tv_usec - startTime.tv_usec) / 1000;

	printf("\n--- %s ping statistics ---\n"
	       "%u packets transmitted, %u received, %.lf%% packet loss, time %dms\n",
	       dstName, nrSend, nrRecv, 1 - (double)nrRecv / nrSend, elapse);

	//统计rtt信息
	std::sort(rtts.begin(), rtts.end());
	double mn = *rtts.begin(), mx = *rtts.rbegin();
	double avg = accumulate(rtts.begin(), rtts.end(), 0.0) / rtts.size();
	double mdev = 0;
	for (std::vector<double>::iterator p = rtts.begin(); p != rtts.end(); p++) 
		mdev += (*p - avg) * (*p - avg);
	mdev = pow(mdev / rtts.size(), 0.5);
	
	printf("rtt min/avg/max/mdev = %.3lf/%.3lf/%.3lf/%.3lf ms\n", mn, avg, mx, mdev);

	exit(0);
}

//发送ICMP包
void sendRequest(int sockfd)
{
	//包计数
	static uint32_t no = 1;

	//填充ICMP包内容
	icmp *icmp = (struct icmp *)sendBuf;
	icmp->icmp_type = ICMP_ECHO;
	icmp->icmp_code = 0;
	icmp->icmp_cksum = 0;
	icmp->icmp_seq = no++;
	icmp->icmp_id = getpid();

	timeval *tv = (timeval *)icmp->icmp_data;
	gettimeofday(tv, NULL);
	icmp->icmp_cksum = calChecksum((uint16_t *)icmp, REQLEN);

	//发送
	int n = sendto(sockfd, sendBuf, REQLEN, 0, (sockaddr *)&dstAddr, sizeof(dstAddr));
	if (n <= 0)
	{
		if (errno == EWOULDBLOCK) 
			printf("Send timed out.\n");
		else 
			perror("sendto error");
	}
	else
		nrSend++;
}

//接收ICMP包
void recvReply(int sockfd)
{
	socklen_t addrLen = sizeof(dstAddr);
	while (true) //接收ICMP应答,无限循环直到成功接收或超时出错。
	{
		int n = recvfrom(sockfd, recvBuf, sizeof(recvBuf), 0, (sockaddr *)&dstAddr, &addrLen); 
		if (n <= 0)
		{
			if (errno == EWOULDBLOCK)
				printf("Receive timed out.\n");
			else
				perror("recvfrom error");
			return;
		}

		ip *ip = (struct ip *)recvBuf;
		int iphdrLen = ip->ip_hl << 2;
		if ((n -= iphdrLen) < 8)
		{
			printf("ICMP packets too short.\n");
			return;
		}

		icmp *icmp = (struct icmp *)(recvBuf + iphdrLen);	
		if (icmp->icmp_type == ICMP_ECHOREPLY && icmp->icmp_id == getpid()) //确实是之前发送包的应答
		{
			nrRecv++; 
			timeval *tvSend = (timeval *)icmp->icmp_data, tvRecv;
			gettimeofday(&tvRecv, NULL);
			double rtt = (tvRecv.tv_sec - tvSend->tv_sec) * 1000 + (tvRecv.tv_usec - tvSend->tv_usec) / 1000.0; //时间差
			rtts.push_back(rtt);
			printf("%d byte from %s: icmp_req=%u, ttl=%d, time=%.1lf ms\n", n, dstIP, icmp->icmp_seq,  ip->ip_ttl, rtt);
			return;
		}
	}
}

//无限ping
void ping(int sockfd)
{
	//扩大缓冲区
	int size = 1024 * 50;
	setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));

	//设置超时值
	timeval tv = {5, 0};
	setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
	setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
	
	//无限ping只能靠ctrl-c结束,设置该信号的响应
	signal(SIGINT, calStat);
	
	nrSend = nrRecv = 0;
	while (1) //无限发送ICMP请求和接收应答
	{
		sendRequest(sockfd);
		recvReply(sockfd);
		sleep(1); //1秒1次
	}
}

int main(int argc, char **argv)
{
	if (argc != 2)
	{
		printf("Usage: Ping IP/host.\n");
		return 0;
	}

	gettimeofday(&startTime, NULL); //计时开始

	int sockfd;
	if ((sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)) < 0)
	{
		perror("Creating socket error");
		exit(1);
	}
			
	memset(&dstAddr, 0, sizeof(dstAddr));
	dstAddr.sin_family = AF_INET;
	if (inet_pton(AF_INET, argv[1], &dstAddr.sin_addr) <= 0) 
	{	
		//输入的是主机名
		hostent *host;
		if ((host = gethostbyname(argv[1])) == NULL)
		{
			perror("Invalid parameter");
			exit(1);
		}
		memcpy((char *)&dstAddr.sin_addr, host->h_addr, sizeof(dstAddr.sin_addr));
		inet_ntop(AF_INET, (void *)&dstAddr.sin_addr, dstIP, sizeof(dstIP));
		strcpy(dstName, host->h_name);
	}
	else
	{
		//输入的是IP地址
		memcpy(dstIP, argv[1], sizeof(argv[1]));
		memcpy(dstName, argv[1], sizeof(argv[1]));
	}

	printf("PING %s (%s) %d(%d) bytes of data.\n", dstName, dstIP, DATALEN, REQLEN + 20);

	ping(sockfd);

	return 0;
}
上一篇: 下一篇:

我的博客

NoAlGo头像编程这件小事牵扯到太多的知识,很容易知其然而不知其所以然,但真正了不起的程序员对自己程序的每一个字节都了如指掌,要立足基础理论,努力提升自我的专业修养。

站内搜索

最新评论