linux ping 命令的实现和分析


相信学过网络原理的童鞋都知道,icmp是一种专门用来“打小报告”的协议,而其中的ping命令则是它提供给我们使用的接口,没错,你可以在dos下拼拼,看

看你的网络通不通(当然,你能看到这篇博客证明阁下的网肯定是通的,嘿嘿)……ICMP是一种利用ping命令实现从应用层跨传输层直接到网络层的一个协议,

所以它肯定不使用tcp或udp的端口,但是,为了实现比较有效的传输机制,ICMP协议规定其查询消息的头4个字节都是标准的,它们分别是消息的类型和代码值

以及校验和(为了保证数据的正确传输使用的~),其中类型和代码分别为一个字节,而校验和则为2个字节。跟在这四个字节后面的是ICMP报文的id字段内容以

及报文的序列号,报文的id是为了在ip分片后重新组合需要用到的。好了,以上基本就是ICMP报文头部最重要的部分,下面以我们教材上的一个ping命令的实现

程序为例,为大家讲解和分析一下ping的基本流程。

首先一上来肯定是一大堆的头文件,后面的某些api可能是由这些头文件提供的,具体如何请读者自己查阅,本人在这里就不啰嗦了~
#include<stdio.h>
#include<stdlib.h>
#include<sys/time.h>
#include<unistd.h>//包含的是sendto和recvfrom函数
#include<string.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netdb.h>
#include<errno.h>
#include<arpa/inet.h>
#include<signal.h>//信号相关函数
#include<netinet/in.h>//地址和端口字节顺序转换相关函数

然后就是定义ICMP的一些基本的宏,这里也不再一一分析
#define ICMP_ECHOREPLY 0
#define ICMP_ECHO 8
#define BUFSIZE 1500
#define DEFAULT_LEN 56
#define u8 unsigned char
#define u16 unsigned short
#define u32 unsigned int

然后,下面就是重中之重了,是icmp的报头,其实我们需要配置和使用的也只是其中的类型、代码、id以及序列号,还有就是一个字节的占位符,用来记录发送

和接收到的时间,这样,我们才能算出ping了多长时间才得到应答,具体请看后面的程序哦!
/***icmpb消息头部****/
struct icmphdr
{
    u8 type;
    u8 code;
    u16 checksum;
    union
    {
        struct
        {
            u16 id;
            u16 sequence;
        }echo;
        u32 gateway;
        struct
        {
            u16 unused;
            u16 mtu;
        }frag;
    }un;
    u8 data[0];//icmp占位符
#define icmp_id un.echo.id
#define icmp_seq un.echo.sequence
};

#define ICMP_HSIZE sizeof(struct icmphdr)

//下面的是第二重要的ip报文头部的结构体,其实我们在程序中也只需要使用其中的头部长度ip_len(用来计算实际ip报文中的数据长度,具体见后面的代码)

和其中的ttl的成员(全称是time to life,最初设想是确定一个时间范围,超过此时间就把包丢弃。由于每个路由器都至少要把TTL域减一,TTL通常表示包在

被丢弃前最多能经过的路由器个数。当记数到0时,路由器决定丢弃该包,并发送一个ICMP报文给最初的发送者。)
struct iphdr
{
    u8 hlen:4;
    u8 tos;
    u16 tot_len;
    u16 id;
    u16 frag_off;
    u8 ttl;
    u8 protocol;
    u16 check;
    u32 saddr;
    u32 daddr;
};

char *hostname;//被ping的主机
int datalen=DEFAULT_LEN;//icmp消息携带的数据长度
char sendbuf[BUFSIZE];
char recvbuf[BUFSIZE];
int nsent;
int nrecv;
pid_t pid;//用来存储进程号的
struct timeval recvtime;//此结构体是用来记录时间的
int sockfd;//用来存储原始套接字的
struct sockaddr_in dest, from;//神一般的结构体,也是网络编程的灵魂结构体,是在网络中传输信息的纽带(至少本人是这样理解的)
struct sigaction act_alarm, act_int;//这是两个事件结构体,用来通知应用程序处理相应的时间的(相当于中断处理)

