用C语言实现Ping命令

测试环境:

Target: x86_64-linux-gnu

gcc version 5.3.1 20160413 (Ubuntu 5.3.1-14ubuntu2.1)

【引言】

前几天脑子里忽然蹦出来一个想法,想试着用C语言写一个自己的Ping命令出来。开始还有些茫然无措,因为此前没有接触过网络编程方面的知识。幸运的是对Ping的实现原理还是比较清楚的,难度和挑战皆在于网络编程方面的零基础,再加上现在的课程比较多,所以只能每天抽出一部分时间来学习所需的网络编程知识。大概会持续个两三周。既然如此,索性开个博客,记录下整个学习的过程,也算是每天的总结了吧。

【第一天】

这也算是一个小项目了,当然不能动手就开始敲代码。总得把思路先理清楚。,搞清楚做什么?怎么做?然后才是实现。

做什么?当然是Ping命令了,来看看这家伙的真实面貌:

              

额额,这个目标仿佛是有些空洞的,等于没说。应该先弄清楚Ping的实现原理才对。于是各种回忆,翻课本,上网查资料。总结如下。

Ping命令介绍

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

其中关键在于发送ICMP数据包,然后对接收到的包进行一定的处理。不可避免,我们要发送ICMP包,必须自己来构建一个包出来。再来回顾一下ICMP:

ICMP是(Internet Control Message Protocol)Internet控制报文协议。它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。ICMP是面向无连接的协议。

IPing命令只使用众多ICMP报文中的两种:"请求回送'(ICMP_ECHO)和"请求回应'(ICMP_ECHOREPLY)。这两种报文类型的报头格式如下:

              

当TYPE字段为ICMP_ECHO,CODE字段为0时,代表请求回送报文;TYPE字段为ICMP_ECHOREPLY,CODE字段为0时,代表请求回应报文。

ICMP报文

在Linux中ICMP数据结构(<netinet/ip_icmp.h>)定义如下:

struct icmp
{
  u_int8_t  icmp_type;  /* type of message, see below */
  u_int8_t  icmp_code;  /* type sub code */
  u_int16_t icmp_cksum; /* ones complement checksum of struct */
  union
  {
    u_char ih_pptr;     /* ICMP_PARAMPROB */
    struct in_addr ih_gwaddr;   /* gateway address */
    struct ih_idseq     /* echo datagram */
    {
      u_int16_t icd_id;
      u_int16_t icd_seq;
    } ih_idseq;
    u_int32_t ih_void;
    /* ICMP_UNREACH_NEEDFRAG -- Path MTU Discovery (RFC1191) */
    struct ih_pmtu
    {
      u_int16_t ipm_void;
      u_int16_t ipm_nextmtu;
    } ih_pmtu;
    struct ih_rtradv
    {
      u_int8_t irt_num_addrs;
      u_int8_t irt_wpa;
      u_int16_t irt_lifetime;
    } ih_rtradv;
  } icmp_hun;
#define icmp_pptr   icmp_hun.ih_pptr
#define icmp_gwaddr icmp_hun.ih_gwaddr
#define icmp_id     icmp_hun.ih_idseq.icd_id
#define icmp_seq        icmp_hun.ih_idseq.icd_seq
#define icmp_void   icmp_hun.ih_void
#define icmp_pmvoid icmp_hun.ih_pmtu.ipm_void
#define icmp_nextmtu    icmp_hun.ih_pmtu.ipm_nextmtu
#define icmp_num_addrs  icmp_hun.ih_rtradv.irt_num_addrs
#define icmp_wpa    icmp_hun.ih_rtradv.irt_wpa
#define icmp_lifetime   icmp_hun.ih_rtradv.irt_lifetime
  union
  {
    struct
    {
      u_int32_t its_otime;
      u_int32_t its_rtime;
      u_int32_t its_ttime;
    } id_ts;
    struct
    {
      struct ip idi_ip;
      /* options and then 64 bits of data */
    } id_ip;
    struct icmp_ra_addr id_radv;
    u_int32_t   id_mask;
    u_int8_t    id_data[1];
  } icmp_dun;
#define icmp_otime  icmp_dun.id_ts.its_otime
#define icmp_rtime  icmp_dun.id_ts.its_rtime
#define icmp_ttime  icmp_dun.id_ts.its_ttime
#define icmp_ip     icmp_dun.id_ip.idi_ip
#define icmp_radv   icmp_dun.id_radv
#define icmp_mask   icmp_dun.id_mask
#define icmp_data   icmp_dun.id_data
};

