关于ping程序

本文深入探讨了ping命令的原理与实现细节,重点介绍了ICMP协议在ping过程中的作用,包括TTL值的概念、ICMP报文的组成与用途,以及ping程序的主要流程。此外,还详细阐述了与ping程序相关的结构体与函数接口,如sockaddr_in、socket、sendto、recvfrom等,帮助读者全面理解网络编程中ping操作的底层实现。

    关于网络编程,知之甚少,linux环境下编程的经验也比较缺乏,于是乎,在百度文库上下载了一个关于ping的程序设计,照着将代码敲打一遍,顺便熟悉某些东东。 敲完代码,运行时,发现错误极多,正好也试着学习用GDB调试。下面是一些琐碎的知识点,写写加深印象。

    1. 关于ping程序

        用于确定本地主机与网络中其它主机的网络通信情况,常使用ping程序。ping程序向指定的IP地址发送ICMP数据包,通过返回的信息来判断网络的连接状况。

        ping程序的返回信息中有一个值为TTL(time to live),表示ping程序发送的icmp数据包的生存周期,每经过一个网段,TTL的值减1,当其值被减为0时,该数据包将被丢弃,但该数据包的源地址将会被告知情况,以重新发送该数据包。另外,不同操作系统的TTL值是不相同的,linux操作系统是64。

    2. ICMP协议

        ICMP(Internet Control Message Protocol),即网际控制报文协议,可用在网络中实现主机探测、路由维护、路由选择和流量控制。

        由于IP协议没有机制来获取网络错误信息以及没有对错误进行处理,所以需要另一协议来解决这一问题,这个协议就是ICMP协议。ICMP常被认为是IP层的一部分,用于传输差错报文及控制报文。ICMP报文是封装在IP数据报内部的。

        关于ICMP协议的更多内容,请参考上篇文章《ICMP协议》。

    3. ping程序主要流程

        创建通信的套接字-->将地址、端口信息与套接字绑定-->构建IP包头与ICMP包头-->发送构建的数据包-->接收对方主机的回应-->给出程序反馈信息

    4. 涉及的一及结构体及函数接口简介

        (1) struct sockaddr_in :此数据结构用做bind、connect、recvfrom、sendto等函数的参数,指明地址信息。

 

            sa_family是地址家族,一般都是“AF_xxx”的形式。通常大多用的是都是AF_INET,代表TCP/IP协议族;

            sin_port存储端口号(使用网络字节顺序);

            sin_addr存储IP地址,使用in_addr这个数据结构

            sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节;

        (2) struct protoent :相关函数: getprotobyname, getprotoent, setprotoent, endprotoent

             p_name: 网络协议名             p_aliases: 别名             p_proto: 网络协议编号

        (3) struct hostent :hostent是host entry的缩写,该结构记录主机的信息,包括主机名、别名、地址类型、地址长度和地址列表

            注:其所需要的头文件还有这一宏定义 #define h_addr h_addr_list[0]

        (4) struct imcp 

        (5) getprotobyname(const char *protoname) 

              getprotobyname()会返回一个protoent结构,参数protoname为欲查询的网络协议名。此函数会从 /etc/protocols中查找符合条件的数据并由结构protoent返回。

        (6) gethostbyname()

              gethostbyname()返回对应于给定主机名的包含主机名字和地址信息的hostent结构指针。

        (7) socket :函数原型 int socket(int domain, int type, int protocol); 

              第一个参数指定应用程序使用的通信协议的协议族,对于TCP/IP协议族,该参数置AF_INET;

              第二个参数指定要创建的套接字类型,流套接字类型为SOCK_STREAM、数据报套接字类型为SOCK_DGRAM、原始套接字SOCK_RAW;

              第三个参数指定应用程序所使用的通信协议;

              该函数如果调用成功就返回新创建的套接字的描述符,如果失败就返回INVALID_SOCKET。套接字描述符是一个整数类型的值。每个进程的进程空间里都有一个套接字描述符表,该表中存放着套接字描述符和套接字数据结构的对应关系。该套接字描述符表中有一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据结构的地址,因此根据套接字描述符就可以找到其对应的套接字数据结构,另外需知套接字数据结构是在操作系统的内核缓冲里的。

        (8) setsockopt():用于任意类型、任意状态套接口的设置选项值

            函数原型:int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

            sockfd:标识一个套接口的描述字
      level:选项定义的层次;支持SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP和IPPROTO_IPV6
      optname:需设置的选项
      optval:指针,指向存放选项值的缓冲区
      optlen:optval缓冲区长度

        (9) inet_addr():将一个点分十进制的IP转换成一个长整数型数

        (10) sendto():经socket传送数据

       函数原型:int sendto ( socket s , const void * msg, int len, unsigned int flags, const struct sockaddr * to , int tolen);

             函数说明:sendto() 用来将数据由指定的socket传给对方主机。

                     参数s为已建好连线的socket,如果利用UDP协议则不需经过连线操作;

                     参数msg指向欲连线的数据内容,参数flags 一般设0,详细描述请参考send();

                     参数to用来指定欲传送的网络地址,结构sockaddr请参考bind();

                     参数tolen为sockaddr的结果长度;

             返回值:成功则返回实际传送出去的字符数,失败返回-1,错误原因存于errno 中。

        (11) recvfrom():经socket接收数据

            函数原型:ssize_t recvfrom(int sockfd,void *buf,int len,unsigned int flags, struct sockaddr *from,socket_t *fromlen);ssize_t 相当于 int,socket_t 相当于int ,这里用这个名字为的是提高代码的自说明性。

            参数说明:
              s:标识一个已连接套接口的描述字
              buf:接收数据缓冲区
              len:缓冲区长度
              flags:调用操作方式 
              from:(可选)指针,指向装有源地址的缓冲区
              fromlen:(可选)指针,指向from缓冲区长度值
      返回值:如果正确接收返回接收到的字节数,失败返回0。

    5. 简单的ping程序,参考自百度文库

 

