ping命令的C语言实现(linux, IPv4,简单版)

这个程序主要运用了ICMPv4协议(回显请求)来测试本机到某服务器的网络是否连通,因为其中用到了原始套接字,所以运行该程序需要管理员权限。

PS:本程序只支持一种输入方式:./myping <hostname>,不支持其他参数。

思路:
1:根据hostname参数创建原始套接字。
2:每隔1秒钟向服务器发送一个ICMP回显请求。
3:循环接收从服务器返回的应答并处理其数据。

上代码:
#include <signal.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>

#include <netinet/in_systm.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <sys/un.h>

//各种缓冲区的长度
#define BUFSIZE 1500
//ICMP回显请求的长度
#define  DATA_LEN 56 

struct proto 
{
	struct sockaddr *sasend; /* sockaddr{} for send, from getaddrinfo */
	struct sockaddr *sarecv; /* sockaddr{} for receiving */
	socklen_t salen; /* length of sockaddr{}s */
	int icmpproto; /* IPPROTO_xxx value for ICMP */
};

//全局变量
pid_t g_pid;
int g_sockfd;
struct proto g_proto = { NULL, NULL, 0, IPPROTO_ICMP };

//处理服务器返回的ICMP回显信息
void proc_msg(char *, ssize_t, struct msghdr *, struct timeval *);

//发送ICMP回显请求
void send_msg(void);

//循环发送、接收信息
void readloop(void);

//定时器入门函数,每隔一秒一次发送ICMP请求
void sig_alrm(int);

//计算两个时间之间的间隔
void tv_sub(struct timeval *, struct timeval *);

//获取服务器的地址等信息
struct addrinfo *host_serv(const char *host, 
	const char *serv, int family, int socktype);

//根据服务器信息,得到服务器的IP地址
char *sock_ntop_host(const struct sockaddr *sa, socklen_t salen);

//计算校验和
uint16_t in_cksum(uint16_t *addr, int len);

//输出错误信息,退出程序
void error_quit(const char *str);


int main(int argc, char **argv)
{ 
	int c;
	struct addrinfo *ai;
	struct sockaddr_in *sin;
	char *ip_address;
	char *host;

	//本程序只支持一种输入方式:./myping <hostname>
	if( argc != 2 )
		error_quit("usage: myping <hostname>");

	host = argv[1];
	//将pid的高二位全置为0,ICMP的ID只有16位
	g_pid = getpid() & 0xffff; 

	//设置定时器,每秒钟向服务器发送一次请求
	signal(SIGALRM, sig_alrm);

	//获取服务器的信息(addrinfo结构)
	ai = host_serv(host, NULL, 0, 0);
	ip_address = sock_ntop_host(ai->ai_addr, ai->ai_addrlen);

	printf("PING %s (%s): %d data bytes\n",
		ai->ai_canonname ? ai->ai_canonname : ip_address,
		ip_address, DATA_LEN);

	//如果返回的协议簇不是AF_INET(IPv4),则退出
	if ( ai->ai_family != AF_INET )
		error_quit("unknown address family");

	//设置proto结构体
	g_proto.sasend = ai->ai_addr;
	g_proto.sarecv = calloc(1, ai->ai_addrlen);
	g_proto.salen = ai->ai_addrlen;

	//开始循环发送/接收请求
	readloop();

	return 0;
}

void readloop(void)
{
	int size;
	char recvbuf[BUFSIZE];
	char controlbuf[BUFSIZE];
	struct msghdr msg;
	struct iovec iov;
	ssize_t n;
	struct timeval tval;

	//创建一个IPv4的原始套接字
	g_sockfd = socket(g_proto.sasend->sa_family, SOCK_RAW, g_proto.icmpproto);
	if( -1 == g_sockfd )
		error_quit("socket error");

	//放弃管理员权限
	//这个程序中,只用创建原始套接字时需要管理员权限
	setuid(getuid());

	//设置socket的接收缓冲区
	size = 60 * 1024;
	setsockopt(g_sockfd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));

	//发出第一个请求
	sig_alrm(SIGALRM);

	//为recvmsg调用设置msghdr结构
	iov.iov_base = recvbuf;
	iov.iov_len = sizeof(recvbuf);
	msg.msg_name = g_proto.sarecv;
	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;
	msg.msg_control = controlbuf;

	//开始死循环,不断读取和处理从服务器中返回的信息
	while( 1 )
	{
		msg.msg_namelen = g_proto.salen;
		msg.msg_controllen = sizeof(controlbuf);
		n = recvmsg(g_sockfd, &msg, 0);
		if (n < 0)
		{
			if (errno == EINTR)
				continue;
			else
				error_quit("recvmsg error");
		}

		//分析返回内容,产生输出
		gettimeofday(&tval, NULL);
		proc_msg(recvbuf, n, &msg, &tval);
	}
}