使用宏定义令表达更简洁,其中ICMP报头为8字节,数据报长度最大为64K字节。

  1. 校验和算法:这一算法称为网际校验和算法,把被校验的数据16位进行累加,然后取反码,若数据字节长度为奇数,则数据尾部补一个字节的0以凑成偶数。此算法适用于IPv4、ICMPv4、IGMPV4、ICMPv6、UDP和TCP校验和,更详细的信息请参考RFC1071,校验和字段为上述ICMP数据结构的icmp_cksum变量。
  2. 标识符:用于唯一标识ICMP报文, 为上述ICMP数据结构的icmp_id宏所指的变量。
  3. 顺序号:ping命令的icmp_seq便由这里读出,代表ICMP报文的发送顺序,为上述ICMP数据结构的icmp_seq宏所指的变量。

ICMP封装好后是这样的:

                


ICMP是为网关和目标主机而提供的一种差错控制机制,使它们在遇到差错时能把错误报告给报文源发方。ICMP协议是IP层的一个协议,但是由于差错报告在发送给报文源发方时可能也要经过若干子网,因此牵涉到路由选择等问题,所以ICMP报文需通过IP协议来发送。ICMP数据报的数据发送前需要两级封装:首先添加ICMP报头形成ICMP报文,再添加IP报头形成IP数据报。因此我们还需知道IP报文的格式。

IP分组

               

整个ICMP报文作为了IP报文的数据部分,再给它加上IP报头就成了这样:

                

在Linux中,IP报头格式数据结构(<netinet/ip.h>)定义如下:

struct ip
  {
#if __BYTE_ORDER == __LITTLE_ENDIAN
    unsigned int ip_hl:4;       /* header length */
    unsigned int ip_v:4;        /* version */
#endif
#if __BYTE_ORDER == __BIG_ENDIAN
    unsigned int ip_v:4;        /* version */
    unsigned int ip_hl:4;       /* header length */
#endif
    u_int8_t ip_tos;            /* type of service */
    u_short ip_len;         /* total length */
    u_short ip_id;          /* identification */
    u_short ip_off;         /* fragment offset field */
#define IP_RF 0x8000            /* reserved fragment flag */
#define IP_DF 0x4000            /* dont fragment flag */
#define IP_MF 0x2000            /* more fragments flag */
#define IP_OFFMASK 0x1fff       /* mask for fragmenting bits */
    u_int8_t ip_ttl;            /* time to live */
    u_int8_t ip_p;          /* protocol */
    u_short ip_sum;         /* checksum */
    struct in_addr ip_src, ip_dst;  /* source and dest address */
  };
 

其中ping程序只使用以下数据:

  • IP报头长度IHL(Internet Header Length)以4字节为一个单位来记录IP报头的长度,是上述IP数据结构的ip_hl变量。
  • 生存时间TTL(Time To Live)以秒为单位,指出IP数据报能在网络上停留的最长时间,其值由发送方设定,并在经过路由的每一个节点时减一,当该值为0时,数据报将被丢弃,是上述IP数据结构的ip_ttl变量。

第一天将ping的实现原理和有关报文的格式理清楚。

【第二天】

基础该该复习的也都复习了,所以今天图书馆挑了几本书,先来大体了解一下关于套接字里边的东西。

推荐的书
  • 《LInux高性能服务器编程》
  • 《Unix网络编程 第2卷》
  • 《Unix环境高级编程》

这些书都不薄,想在一天从零基础内看完一本都是不可能的,我只是打算通读一下其中关于套接字和网络编程基础的章节,以期有对这部分知识一个大体了解,而非精读。这样的话加起来也就100多页的样子。今天下来,也算初步有个认识了。后期用到具体的函数时,再具体深入的了解。

【第三天】