#include <stdio.h>		//perror()
#include <stdlib.h>
#include <netdb.h>		//getprotobynumber  gethostbyname
#include <unistd.h>		//setuid() getuid() sleep() alarm()
#include <sys/types.h>	//getuid()
#include <string.h>		//bzero(s, n) 将s的前n个字节设为0
#include <signal.h>		//signal()
#include <time.h>		//gettimeofday()
#include <netinet/ip_icmp.h>
#include <errno.h>		//EINTR
#include <arpa/inet.h>	//inet_ntoa()

#define MAX_NO_PACKETS	10
#define PACKET_SIZE		4096
#define MAX_WAIT_TIME	5

int sockfd;
int datalen = 56;
struct sockaddr_in dest_addr;
struct sockaddr_in from;
pid_t pid;//获取进程号

int nsend = 0, nreceived = 0;

char sendpacket[PACKET_SIZE];
char recvpacket[PACKET_SIZE];

struct timeval tvrecv;

void statistics();				//显示发送数据包的总结信息
void send_packet(int count);	//发送ICMP报文
void recv_packet(int count);	//接收所有ICMP报文

int main(int argc, char** argv)
{
	struct protoent* protocol;
	int size = 1024*50;
	unsigned int inaddr = 0l;//0L
	struct hostent *host;

	if (argc < 3)
	{
		printf("usage:%s hostname/IP address or usage:%s hostname/IP address -r\n", argv[0], argv[0]);
		exit(1);
	}

	//getprotobyname()会返回一个protoent结构,参数proto为欲查询的网络协议名。
	//此函数会从 /etc/protocols中查找符合条件的数据并由结构protoent返回
	if ((protocol=getprotobyname("icmp")) == NULL)
	{
		perror("getprotobyname");
		exit(1);
	}

	//生成使用ICMP的原始套接字,这种套接字只有root用户才能生成
	if ((sockfd=socket(AF_INET, SOCK_RAW, protocol->p_proto)) < 0)
	{
		perror("socket error");
		exit(1);
	}

	//setuid(getuid()); //回收root权限,设置当前用户权限

	//扩大套接字接收缓冲区到50K这样做主要为了减小接收缓冲区溢出的的可能性,若无意中ping一个广播地址或多播地址,将会引来大量应答
	//SO_RCVBUF int 为接收确定缓冲区大小
	//选项定义的层次,目前仅支持SOL_SOCKET和IPPROTO_TCP层次。
	setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));

	bzero(&dest_addr, sizeof(dest_addr));

	//关于sockaddr_in为成员
	//sa_family是地址家族,一般都是“AF_xxx”的形式。通常大多用的是都是AF_INET,代表TCP/IP协议族。
	//sin_port存储端口号(使用网络字节顺序)
	//sin_addr存储IP地址,使用in_addr这个数据结构
	dest_addr.sin_family = AF_INET;	

	inaddr = inet_addr(argv[2]);//将一个点分十进制的IP转换成一个长整数型数

	//判断是主机名还是ip地址
	if (inaddr == INADDR_NONE)
	{
		if ((host=gethostbyname(argv[2])) == NULL)
		{
			perror("gethostbyname error");
			exit(1);
		}
		//是主机地址
		memcpy((char*)&dest_addr.sin_addr, host->h_addr, sizeof(dest_addr.sin_addr));//没有h_addr这个成员,但是h_addr表示h_addr_list的第一个地址,因为#define h_addr h_addr_list[0]
	}
	else//是ip地址
	{
		memcpy((char *)&dest_addr.sin_addr, (char *)&inaddr, sizeof(inaddr)); 
	}

	//获取main的进程id,用于设置icmp的标志符
	pid = getpid();

	printf("PING %s(%s): %d bytes data in ICMP packets.\n", argv[2], inet_ntoa(dest_addr.sin_addr), datalen);

	//设置某一信号的对应动作
	//SIGINT:由Interrupt Key产生,通常是CTRL+C或者DELETE。发送给所有ForeGround Group的进程
	signal(SIGINT, statistics);

	if (argc == 3)
	{
		send_packet(5);
		recv_packet(5);
		statistics();
	}
	else
	{
		printf("input error, pls check it...\n");
	}
	return 0;
}