void proc_msg(char *ptr, ssize_t len, struct msghdr *msg, struct timeval *tvrecv)
{
	int hlen1, icmplen;
	double rtt;
	struct ip *ip;
	struct icmp *icmp;
	struct timeval *tvsend;

	//将服务器返回的字符串强转为ip结构
	ip = (struct ip *) ptr; 

	//得到IP表头的长度
	hlen1 = ip->ip_hl << 2; 

	//如果不是ICMP的应答,则返回
	if (ip->ip_p != IPPROTO_ICMP)
		return;

	icmp = (struct icmp *) (ptr + hlen1); 
	//长度不足,不是合法应答
	if ( (icmplen = len - hlen1) < 8)
		return;

	//不是回显应答,返回
	if (icmp->icmp_type != ICMP_ECHOREPLY) 
		return;

	//不是我们发出请求的应答,返回
	if (icmp->icmp_id != g_pid)
		return; 
	//长度不足,非法应答
	if (icmplen < 16)
		return;

	//计算网络延时
	tvsend = (struct timeval *) icmp->icmp_data;
	tv_sub(tvrecv, tvsend);
	rtt = tvrecv->tv_sec * 1000.0 + tvrecv->tv_usec / 1000.0;

	//输出信息
	printf("%d bytes from %s: seq=%u, ttl=%d, rtt=%.3f ms\n",
		icmplen, sock_ntop_host(g_proto.sarecv, g_proto.salen),
		icmp->icmp_seq, ip->ip_ttl, rtt);

}


void send_msg(void)
{
	int len;
	int res;
	struct icmp *icmp;
	char sendbuf[BUFSIZE];
	static int nsent = 0;

	//根据ICMPv4协议来设置发送信息
	icmp = (struct icmp *) sendbuf;

	//ICMP回显请求
	icmp->icmp_type = ICMP_ECHO;
	icmp->icmp_code = 0;

	//ICMP标识符字段为本进程的PID
	icmp->icmp_id = g_pid;

	//ICMP序列号字段为不断递增的全局变量nsent
	icmp->icmp_seq = nsent++;

	//ICMP数据字段为当前时间截,空白部分填充0xa5
	memset(icmp->icmp_data, 0xa5, DATA_LEN);
	gettimeofday((struct timeval *)icmp->icmp_data, NULL);

	//计算并填充校验和
	len = 8 + DATA_LEN;
	icmp->icmp_cksum = 0;
	icmp->icmp_cksum = in_cksum((u_short *) icmp, len);

	//发送数据
	res = sendto(g_sockfd, sendbuf, len, 0, g_proto.sasend, g_proto.salen);
	if( -1 == res )
		error_quit("sendto error");
}


void sig_alrm(int signo)
{
	send_msg();
	alarm(1);
}

void tv_sub(struct timeval *out, struct timeval *in)
{
	//将两个时间相减,并把结果存入第一个参数中( out -= in )
	if ( (out->tv_usec -= in->tv_usec) < 0) 
	{ 
		--out->tv_sec;
		out->tv_usec += 1000000;
	}
	out->tv_sec -= in->tv_sec;
}

struct addrinfo *host_serv(const char *host, const char *serv, int family, int socktype)
{
	int n;
	struct addrinfo hints, *res;

	memset(&hints, 0, sizeof(struct addrinfo));
	hints.ai_flags = AI_CANONNAME;
	hints.ai_family = family; 
	hints.ai_socktype = socktype;

	n = getaddrinfo(host, serv, &hints, &res);
	if ( n != 0 )
		error_quit("getaddrinfo error");

	return res;
}

char *sock_ntop_host(const struct sockaddr *sa, socklen_t salen)
{
	static char str[128];
	struct sockaddr_in *sin = (struct sockaddr_in *) sa;

	//本程序只支持IPv4协议
	if( sa->sa_family != AF_INET )
		error_quit("sock_ntop_host: the type must be AF_INET");

	if (inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str)) == NULL)
		error_quit("inet_ntop error");

	return str;
}