/*****以下是函数声明*******/
void alarm_handler(int);
void int_handler(int);
void set_sighandler(void);
void send_ping(void);
void recv_reply(void);
u16 checksum(u8 *, int);
int handle_pkt(void);
void get_statistics(int, int);
void bail(const char *);

//这里是一个定时器,为什么要设置定时器呢?如果ping过的盆友肯定知道,每ping一次就会周期性的返回ping的结果,这正是通过定时器实现的,要通知应用

程序定时器到时,我们就还必须要用到尚明的信号结构体act_alarm用来通知应用程序处理中断程序(这里是send_ping函数),也就是每隔一段时间就发送报文

。还有一个信号结构体act_int则是用来处理软中断(ctrl+c退出终端命令)的。
定时器的时间设定意思简要如下:前两个成员是用来设定第一次的定时时间的,聪明的你一定看出来是1.0秒,然后,后面的两个成员就是以后的定时时间了,没

错,就是0.000001秒,也就是说以后每隔0.0000001秒就发送一次报文。
struct itimerval val_alarm = {.it_interval.tv_sec=1, .it_interval.tv_usec=0, .it_value.tv_sec=0, .it_value.tv_usec=1};

哈哈,前面的都属于头文件部分,下面进入主函数吧,看程序就要从主函数开始看起,这样才能保证流程的正确性(怎么好像是一句废话啊~~~)

int main(int argc, char* argv[])
{
    struct hostent *host;
    
    hostname = argv[1];
    /*这里定义了一个hostent的结构体指针,通过用户在执行时输入的参数,告诉程序,需要ping的主机名或域名,使用这个东西,首先要包含2个头文件

:#include <netdb.h>#include <sys/socket.h>struct hostent *gethostbyname(const char *name);这个函数的传入值是域名或者主机名,例如""等等

。传出值,是一个hostent的结构(如下)。如果函数调用失败,将返回NULL。
struct hostent {
     char *h_name;
     char **h_aliases;
     int h_addrtype;
     int h_length;
     char **h_addr_list;
};
解释一下这个结构, 其中:
char *h_name 表示的是主机的规范名。例如www.google.com的规范名其实是www.l.google.com。
char **h_aliases 表示的是主机的别名。www.google.com就是google他自己的别名。有的时候,有的主机可能有好几个别名,这些,其实都是为了易于用户记

忆而为自己的网站多取的名字。
int h_addrtype 表示的是主机ip地址的类型,到底是ipv4(AF_INET),还是ipv6(AF_INET6)
int h_length 表示的是主机ip地址的长度
int **h_addr_lisst 表示的是主机的ip地址,注意,这个是以网络字节序存储的。千万不要直接用printf带%s参数来打这个东西,会有问题的哇。*/

    memset(&dest, 0, sizeof(dest));
    dest.sin_family = PF_INET;
    dest.sin_port = ntohs(0);
    dest.sin_addr = *(struct in_addr*)host->h_addr_list[0];
    /*然后就是大家都很熟悉的socket套接字的配置,需要注意的是最后的地址的设置,因为我们这个套接字最终是要发送到网络上面的,所以需要的是网

络地址,即网络字节顺序(大端机上,字节顺序是高地址在内存的高地址处,低地址在内存的低地址处,和我们的微机的字节顺序恰好是相反的哦*/
    if((sockfd = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP)) < 0)
    {
        perror("raw socket created error");
        exit(1);
    }
/*接下来就是利用神奇的函数socket生成一个ICMP的原始套接字,但是要注意,这个操作只有root用户执行才有效哦!

    setuid(getuid());//将该进程的用户id设置为当前用户id,避免仍以root用户运行
    pid = getuid();

    set_sighandler();//设置信号,我们跳转到该函数的定义去看看~
    printf("Ping %s(%s): %d bytes data in ICMP packets.\n\n", argv[1], inet_ntoa(dest.sin_addr), datalen);
    if((setitimer(ITIMER_REAL, &val_alarm, NULL) )== -1)//设置定时器,和SIGALRM连用
    {
        bail("setitimer fails.");
    }
    /*****接收程序发出的ping命令的应答*********/
    recv_reply();//好了,在去瞧瞧这个函数是用来干神马的吧~
    return 0;
}