现在可以先初步搭一个框架出来:

  1. 检测参数
  2. 获取(转换)目标IP地址
  3. 发报文
  4. 接收报文
  5. 打印信息

#include "ping.h"


void Call(int argc, char *argv[])
{
	struct hostent * pHost;		//保存主机信息
	struct sockaddr_in dest_addr; 	//IPv4专用socket地址,保存目的地址
	in_addr_t inaddr;		//ip地址(网络字节序)

	if (argc < 2)
	{
		printf("Usage: %s [hostname/IP address]\n", argv[0]);
		exit(EXIT_FAILURE);	
	}


	/* 将点分十进制ip地址转换为网络字节序 */
	if ((inaddr = inet_addr(argv[1])) == INADDR_NONE)
	{
		/* 转换失败,表明是主机名,需通过主机名获取ip */
		if ((pHost = gethostbyname(argv[1])) == NULL)
		{
			herror("gethostbyname()");
			exit(EXIT_FAILURE);
		}
		memmove(&dest_addr.sin_addr, pHost->h_addr_list[0], pHost->h_length);
	}
	else
	{
		memmove(&dest_addr.sin_addr, &inaddr, sizeof(struct in_addr));
	}


	printf("PING %s(%s) %d bytes of data.\n", argv[1], 
			inet_ntoa(dest_addr.sin_addr), ICMP_DATA_LEN);
//	SendPacket();
//	RecvePacket();
//	Print();
}

int main(int argc, char *argv[])
{
	Call(argc, argv);

	return 0;
}
hostent的定义如下:

struct hostent{
char * h_name; //地址的正式名称
char ** h_aliases; //空字节-地址的预备名称的指针
short h_addrtype; //地址类型; 通常是AF_INET
short h_length; //地址长度
char ** h_addr_list; //机网络地址指针。网络字节序
};
#define h_addr h_addr_list[0]

gethostbyname()函数用来通过主机名获得ip地址(要发当然需要知道目标IP地址),用法不难,详细信息可以查看: Here

为什么要将点分十进制ip地址转换为网络字节序(TCP/IP协议栈使用大端字节序:详细介绍:大端字节序和小端字节序_大端排序-CSDN博客)?

通常,然人们习惯用可读性好的字符串来表示IP地址,比如用点分十进制字符串表示IPv4地址,以及以十六进制字符串表示IPV6地址。但编程中我们需要首先把它们转化为整数(二进制数)方能使用。而记录日志时则相反,我们要把整数表示的IP地址转化为可读的字符串。下面3个函数可用与点分十进制字符串表示的IPV4地址和用网络字节序表示的IPv4地址之间进行转化:
#include <arpa/inet.h>

in_addr_t inet_addr(const char * strptr);

int inet_aton(const char *cp, struct in_addr* inp);

char * inet_ntoa(struct in_addrt in);

ICMP_DATA_LEN:define 定义的标识符,放在头文件,用来表示ICMP报文的长度,为64

最后注释掉的三个函数尚未实现。

【第四天】

要实现发送报文的函数,首先得有报文才行,所以要先实现一个设置ICMP报文的函数。有关ICMP报文的信息已经了解的差不多了,就看具体实现了:

char SendBuffer[SEND_BUFFER_SIZE];

u_int16_t Compute_cksum(struct icmp *pIcmp)
{
	u_int16_t *data = (u_int16_t *)pIcmp;
	int len = ICMP_LEN;
	u_int32_t sum = 0;
	
	while (len > 1)
	{
		sum += *data++;
		len -= 2;
	}
	if (1 == len)
	{
		u_int16_t tmp = *data;
		tmp &= 0xff00;
		sum += tmp;
	}
	sum = (sum >> 16) + (sum & 0x0000ffff);
	sum += sum >> 16;
	sum = ~sum;
	
	return sum;
}

void SetICMP(u_int16_t seq)
{
	struct icmp *pIcmp;
	struct timeval *pTime;

	pIcmp = (struct icmp*)SendBuffer;
	
	/* 类型和代码分别为ICMP_ECHO,0代表请求回送 */
	pIcmp->icmp_type = ICMP_ECHO;
	pIcmp->icmp_code = 0;
	pIcmp->icmp_cksum = 0;		//校验和
	pIcmp->icmp_seq = seq;		//字节号
	pIcmp->icmp_id = getpid();	//取进程号作为标志
	pTime = (struct timeval *)pIcmp->icmp_data;
	gettimeofday(pTime, NULL);	//数据段存放发送时间
	pIcmp->icmp_cksum = Compute_cksum(pIcmp);
}
 