/***********************************************************************/
//FUNCTION:校验和算法
unsigned short cal_chksum(unsigned short *addr, int len)
{
	int nleft = len;
	int sum = 0;
	unsigned short *w = addr;
	unsigned short answer = 0;

	//把ICMP报头二进制数据以2字节为单位累加起来
	while (nleft > 1)
	{
		sum += *w++;
		nleft -= 2;
	}

	//若ICMP报头为奇数个字节,会剩下最后一字节。把最后一个字节视为一个2字节数据的高字节,
	//这个2字节数据的低字节为0,继续累加
	if (nleft == 1)
	{
		*(unsigned char *)(&answer) = *(unsigned char *)w;
		sum += answer;
	}

	sum = (sum>>16) + (sum&0xffff);
	sum += (sum>>16);
	answer = ~sum;

	return answer;
}

/***********************************************************************/
//FUNCTION:两个timeval结构相减
void tv_sub(struct timeval *out,struct timeval *in)
{
	if ((out->tv_usec-=in->tv_usec) < 0)
	{
		--out->tv_sec;
		out->tv_usec += 1000000;
	}

	out->tv_sec -= in->tv_sec;
}

/***********************************************************************/
//FUNCTION:设置ICMP报头
int pack(int pack_no)
{
	int i, packsize;
	struct icmp *icmp;
	struct timeval *tval;

	icmp = (struct icmp *)sendpacket;
	icmp->icmp_type = ICMP_ECHO;
	icmp->icmp_code = 0;
	icmp->icmp_cksum = 0;
	icmp->icmp_seq = pack_no;
	icmp->icmp_id = pid;

	packsize = datalen + 8;
	tval = (struct timeval *)icmp->icmp_data;
	gettimeofday(tval, NULL);	//记录发送时间

	icmp->icmp_cksum = cal_chksum((unsigned short *)icmp, packsize);

	return packsize;
}