//《UNIX网络编程》书上的源码
uint16_t in_cksum(uint16_t *addr, int len)
{
	int nleft = len;
	uint32_t sum = 0;
	uint16_t *w = addr;
	uint16_t answer = 0;

	/*
	* Our algorithm is simple, using a 32 bit accumulator (sum), we add
	* sequential 16 bit words to it, and at the end, fold back all the
	* carry bits from the top 16 bits into the lower 16 bits.
	*/
	while (nleft > 1) 
	{
		sum += *w++;
		nleft -= 2;
	}

	/* 4mop up an odd byte, if necessary */
	if (nleft == 1) {
		*(unsigned char *)(&answer) = *(unsigned char *)w ;
		sum += answer;
	}

	/* 4add back carry outs from top 16 bits to low 16 bits */
	sum = (sum >> 16) + (sum & 0xffff); /* add hi 16 to low 16 */
	sum += (sum >> 16); /* add carry */
	answer = ~sum; /* truncate to 16 bits */
	return(answer);
}

void error_quit(const char *str)
{
	//输出错误信息,退出程序
	fprintf(stderr, "%s", str);   
	if( errno != 0 )    
		fprintf(stderr, " : %s", strerror(errno));    
	fprintf(stderr, "\n");        
	exit(1); 
}


运行示例:
qch@LinuxMint ~/program/tcode $ gcc myping.c -o myping
qch@LinuxMint ~/program/tcode $ sudo ./myping www.baidu.com
PING www.a.shifen.com (115.239.210.26): 56 data bytes
64 bytes from 115.239.210.26: seq=0, ttl=128, rtt=31.272 ms
64 bytes from 115.239.210.26: seq=1, ttl=128, rtt=34.722 ms
64 bytes from 115.239.210.26: seq=2, ttl=128, rtt=30.822 ms
64 bytes from 115.239.210.26: seq=3, ttl=128, rtt=31.273 ms
64 bytes from 115.239.210.26: seq=4, ttl=128, rtt=29.995 ms
...........................

要在Linux下扩展C语言ping程序的功能,您可以按照以下步骤进行操作: 1. 打开名为pingC语言程序的源代码文件。您可以使用文本编辑器(如vi或nano)打开该文件。 2. 在程序中找到主函数(通常是`main`函数)的定义位置。 3. 在主函数之前,添加一个处理命令行参数的代码块。您可以使用`getopt`函数来处理命令行参数。在该代码块中,您可以检查是否传递了`-h`和`-b`选项,并相应地执行相关操作。以下是一个示例代码片段: ```c #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(int argc, char *argv[]) { int opt; int enable_broadcast = 0; while ((opt = getopt(argc, argv, "hb")) != -1) { switch (opt) { case 'h': printf("Usage: ping [-h] [-b] destination\n"); printf("-h: Display help information\n"); printf("-b: Allow pinging a broadcast address (IPv4 only)\n"); exit(0); case 'b': enable_broadcast = 1; break; default: exit(1); } } // Add your code for ping functionality here // ... return 0; } ``` 在上述代码中,我们使用`getopt`函数来获取命令行参数。如果传递了`-h`选项,则显示帮助信息并退出。如果传递了`-b`选项,则将`enable_broadcast`标志设置为1。 4. 在合适的位置根据`enable_broadcast`标志的值添加广播ping的功能代码。根据您的需求和系统环境,您可以使用套接字编程(例如使用`socket`函数和`sendto`函数)来实现广播ping。 5. 保存并关闭C语言程序的源代码文件。 6. 编译更新后的ping程序。使用适当的编译器命令将源代码文件编译为可执行文件。例如,使用以下命令将名为`ping.c`的源代码文件编译为`ping`可执行文件: ``` gcc ping.c -o ping ``` 7. 现在,您可以在Linux主目录中运行扩展后的ping程序了。例如,使用以下命令运行帮助信息: ``` ./ping -h ``` 或者,使用以下命令运行广播ping: ``` ./ping -b 192.168.0.255 ``` 请注意,对于广播ping功能的实现,您需要根据您的需求和系统环境添加适当的套接字编程代码。 希望这可以帮助您扩展C语言ping程序的功能!如果您有任何进一步的问题,请随时提问。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值