如上,单独将计算校验和的项给分离了出来,为了逻辑更加清晰。设置一个发送缓冲区,SEND_BUFFER_SIZE为暂为128(字节),理论上来讲64字节足够了,后边视情况再修改。计算校验和的方法很简单,网上示例很多,知道原理,写出来不是什么难事。

Ping命令中需要显示的信息,包括icmp_seq和ttl都已有实现的办法,但是还不知道往返时间rtt。为了实现这一功能,可以通过ICMP数据报携带一个时间戳来实现。(在回送请求报文中包含了的可选数据,在应答报文中包含了该可选数据的一个副本)

struct timevl结构体如下:

struct timeval{
			long tv_sec;
			long tv_usec;
		}
可以通过函数 int gettimeofday(struct timeval *tp,void *tzp);来获取系统当前时间。其中tv_sec为秒数,tv_usec为微妙数。在发送报文和接收报文时各通过gettimeofday函数获取一次时间,两次时间差就可以求出往返时间。我把这个时间戳作为了数据信息。tzp指针表示时区,这里只是要时间差,所以不需要,赋为NULL值。
 

【第五天】

既然设置好了ICMP报文,便可以发送了。

但是发送往哪发送呢?当然是往套接字上发送了。可以把套接字看成一个文件,往这上边写数据,即代表发送。

创建一个套接字
/* 创建ICMP套接字 */
	//AF_INET:IPv4, SOCK_RAW:IP协议数据报接口, IPPROTO_ICMP:ICMP协议
	if ((sock_icmp = socket(PF_INET, SOCK_RAW, protocol->p_proto/*IPPROTO_ICMP*/)) < 0)
	{
		perror("socket()");
		exit(EXIT_FAILURE);
	}

Unix/Linux的一个哲学是:一切皆文件。socket也不例外,它就是可读/可写/可控制/可关闭的文件描述符。下面的socket调用可创建一个socket:

#include <sys/types.h>

#include <sys/socket.h>

int socket(int domain, int type, int protocol);

domain参数告诉系统使用哪个底层协议族。对于TCP/IP协议族而言,该参数应该设置为PF_INET(用于IPv4)或PF_INET6(用于IPv6);type参数指定服务类型,进一步确定通信特征,这里选择SOCK_RAW,表示IP协议的数据报接口。参数protocol通常为0,表示为给指定的域和套接字选择默认协议。这里直接选择ICMP协议。表示ICMP协议的编号为IPROTO_ICMP,这里可以直接用它,或者通过一下这种方式获取:

struct protoent *protocol;

	if ((protocol = getprotobyname("icmp")) == NULL)
	{
		perror("getprotobyname");
		exit(EXIT_FAILURE);
	}
getprotobyname()是一个函数,返回对应于给定协议名的相关协议信息。此函数可以将协议名字映射为协议编号。

struct protoent结构至少包含以下成员:

stuct protoent
{
    char *p_name;
    char **p_aliases;
    int p_proto;  //协议编号
};
报文发送方法
void SendPacket(int sock_icmp, struct sockaddr_in *dest_addr)
{
	int nSend = 0;

	while (nSend < SEND_NUM)
	{
		SetICMP(nSend + 1);
		nSend++;
		if (sendto(sock_icmp, SendBuffer, ICMP_LEN, 0,
			(struct sockaddr *)dest_addr, sizeof(struct sockaddr_in)) < 0)
		{
			perror("sendto");
		}
		sleep(1);
	}
}

sendto函数原型:

ssize_t  sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);

sendto往sockfd上写入数据,buf和len参数分别指定写缓冲区的位置和大小。dest_addr参数指定接收端的socket地址,addrlen参数1则指定该地址的长度。flag参数为数据收发提供了额外控制,通常为0.send成功时返回实际写入的数据的长度,失败则返回-1并设置errno。