/***********************************************************************/
//FUNCTION:剥去ICMP报头加显示
int unpack(char *buf, int len)
{
	int i, iphdrlen;
	struct ip *ip;
	struct icmp *icmp;
	struct timeval *tvsend;
	double rtt;

	ip = (struct ip *)buf;
	iphdrlen = ip->ip_hl<<2;//求ip报头长度,即ip报头的长度标志乘4
	icmp = (struct icmp *)(buf+iphdrlen);//越过ip报头,指向ICMP报头
	
	len -= iphdrlen;	//ICMP报头及ICMP数据报的总长度
	if (len < 8)		//小于ICMP报头长度则不合理
	{
		printf("ICMP packets\'s length is less than 8\n");
		return -1;
	}

	//确保所接收的是我所发的的ICMP的回应
	if ((icmp->icmp_type == ICMP_ECHOREPLY) && (icmp->icmp_id == pid))
	{
		tvsend = (struct timeval *)icmp->icmp_data;
		tv_sub(&tvrecv, tvsend);//接收和发送的时间差

		rtt = tvrecv.tv_sec*1000 + tvrecv.tv_usec/1000;//以毫秒单位计算rtt
		
		printf("%d byte from %s: icmp_seq=%u ttl=%d rtt=%.3f ms\n", len, inet_ntoa(from.sin_addr), icmp->icmp_seq, ip->ip_ttl, rtt);
	}
	else
		return -1;
}

/***********************************************************************/
//FUNCTION:
void statistics()
{
	printf("\n------------------------ping statistics------------------------\n");
	printf("%d packets transmitted, %d received, %.2f%% packets lost\n", nsend, nreceived, (float)(nsend-nreceived)/(float)nsend*100);
	printf("---------------------------------------------------------------\n");
	close(sockfd);
	exit(1);
}

/***********************************************************************/
//FUNCTION:
void send_packet(int count)
{
	int packetsize;

	while (nsend < MAX_NO_PACKETS)//发送MAX_NO_PACKETS个报文
	{
		nsend++;
		packetsize = pack(nsend);

		//sendpacket为要发送的内容,由pack()函数设定,dest_addr是目的地址
		if (sendto(sockfd, sendpacket, packetsize, 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr)) < 0)
		{
			perror("sendto error");
			continue;
		}
		//sleep(1);//每隔一秒发送一个ICMP报文
	}
}

/***********************************************************************/
//FUNCTION:接收所有ICMP报文
void recv_packet(int count)
{
	int n, fromlen;
	extern int errno;

	//用alarm函数设置的timer超时或setitimer函数设置的interval timer超时
	signal(SIGALRM, statistics);

	fromlen = sizeof(from);
	while (nreceived < nsend)
	{
		//alarm()用来设置信号SIGALRM在经过参数seconds指定的秒数后传送给目前的进程
		alarm(MAX_WAIT_TIME);

		//(经socket接收数据
		if ((n=recvfrom(sockfd, recvpacket, sizeof(recvpacket), 0, (struct sockaddr *)&from, &fromlen)) < 0)
		{
			if (errno == EINTR)
				continue;
			perror("recvfrom error");
			continue;
		}

		gettimeofday(&tvrecv, NULL);//记录接收时间

		if (unpack(recvpacket, n) == -1)
			continue;

		nreceived++;
	}
}

    6.程序结果
(1) ping 127.0.0.1

 

 

 

(2) ping ubuntu

(3) ping 192.168.18.124

(4) ping www.baidu.com

(5) ping www.sina.com

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值