void send_ping(void)
{
    struct icmphdr *icmp_hdr;
    int len;

    icmp_hdr = (struct icmphdr*)sendbuf;
    icmp_hdr->type =    ICMP_ECHO;
    icmp_hdr->code = 0;
    icmp_hdr->icmp_id = pid;
    icmp_hdr->icmp_seq = nsent++;//每发送一次就增加一次
    memset(icmp_hdr->data, 0xff, datalen);//填充字段为0xff
    
    gettimeofday((struct timeval *)icmp_hdr->data, NULL);//获得发送时的时间,填充到填充字段中
    
    len = ICMP_HSIZE + datalen;//icmp报文总长度
    icmp_hdr->checksum = 0;//校验和先清空为0
    icmp_hdr->checksum = checksum((u8 *)icmp_hdr, len);//计算校验和

    sendto(sockfd , sendbuf, len, 0, (struct sockaddr*)&dest, sizeof(dest));//
}
/*这个函数式信号act_alarm的处理函数,当定时器到时时,程序就会跳到这里进行处理,我们来看看里面的内容吧~首先将一段空的缓冲区sendbuf变身为icmp

的头部指针,使之变成理想的内存结构,然后配置了类型、代码,以及标识用的id号,诶?大家有没有发现,这个id号就是我们之前通过getpid函数获得的进程

的id号呢?后面的序列号在没发送一次就自增一次,个人感觉,这个序列号和tcp的三次握手的序列号的作用隔得很远~接下来,就要用到不起眼的一个字节的填

充字段了(它在这里可是发挥了大作用哦!),我们将用它来存储时间,真的是,小人物也有大作用啊!在利用那个固定的校验和计算函数设置了校验和后,就

可以通过sendto承载着icmp数据包发送出去了。

void recv_reply()
{
    int n,len;
    int errno;
    n = nrecv = 0;
    
    while(nrecv < 3)
    {
        if((n = recvfrom(sockfd, recvbuf, sizeof recvbuf, 0, (struct sockaddr*)&from, &len)) < 0)
        {
            if(errno == EINTR) continue;
            bail("recvfrom error");
        }
        gettimeofday(&recvtime, NULL);//获得接收时候的时间
        
        if(handle_pkt()) continue;//
        nrecv++;
    }
}//当网络是通的时候,icmp协议报文会从目标主机那里返回一个应答消息(reply),表明网络是通畅的(也就是打小报告看网络是否连通,这就是icmp协议的

主要功能),通过icmp的原始套接字,调用recvfrom函数可以返回接收信息,接收到信息后,转到handle_pkt函数进行处理,下面我们跳到该函数去看看~


u16 checksum(u8 *buf, int len)
{    
    u32 sum=0;
    u16 *cbuf;
    cbuf =(u16 *)buf;
    while(len > 1)    
    {
        sum += *cbuf++;
        len -= 2;    
    }
    if(len)    sum += *(u8 *)cbuf;
    sum = (sum >> 16) + (sum & 0xffff);
    sum += (sum >> 16);
    return -sum;
}

/******解析收到的echo应答分组*******
/int handle_pkt()
{    
    struct iphdr *ip;        struct icmphdr *icmp;
    int ip_hlen;
    u16 ip_datalen;
    double rtt;        struct timeval *sendtime;
        ip = (struct iphdr *)recvbuf;        ip_hlen = ip->hlen << 2;//获得ip报文头部长度
    ip_datalen = ntohs(ip->tot_len) - ip_hlen;
    icmp = (struct icmphdr *)(recvbuf + ip_hlen);//获得icmp消息头部指针
    if(checksum((u8 *)icmp, ip_datalen)) return -1;
    if(icmp->icmp_id != pid) return -1;//检查是否是期望接收的    
    sendtime = (struct timeval *)icmp->data;
    rtt = ((&recvtime)->tv_sec - sendtime->tv_sec)*1000 + ((&recvtime)->tv_usec - sendtime->tv_usec)/1000.0f;
    printf("%d bytes from %s: icmp_seq = %u ttl = %d rtt = %.3f ms\n", ip_datalen, inet_ntoa(from.sin_addr),
 icmp->icmp_seq, ip->ttl,rtt);
    return 0;
}//可以看到,该函数和发送函数一样,将接收缓冲区recvbuf(注意,此时不再是空的了哦,而是通过recvfrom函数从套接字中将内容读出来了)通过强制类型

转换成icmp的报头指针,然后取其中的ip报头长度,这里我们可以简要分析一下ip_hlen=ip->hlen<<2这条语句的意思:大家都知道,ip首部有个首部长度hlen

:4(如上面所示,后面的是域运算符,表示只有低4位使用),ip报文头部的长度范围在20~60bytes,为什么是这个范围呢?因为hlen的低四位的范围为

0000~1111(也就是0~15),代表的是长度单位,每个长度单位为4字节(TCP/IP标准,DoubleWord),所以最大范围为15*4=60bytes,当没有设置ip的可选项

时,ip报头长度就是20个字节,hlen的初始值就是5,而这个5左移2位恰好是20,大家有没有意识到什么?对,其实就是将hlen变成了20.然后,用内核自动计算

的ip报文的的总长度减去首部长度,就得到了ip的数据长度(包含icmp头部)。接下来,为了保证icmp报文传输的完整性,再次进行校验。完成后,再验证一下

进程的id号是不是发送那个icmp数据包的ping命令的进程(因为可能调用多个ping命令或是其它的程序也发送相关的数据包)。然后,计算一下从发送的时间到

现在接收的时间差,最后打印相关信息,整个的接收过程就完了~

//set the function to the signal struct and register to the kernal
void set_sighandler()
{
    act_alarm.sa_handler = alarm_handler;//set the function
    if(sigaction(SIGALRM, &act_alarm, NULL) == -1) bail("SIGALRM handler setting fails.");
    act_int.sa_handler = int_handler;//set the function
    if(sigaction(SIGINT, &act_int, NULL) == -1) bail("SIGINT handler setting fails.");    
}//end for set_sighandler,看见没,主要就是设置了两个信号结构体,然后向内核进行注册,这两个信号的功能在定时器val_alarm的定义处已经进行说明

了哦!

void get_statistics(int nsent, int nrecv)
{
    printf("--- %s ping statistics ---\n", inet_ntoa(dest.sin_addr));
    printf("%d packets transmitted, %d received, %0.0f%%""packet loss \n", nsent, nrecv, 1.0*(nsent - nrecv)/nsent*100);
}//end for get_statistics

void bail(const char *on_what)
{
    fputs(strerror(errno), stderr);
    fputs(":", stderr);
    fputs(on_what, stderr);
    fputc('\n', stderr);
    exit(1);
}

//SIGINT handling function
void int_handler(int sig)
{
    get_statistics (nsent, nrecv);
    close(sockfd);
    exit(1);
}

//SIGALRM handling function
void alarm_handler(int signo)
{
    send_ping();
}


PS:本人之前老是以为send和write是TCP专用,而sendto和recvfrom函数则是UDP专用,做了icmp的原始套接字后,才意识到,原来数据包套接字只是使用

sendto和recvfrom进行数据的发送和接收,而流式套接字则是使用send和write进行数据的发送和接收,当然,整个的过程都是由内核来完成的……ICMP的ping

命令需要输入对方主机名或域名以测试网络的连通性,给人感觉好像是面向连接的,但是我们发送的是icmp的数据报文,所以是利用数据报的发送和接收函数

(sendto和recvfrom函数)进行通讯的……所以,东西还是要灵活的理解和运用啊~~~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值