dest_addr类型为专用socket地址,所有专用socket地址类型的变量在实际使用是都需要转化为通用socket地址类型sockaddr(强制转换即可),因为所有scket编程接口使用的地址参数的类型都是sockaddr。

【第六天】

接收方法与拆包访法

int unpack(struct timeval *RecvTime)
{
	struct ip *Ip = (struct ip *)RecvBuffer;
	struct icmp *Icmp;
	int ipHeadLen;
	double rtt;

	ipHeadLen = Ip->ip_hl << 2;	//ip_hl字段单位为4字节
	Icmp = (struct icmp *)(RecvBuffer + ipHeadLen);
	
	if ((Icmp->icmp_type == ICMP_ECHOREPLY) && Icmp->icmp_id == getpid())
	{
		struct timeval *SendTime = (struct timeval *)Icmp->icmp_data;
		GetRtt(RecvTime, SendTime);	
		rtt = RecvTime->tv_sec * 1000.0 + RecvTime->tv_usec / 1000.0; //单位毫秒
		
		printf("%u bytes from %s: icmp_seq=%u ttl=%u time=%.3f ms\n",
			ntohs(Ip->ip_len) - ipHeadLen,
			inet_ntoa(Ip->ip_src),
			Icmp->icmp_seq,
			Ip->ip_ttl,
			rtt);
		return 0;
	}
	else
		return -1;
}

void GetRtt(struct timeval *RecvTime, struct timeval *SendTime)
{
    if ((RecvTime->tv_usec -= SendTime->tv_usec) < 0)
    {
        --(RecvTime->tv_sec);
        RecvTime->tv_usec += 1000000;
    }
    RecvTime->tv_sec -= SendTime->tv_sec;
}

void RecvePacket(int sock_icmp, struct sockaddr_in *dest_addr)
{
	int nRecv = 0;
	int RecvBytes = 0;
	int addrlen = sizeof(struct sockaddr_in);
	struct timeval RecvTime;
	
	while (nRecv < SEND_NUM)
	{
		if ((RecvBytes = recvfrom(sock_icmp, RecvBuffer, RECV_BUFFER_SIZE,
			0, (struct sockaddr *)dest_addr, &addrlen)) < 0)
		{
			perror("recvfrom");
		}
		//printf("nRecv=%d\n", RecvBytes);
		gettimeofday(&RecvTime, NULL);
		if (unpack(&RecvTime) == -1)	//接收到的报文并非所发报文的回应
			continue;
		nRecv++;
	}
}
好不容易写出这几个函数,但是目前还有点小问题,后边继续修改。先不管这些。

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr* src_addr, sockklen_t* addrlen);

recvfrom读取sockfd上的数据,buf和len参数分别指定度缓冲区的位置和大小。ICMP数据报是封装IP报文里发送的,IP协议是无连接的,不可靠的协议。所以每次读取数据都要获取发送端的socket地址,即参数src_addr所指的内容,addlen参数则指定该地址的长度。返回值:返回接收数据的字节长度;若无可用数据或对等方已经按序结束,返回0;若出错,返回-1.

当调用recvfrom时,需要设置addlen参数指向一个整数,该整数包含addr所指向的套接字缓冲区的字节长度。返回时,该整数设为该地址的实际字节长度。

GetRtt用来获取两个时间戳的差,结果保存在第一个参数所指空间处。

unpack用来剥去IP和ICMP报头。

【第七天】

其实这不是真正的第七天了,只是有些时候比较忙,没来得及写博客。到昨天为止,整个框架已经一目了然了,无非是一些细节上的修改以及最后的统计信息。

在下边要贴上的源码基本就是最终版了,和前面的细节处有些许不同也是正常,是因为我做了修改,为了统计信息方便。比如统计信息中的avg,min,max,mdev几个时间的计算,是我一开始没有考虑到的,只能后面修改。至于前面已经写好的博文也没必要改了。如果还有后续的修改,我会更新在博客最后的源程序中,也会上传到github,文章末尾会附上本程序在github中的链接。

总共的源文件有三个,分别是:main.c, ping.c, ping.h。,头文件和公用的接口都放在ping.h中,函数的实现在ping.c中,main.c中负责调用已有的接口,搭建程序的骨架,将这些函数拼接起来。

ping.h

#ifndef __PING_H__
#define __PING_H__

#include <stdio.h>
#include <sys/time.h>
#include <netdb.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <netinet/ip_icmp.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>
#include <signal.h>
#include <math.h>

#define ICMP_DATA_LEN 56		//ICMP默认数据长度
#define ICMP_HEAD_LEN 8			//ICMP默认头部长度
#define ICMP_LEN  (ICMP_DATA_LEN + ICMP_HEAD_LEN)
#define SEND_BUFFER_SIZE 128		//发送缓冲区大小
#define RECV_BUFFER_SIZE 128		//接收缓冲区大小
#define SEND_NUM 100 			//发送报文数
#define MAX_WAIT_TIME 3

extern struct hostent *pHost;
extern int sock_icmp;
extern int nSend;
extern char *IP;

//发送ICMP报文
void SendPacket(int sock_icmp, struct sockaddr_in *dest_addr, int nSend);

//接收ICMP报文
int RecvePacket(int sock_icmp, struct sockaddr_in *dest_addr);

//计算校验和
u_int16_t Compute_cksum(struct icmp *pIcmp);

//设置ICMP报文
void SetICMP(u_int16_t seq);

//剥去报头
int unpack(struct timeval *RecvTime);

//计算往返时间
double GetRtt(struct timeval *RecvTime, struct timeval *SendTime);

//统计信息
void Statistics(int signo);


#endif	//__PING_H__
 

ping.c
 
 

#include "ping.h"

#define WAIT_TIME 5

char SendBuffer[SEND_BUFFER_SIZE];
char RecvBuffer[RECV_BUFFER_SIZE];
int nRecv = 0;	//实际接收到的报文数
struct timeval FirstSendTime;	//用以计算总的时间
struct timeval LastRecvTime;
double min = 0.0;
double avg = 0.0;
double max = 0.0;
double mdev = 0.0;

u_int16_t Compute_cksum(struct icmp *pIcmp)
{
	u_int16_t *data = (u_int16_t *)pIcmp;
	int len = ICMP_LEN;
	u_int32_t sum = 0;
	
	while (len > 1)
	{
		sum += *data++;
		len -= 2;
	}
	if (1 == len)
	{
		u_int16_t tmp = *data;
		tmp &= 0xff00;
		sum += tmp;
	}

	//ICMP校验和带进位
	while (sum >> 16)
		sum = (sum >> 16) + (sum & 0x0000ffff);
	sum = ~sum;
	
	return sum;
}

void SetICMP(u_int16_t seq)
{
	struct icmp *pIcmp;
	struct timeval *pTime;

	pIcmp = (struct icmp*)SendBuffer;
	
	/* 类型和代码分别为ICMP_ECHO,0代表请求回送 */
	pIcmp->icmp_type = ICMP_ECHO;
	pIcmp->icmp_code = 0;
	pIcmp->icmp_cksum = 0;		//校验和
	pIcmp->icmp_seq = seq;		//序号
	pIcmp->icmp_id = getpid();	//取进程号作为标志
	pTime = (struct timeval *)pIcmp->icmp_data;
	gettimeofday(pTime, NULL);	//数据段存放发送时间
	pIcmp->icmp_cksum = Compute_cksum(pIcmp);
	
	if (1 == seq)
		FirstSendTime = *pTime;
}

void SendPacket(int sock_icmp, struct sockaddr_in *dest_addr, int nSend)
{
	SetICMP(nSend);
	if (sendto(sock_icmp, SendBuffer, ICMP_LEN, 0,
		(struct sockaddr *)dest_addr, sizeof(struct sockaddr_in)) < 0)
	{
		perror("sendto");
		return;
	}
}

double GetRtt(struct timeval *RecvTime, struct timeval *SendTime)
{
	struct timeval sub = *RecvTime;

	if ((sub.tv_usec -= SendTime->tv_usec) < 0)
	{
		--(sub.tv_sec);
		sub.tv_usec += 1000000;
	}
	sub.tv_sec -= SendTime->tv_sec;
	
	return sub.tv_sec * 1000.0 + sub.tv_usec / 1000.0; //转换单位为毫秒
}

int unpack(struct timeval *RecvTime)
{
	struct ip *Ip = (struct ip *)RecvBuffer;
	struct icmp *Icmp;
	int ipHeadLen;
	double rtt;

	ipHeadLen = Ip->ip_hl << 2;	//ip_hl字段单位为4字节
	Icmp = (struct icmp *)(RecvBuffer + ipHeadLen);

	//判断接收到的报文是否是自己所发报文的响应
	if ((Icmp->icmp_type == ICMP_ECHOREPLY) && Icmp->icmp_id == getpid())
	{
		struct timeval *SendTime = (struct timeval *)Icmp->icmp_data;
		rtt = GetRtt(RecvTime, SendTime);
			
		printf("%u bytes from %s: icmp_seq=%u ttl=%u time=%.1f ms\n",
			ntohs(Ip->ip_len) - ipHeadLen,
			inet_ntoa(Ip->ip_src),
			Icmp->icmp_seq,
			Ip->ip_ttl,
			rtt);
		
		if (rtt < min || 0 == min)
			min = rtt;
		if (rtt > max)
			max = rtt;
		avg += rtt;
		mdev += rtt * rtt;
		
		return 0;
	}
		
	return -1;
}


void Statistics(int signo)
{
	double tmp;
	avg /= nRecv;
	tmp = mdev / nRecv - avg * avg;
	mdev = sqrt(tmp);
	
	if (NULL != pHost)
		printf("--- %s  ping statistics ---\n", pHost->h_name);
	else
		printf("--- %s  ping statistics ---\n", IP);
		
	printf("%d packets transmitted, %d received, %d%% packet loss, time %dms\n"
		, nSend
		, nRecv
		, (nSend - nRecv) / nSend * 100
		, (int)GetRtt(&LastRecvTime, &FirstSendTime));
	printf("rtt min/avg/max/mdev = %.3f/%.3f/%.3f/%.3f ms\n",
		min, avg, max, mdev);
	
	close(sock_icmp);
	exit(0);
}

int RecvePacket(int sock_icmp, struct sockaddr_in *dest_addr)
{
	int RecvBytes = 0;
	int addrlen = sizeof(struct sockaddr_in);
	struct timeval RecvTime;

	signal(SIGALRM, Statistics);
	alarm(WAIT_TIME);
	if ((RecvBytes = recvfrom(sock_icmp, RecvBuffer, RECV_BUFFER_SIZE,
			0, (struct sockaddr *)dest_addr, &addrlen)) < 0)
	{
		perror("recvfrom");
		return 0;
	}
	//printf("nRecv=%d\n", RecvBytes);
	gettimeofday(&RecvTime, NULL);
	LastRecvTime = RecvTime;

	if (unpack(&RecvTime) == -1)
	{
		return -1; 
	}
	nRecv++;
}
 

main.c

#include "ping.h"


struct hostent * pHost = NULL;		//保存主机信息
int sock_icmp;				//icmp套接字
int nSend = 1;
char *IP = NULL;

void Call(int argc, char *argv[])
{

	struct protoent *protocol;
	struct sockaddr_in dest_addr; 	//IPv4专用socket地址,保存目的地址

	in_addr_t inaddr;		//ip地址(网络字节序)

	if (argc < 2)
	{
		printf("Usage: %s [hostname/IP address]\n", argv[0]);
		exit(EXIT_FAILURE);	
	}

	if ((protocol = getprotobyname("icmp")) == NULL)
	{
		perror("getprotobyname");
		exit(EXIT_FAILURE);
	}

	/* 创建ICMP套接字 */
	//AF_INET:IPv4, SOCK_RAW:IP协议数据报接口, IPPROTO_ICMP:ICMP协议
	if ((sock_icmp = socket(PF_INET, SOCK_RAW, protocol->p_proto/*IPPROTO_ICMP*/)) < 0)
	{
		perror("socket");
		exit(EXIT_FAILURE);
	}
	dest_addr.sin_family = AF_INET;

	/* 将点分十进制ip地址转换为网络字节序 */
	if ((inaddr = inet_addr(argv[1])) == INADDR_NONE)
	{
		/* 转换失败,表明是主机名,需通过主机名获取ip */
		if ((pHost = gethostbyname(argv[1])) == NULL)
		{
			herror("gethostbyname()");
			exit(EXIT_FAILURE);
		}
		memmove(&dest_addr.sin_addr, pHost->h_addr_list[0], pHost->h_length);
	}
	else
	{
		memmove(&dest_addr.sin_addr, &inaddr, sizeof(struct in_addr));
	}

	if (NULL != pHost)
		printf("PING %s", pHost->h_name);
	else
		printf("PING %s", argv[1]);
	printf("(%s) %d bytes of data.\n", inet_ntoa(dest_addr.sin_addr), ICMP_LEN);

	IP = argv[1];
	signal(SIGINT, Statistics);
	while (nSend < SEND_NUM)
	{
		int unpack_ret;
		
		SendPacket(sock_icmp, &dest_addr, nSend);
		
		unpack_ret = RecvePacket(sock_icmp, &dest_addr);
		if (-1 == unpack_ret)	//(ping回环时)收到了自己发出的报文,重新等待接收
			RecvePacket(sock_icmp, &dest_addr);
			

		sleep(1);
		nSend++;
	}
	
	Statistics(0);	//输出信息,关闭套接字
}

int main(int argc, char *argv[])
{
	Call(argc, argv);

	return 0;
}

统计信息中mdev的计算方法

在运行 ping 命令的时候,里面有一项输出叫 mdev:

它是什么意思呢? ping 的手册中并没有提到。我们不妨看一下 ping 的源代码,见 ping_common.c:

tsum += triptime;
tsum2 += (long long)triptime * (long long)triptime

以及

tsum /= nreceived + nrepeats;
tsum2 /= nreceived + nrepeats;
tmdev = llsqrt(tsum2 – tsum * tsum);

所以我们可以得出:

mdev = SQRT(SUM(RTT*RTT) / N – (SUM(RTT)/N)^2)

也就是这个平均偏差的公式:

                                                    

所以 mdev 就是 Mean Deviation 的缩写,它表示这些 ICMP 包的 RTT 偏离平均值的程度,这个值越大说明你的网速越不稳定。

测试

因为创建这个套接字需要有root权限,所以运行的时候只能这样了(不过我还会继续优化)

          

GitHub链接:https://github.com/Fireplusplus/Project/tree/master/Ping

  • 83
    点赞
  • 257
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 13
    评论
Ping命令是一种网络诊断工具,可以检查网络主机是否可达,以及检测主机之间的网络延迟。为了实现Ping命令,我们需要使用C语言编写一个程序,该程序利用Internet控制消息协议(ICMP)协议发送数据包,并接收目标主机返回的响应,以计算网络延迟。 要使用C语言实现Ping命令,需要遵循以下步骤: 1. 创建原始套接字 使用socket()函数创建原始套接字。原始套接字可以直接使用ICMP协议进行通信。 2. 填写ICMP协议头 ICMP协议头中包含了一些必要的信息,例如ICMP类型、ICMP编码和校验和等。 3. 填写数据部分 Ping命令通常会发送一些生产数据,例如时间戳或序列号。 4. 计算校验和 在ICMP协议头填写完毕后,需要计算校验和。校验和用于检验数据在传输过程中是否被篡改。 5. 发送请求 使用sendto()函数发送ICMP数据包,以请求目标主机进行响应。 6. 接收响应 使用recvfrom()函数接收目标主机的响应数据包。 7. 解析响应 根据ICMP协议头中的类型和编码信息,判断响应是来自目标主机的响应,还是一些异常情况的提示信息。如果是目标主机的响应,则从数据部分解析出需要的信息(例如网络延迟),并打印在控制台上。 以上就是使用C语言实现Ping命令的步骤。需要注意的是,由于Ping命令需要使用超级用户权限运行,因此我们需要将编写好的程序设置为setuid程序,以获取高级别用户权限。同时,由于Ping命令是一个常见的网络诊断工具,因此我们还需要对程序进行性能和安全性优化,以便更好地服务于用户。
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fireplusplus

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值