ping源码分析(超详细,多图,带背景分析)

ping源码分析

内容比较多,目录如下

文章目录

  • ping源码分析
    • 1. 背景知识简介
      • 1.1 ping简介
      • 1.2 ICMP协议简介
        • 1.2.1 ICMP报文分类
        • 1.2.2 ICMP报文结构
        • 1.2.3 ICMP封装过程
      • 1.3 socket简介
        • 1.3.1 socket创建
        • 1.3.2 socket绑定
        • 1.3.3 socket选项
        • 1.3.4 socket发送消息
        • 1.3.5 socket接受消息
    • 2. 分析准备
      • 2.1 源码简介
      • 2.2 源码获取
      • 2.3 源码结构
      • 2.4 编译测试
      • 2.5 分析对象
    • 3. 源码概要分析
      • 3.1 逻辑功能流程
      • 3.2 主要数据结构
        • 3.2.1 网络通信地址sockaddr_in
        • 3.2.2 主机信息 hostent
        • 3.2.3 I/O向量 iovec
        • 3.2.4 消息组织结构 msghdr
        • 3.2.5 时间变量 timeval
        • 3.2.6 ICMP报文头部 icmphdr
        • 3.2.7 IP报文头部 iphdr
      • 3.3 主要函数及其功能简介
        • 3.3.1 main函数(ping.c中)
        • 3.3.2 setup函数 (ping_common.c中)
        • 3.3.3 main_loop函数 (ping_common.c中)
        • 3.3.4 pinger函数 (ping_common.c中)
        • 3.3.5 receive_error_msg函数 (ping.c中)
        • 3.3.6 parse_reply函数(ping.c中)
        • 3.3.7 send_prob函数 (ping.c中)
        • 3.3.8 in_cksum函数 (ping.c中)
        • 3.3.9 finish函数(ping_common.c中)
        • 3.3.10 recvmsg函数(共享库函数)
        • 3.3.11 sendmsg函数(共享库函数)
      • 3.4 主要函数间调用关系
    • 4. 源码详细分析
      • 4.1 重要变量分析
        • 4.1.1 ping.c中的全局变量分析
        • 4.1.2 ping_common.c中的全局变量分析
      • 4.2 代码详细分析
        • 4.2.1 main函数
        • 4.2.2 main_loop函数
        • 4.2.3 pinger函数
        • 4.2.4 recive_error_msg函数
        • 4.2.5 parse_reply函数

1. 背景知识简介

1.1 ping简介

​ Ping是Windows、Unix和Linux系统下的一个命令。ping也属于一个通信协议,是TCP/IP协议的一部分。利用“ping”命令可以检查网络是否连通,可以很好地帮助我们分析和判定网络故障。ping程序是基于ICMP协议实现的。

​ Ping命令常见格式:ping [-t] [-a] [-n count] [-l length] [-f] [-i ttl] [-v tos] [-r count] [-s count] [[-j -Host list] | [-k Host-list]] [-w timeout] destination-list

1.2 ICMP协议简介

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

1.2.1 ICMP报文分类

​ ICMP报文主要分为两类:一类是差错报文,一类是查询报文。

查询报文

  1. ping命令请求与回复报文
YPE CODE Description
0 0 Echo Reply——回显应答(Ping应答)
8 0 Echo request——回显请求(Ping请求)
  1. 时间戳请求和时间戳答复
YPE CODE Description
13 0 Timestamp request (obsolete)——时间戳请求(作废不用)
14 0 Timestamp reply (obsolete)——时间戳应答(作废不用)
  1. 路由器请求与通告
YPE CODE Description
9 0 Router advertisement——路由器通告
10 0 Route solicitation——路由器请求
  1. 地址码请求与答复
YPE CODE Description
17 0 Address mask request——地址掩码请求
18 0 Address mask reply——地址掩码应答

差错报文

  1. 终点不可达,当数据包不能发送到目标主机或路由时,就会丢弃该数据包向源点发送终点不可达报文。
YPE CODE Description
3 0 Network Unreachable——网络不可达
3 1 Host Unreachable——主机不可达
3 2 Protocol Unreachable——协议不可达
3 3 Port Unreachable——端口不可达
3 4 Fragmentation needed but no frag. bit set——需要进行分片但设置不分片比特
3 5 Source routing failed——源站选路失败
3 6 Destination network unknown——目的网络未知
3 7 Destination host unknown——目的主机未知
3 8 Source host isolated (obsolete)——源主机被隔离(作废不用)
3 9 Destination network administratively prohibited——目的网络被强制禁止
3 10 Destination host administratively prohibited——目的主机被强制禁止
3 11 Network unreachable for TOS——由于服务类型TOS,网络不可达
3 12 Host unreachable for TOS——由于服务类型TOS,主机不可达
3 13 Communication administratively prohibited by filtering——由于过滤,通信被强制禁止
3 14 Host precedence violation——主机越权
3 15 Precedence cutoff in effect——优先中止生效
  1. 源点抑制,用于告知源点应该降低发送数据包的速率。
YPE CODE Description
4 0 Source quench——源端被关闭(基本流控制)
  1. 超时 当路由器收到TTL值为0的数据包时,会丢弃该数据包并向源点发送超时报文。
YPE CODE Description
11 0 TTL equals 0 during transit——传输期间生存时间为0
11 1 TTL equals 0 during reassembly——在数据报组装期间生存时间为0
  1. 参数问题,可能是IP首部有的字段值是错误的或者IP首部被修改,破坏都有可能
YPE CODE Description
12 0 IP header bad (catchall error)——坏的IP首部(包括各种差错)
12 1 Required options missing——缺少必需的选项
  1. 路由重定向
YPE CODE Description
5 1 Redirect for host——对主机重定向
5 2 Redirect for TOS and network——对服务类型和网络重定向
5 3 Redirect for TOS and host——对服务类型和主机重定向

1.2.2 ICMP报文结构

​ 不同的ICMP报文有着不同的报文结构,但是都有着如下的共同结构:

ping源码分析(超详细,多图,带背景分析)_第1张图片

  • TYPE字段 8bits 类型字段
  • CODE字段 8bits 提供更多的报文类型信息
  • CHECKSUM字段 16bits 校验和字段

1.2.3 ICMP封装过程

ICMP协议是基于IP协议的。ICMP报文被封装在IP报文的数据段中,其封装原理图如下:

ping源码分析(超详细,多图,带背景分析)_第2张图片

1.3 socket简介

​ 网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。

​ 建立网络通信连接至少要一对端口号(socket)。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。

1.3.1 socket创建

函数int socket(int domain, int type, int protocol);用于创建一个新的socket。

参数说明:

  • domain:协议域,又称协议族(family)。常用的协议族有AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE等。协议族决定了socket的地址类型,在通信中必须采用对应的地址。

    • AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合
    • AF_UNIX决定了要用一个绝对路径名作为地址。
  • type:指定Socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。

    • 流式Socket(SOCK_STREAM)是一种面向连接的Socket,针对于面向连接的TCP服务应用。
    • 数据报式Socket(SOCK_DGRAM)是一种无连接的Socket,对应于无连接的UDP服务应用
  • protocol:指定协议。

    • IPPROTO_TCP TCP传输协议
    • IPPROTO_UDP UDP传输协议
    • IPPROTO_STCP STCP传输协议
    • IPPROTO_TIPC TIPC传输协议。

返回值:

  • 成功就返回新创建的套接字的描述符
  • 失败就返回INVALID_SOCKET(Linux下失败返回-1)。

1.3.2 socket绑定

函数int bind(SOCKET socket, const struct sockaddr address, socklen_t address_len);用于将套接字文件描述符绑定到一个具体的协议地址。

参数说明:

  • socket:是一个套接字描述符。

  • address:是一个sockaddr结构指针,该结构中包含了要结合的地址和端口号。

  • address_len:确定address缓冲区的长度。

返回值:

  • 执行成功,返回值为0
  • 执行失败,返回SOCKET_ERROR。

1.3.3 socket选项

使用int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);函数来对socket选项进行设置。

参数说明:

  • socket:是一个套接字描述符。
  • level:选项所在的协议层。
    • SOL_SOCKET:通用套接字选项.
    • IPPROTO_IP:IP选项.
    • IPPROTO_TCP:TCP选项.
  • optname:需要访问的选项名
  • *optval: 新选项值的缓冲
  • optlen: 选项的长度

返回值:

  • 成功:返回0
  • 失败:返回错误码

该程序中使用了的选项值如下:

level 级别 选项名字 说明
SOL_SOCKET SO_BROADCAST 允许或禁止发送广播数据
SOL_SOCKET SO_DEBUG 打开或关闭调试信息
SOL_SOCKET SO_DONTROUTE 打开或关闭路由查找功能。
SOL_SOCKET SO_TIMESTAMP 打开或关闭数据报中的时间戳接收。
IPPROTO_IP IP_MULTICAST_LOOP 多播API,禁止组播数据回送
SOL_IP IP_MTU_DISCOVER 为套接字设置Path MTU Discovery setting(路径MTU发现设置)
SOL_IP IP_RECVERR 允许传递扩展的可靠的错误信息

1.3.4 socket发送消息

socket使用ssize_t sendmsg(int sockfd, struct msghdr *msg, int flags);函数来发送消息

参数说明:

  • socketfd:一个套接字描述符;
  • *msg:  信息头结构指针;
  • flags: 标记参数

返回值:

  • 发生错误:返回-1
  • 正确发送: 返回发送的字节数

1.3.5 socket接受消息

socket使用ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);函数来接收消息

参数说明:

  • socketfd:一个套接字描述符;
  • *msg:  信息头结构指针;
  • flags: 标记参数

返回值:

  • 发生错误:返回-1
  • 正确发送: 接收了的字节数

2. 分析准备

2.1 源码简介

​ 分析的源码选自Linux环境下的iputils。iputils包是一组用于Linux网络的小型实用工具。它最初由亚历克赛·库兹涅佐夫维护。

2.2 源码获取

​ 本此源码分析选择了iputils-s20121221版本的中的代码作为分析对象。获取来源为github开项目。其地址为https://github.com/dgibson/iputils

2.3 源码结构

​ 与ping程序有关的主要为以下4个文件,其各自的名字及功能简介如下:

ping.c   // ipv4 下的ping程序
ping6.c  // ipv6 下的ping程序
ping_common.c //ipv4与ipv6共有的 与协议无关的共同代码 
ping_common.h //ping_common.c的头文件

2.4 编译测试

  1. 在Linux环境下下载iputils源码。其目录结构如下:

    ping源码分析(超详细,多图,带背景分析)_第3张图片

  2. 使用make命令进行编译,编译完成后。 运行ping命令测试。可以看到编译通过且程序测试成功。

    ping源码分析(超详细,多图,带背景分析)_第4张图片

2.5 分析对象

​ 本次实验以ipv4下的ping程序下的完整流程为分析对象,重点了解ping程序的完整流程以及学习其中网络编程的方法。

3. 源码概要分析

3.1 逻辑功能流程

​ 代码的逻辑功能主要分为1. 使用UDP报文对目标主机进行一个connetc尝试 2. 时候使用循环发送ICMP报文并对回复报文进行处理 这两个主要部分。当遇到中断等条件时候打印信息后进行退出。

Created with Raphaël 2.3.0 开始 用户调用ping程序 根据用户的设置的选项设置参数 设置ICMP报文选项 是否退出?(中断 超时等) 打印出信息 退出整个程序 发送ICMP报文 还有时间? 处理ICMP答复报文 yes no yes no

3.2 主要数据结构

3.2.1 网络通信地址sockaddr_in

用来表示socket的通信地址,其与sockaddr的区别是将端口和地址进行了区别。

struct sockaddr_in {
    sa_family_t    sin_family;  //地址种类 AF_INET代表IPV4
    u_int16_t      sin_port;   //端口
    struct in_addr sin_addr;   //地址
}

struct in_addr {
    u_int32_t      s_addr;     // 地址是按照网络字节顺序排列的
};

3.2.2 主机信息 hostent

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

struct hostent
{
    char *h_name;         //正式主机名
    char **h_aliases;     //主机别名
    int h_addrtype;       //主机IP地址类型:IPV4-AF_INET
    int h_length;		  //主机IP地址字节长度,对于IPv4是四字节,即32位
    char **h_addr_list;	  //主机的IP地址列表
};

3.2.3 I/O向量 iovec

I/O vector,与readv和wirtev操作相关的结构体。readv和writev函数用于在一次函数调用中读、写多个非连续缓冲区。其主要用于发送和接收ICMP报文中。

struct iovec {
    ptr_t iov_base; //开始地址
    size_t iov_len; //长度
};

3.2.4 消息组织结构 msghdr

在套接字发送接收系统调用流程中,send/recv,sendto/recvfrom,sendmsg/recvmsg最终都会使用内核中的msghdr来组织数据。其主要用于发送和接收ICMP报文中。

struct msghdr {
    void        *msg_name;    //指向socket地址结构 
    int        msg_namelen;    //地址结构长度
    struct iov_iter    msg_iter; //数据
    void        *msg_control;    //控制信息
    __kernel_size_t    msg_controllen; //控制信息缓冲区长度
    unsigned int    msg_flags;  //接收信息的标志
    struct kiocb    *msg_iocb; //异步请求控制块
};

3.2.5 时间变量 timeval

时间结构体,用来记录时间。其在本程序中主要用于接受报文部分中。

struct timeval  
{  
__time_t tv_sec;        //秒
__suseconds_t tv_usec;  //微秒  
};

3.2.6 ICMP报文头部 icmphdr

用来表示ICMP报文的头部信息。其主要用于处理ICMP差错报文部分中。

struct icmphdr
{
  u_int8_t type;        //报文类型
  u_int8_t code;        //类型子代码
  u_int16_t checksum;   //校验和
  union
  {
    struct
    {
      u_int16_t    id;
      u_int16_t    sequence;
    } echo;            //回复数据流
    u_int32_t    gateway;    //网关地址
    struct
    {
      u_int16_t    __unused;
      u_int16_t    mtu;
    } frag;            //路径MTU
  } un;
};

3.2.7 IP报文头部 iphdr

表示IP报文的头部的结构体,其主要用于处理ICMP回复报文中。

struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
    __u8    ihl:4,     //首部长度
            version:4;  //版本
#elif defined (__BIG_ENDIAN_BITFIELD)
    __u8    version:4, //版本
            ihl:4;     //首部长度
#else
#error "Please fix "
#endif
    __u8    tos;       //服务类型
    __be16 -tot_len;   //总长度
    __be16 -id;        //标识
    __be16 -frag_off;  //标志——偏移
    __u8    ttl;       //生存时间
    __u8    protocol;  //协议
    __be16 -check;     //首部校验和
    __be32 -saddr;     //源IP地址
    __be32 -daddr;     //目的IP地址
};

3.3 主要函数及其功能简介

3.3.1 main函数(ping.c中)

main函数为ping.c程序中,是整个ping程序流程中的开始入口。其完成了如下的几个功能:

  1. 创建ICMP报文;

  2. 根据用户的选项参数来循环设置选项标志;

  3. 处理用户后面的地址参数,将其保存在route数组中;

  4. 对目标地址连接一个UDP报文,获知目标主机的基本情况;

  5. 之后设置ICMP报文选项(ipv4特有);

  6. 调用ping_common.c/setup()函数来对ICMP报文设置参数(与ipv6共用);

  7. 调用ping_common.c/main_loop()函数来完成探测

3.3.2 setup函数 (ping_common.c中)

该函数主要负责对ICMP报文进行设置选项,其是ipv4与ipv6的公共部分。

3.3.3 main_loop函数 (ping_common.c中)

该函数主要负责来循环发送报文,分析报文。在一个for无限循环中,完成了了如下的几个功能:

  1. 检查是否因为 中断,超出次数限制,超出时间限制,SIGQUIT中断而退出;
  2. 调用ping.c/pinger()函数来 发送ICMP报文;
  3. 调用recvmsg()函数来接收报文;
  4. 如果没有正确接收报文,就调用ping.c/receive_error_msg()函数来处理ICMP差错报文;
  5. 如果正确处理了接收报文,就调用ping.c/parse_reply()函数来解析ICMP回复报文;
  6. 如此循环知道不满足条件之后调用ping.c/finish()函数来打印统计信息之后退出;

3.3.4 pinger函数 (ping_common.c中)

该函数用来编写和发送ICMP数据包。

  1. 调用ping.c/send_probe()函数来组成并发送ICMP回显请求包;
  2. 发送成功则返回剩余时间并退出;
  3. 发送失败则根据各种情况处理失败:
  4. 非常见错误调用abort()函数退出;
  5. ENOBUFS:输出网络接口缓存满 或者 ENOMEM:没有内存。则减缓发送速度;
  6. EAGAIN:缓冲区满 则增加时间间隔返回;
  7. ICMP错误报文 使用 ping.c/receive_error_msg()函数来处理ICMP差错报文;

3.3.5 receive_error_msg函数 (ping.c中)

用来处理ICMP错误报文信息。

  1. 首先通过 recvmsg()函数来获得接收消息;
  2. 如果是本地错误,则根据不同的标志情况来处理;
  3. 否则则是ICMP差错报文信息,如果不是我们的错误,则退出。如果是网络出错,则安装一个更严格的过滤器。之后根据不同模式处理退出。

3.3.6 parse_reply函数(ping.c中)

该函数用来处理ICMP答复报文。

  1. 首先先检查IP头部 错误则退出;
  2. 检查ICMP头部,调用ping.c/in_cksum()函数来检测校验和;
  3. 如果是ICMP回复报文,则根据进程ID判断是不是回复本进程。并做出对应操作之后退出;
  4. 如果不是ICMP回复报文,则根据不同的报文类型来进行不同的操作。

3.3.7 send_prob函数 (ping.c中)

该函数用来发送ICMP报文。

  1. 首先先对ICMP报文头部进行了一些设置:

    头部位 设置值
    type ICMP_ECHO 回复报文类型
    code 0
    ckecksum 0 (后面计算)
    un.echo.sequence htons(ntransmitted+1) 将主机字节序转换为网络字节序
    un.echo.id ident 进程ID
  2. 之后调用ping.c/in_cksum()函数来计算校验和并写入;

  3. 之后嗲用sendmsg()函数发送ICMP报文;

3.3.8 in_cksum函数 (ping.c中)

此函数主要用来验证校验和,使用32累加器,向它添加连续的16位字,在最后,将所有的进位从前16位折回到下16位。

3.3.9 finish函数(ping_common.c中)

此函数在最后时候调用,主要用来打印一些统计信息。

3.3.10 recvmsg函数(共享库函数)

用来接收socket套接字,通用的I/O函数,可以接收面向连接或者非连接套接字。返回值返回读取的字节数。

3.3.11 sendmsg函数(共享库函数)

用来发送socket套接字,通用的I/O函数,可以接收面向连接或者非连接套接字。返回值返回发送的字节数。

3.4 主要函数间调用关系

设置共有套接字选项
发送和接收ICMP报文
发送ICMP报文
重复运行 不满足退出条件
接收ICMP报文
没有正确接收
正确接收
因为中断等退出
构造和发送ICMP消息
发送报文
检查校验和
main 解析参数 设置套ipv4特有接字选项
setup
main_loop
pinger
recvmsg
receive_error_msg
parse_reply
finish
send_probe
sendmsg
in_cksum 检查校验和

4. 源码详细分析

4.1 重要变量分析

4.1.1 ping.c中的全局变量分析

static int nroute = 0;        // 输入的主机数目 
static __u32 route[10];       // 多个主机存储数组

struct sockaddr_in whereto;	  /* 套接字地址 目标地址 ping谁  */
struct sockaddr_in source;    /* 套接字地址 源地址 谁在ping  */

int optlen = 0;               //ip选项的长度
int settos = 0;			      /* 设置TOS  服务质量  */

int icmp_sock;			      /* icmp socket 文件描述符 */

static int broadcast_pings = 0;//是不是ping广播地址

char *device;                  //如果-I选项后面带的是设备名而不是源主机地址的话,如eth0,就用device指向该设备名。该device指向一个设备名之后,会设置socket的对应设备为该设备

4.1.2 ping_common.c中的全局变量分析

int options;                    /*存储各种选项的FLAG设置情况 在判断输入选项时候设置*/

int sndbuf;                     // 发送缓冲区的大小  可以通过-S参数指定
int ttl;                        /* 报文 ttl值 */
int rtt;                        //RTT值  用指数加权移动平均算法估计出来
__u16 acked;                   //接到ACK的报文的16bit序列号

/* 计数器  会在finish函数中调用 */
long npackets;			//需要传输的最多报文数 -c参数可以设置
long nreceived;			//得到回复的报文数 
long nrepeats;			//重复的报文数
long ntransmitted;		//发送的报文的最大序列号
long nchecksum;			//checksum错误的恢复报文
long nerrors;			// icmp错误数

int interval = 1000;    /* 两个相邻报文之间相距的时间,单位为毫秒 -i参数可以设置 -f 洪泛模式下为0 */
int preload;            /* 在接受到第一个回复报文之前所发送的报文数 -l参数可以设置 默认为1 */            
int deadline = 0;		/* 退出时间  -w参数设置 SIGALRM中断 */
int lingertime = MAXWAIT*1000;/* 等待回复的最长时间,单位为毫秒 -W参数设置 默认值10000 */

struct timeval start_time, cur_time; /* 程序运行开始时的主机时间 当前的主机时间 */
volatile int exiting;       //程序是不是要退出

/* 计时 */
int timing;			        // 能否测算时间
long tmin = LONG_MAX;		// 最小RRT  初始值为LONG_MAX
long tmax;			        // 最大RRT  初始值为0
long long tsum;			    // 每次RRT之和
long long tsum2;            // 每次RRT的平方和

int datalen = DEFDATALEN;   /* 数据长度 初始值为56 -s参数设置*/

char *hostname;             /* 目的主机名字 通过gethostbyname()函数获得 */
int uid;                    /* 用户ID getuid()取得 如果不是超级用户则有限制*/
int ident;			        /* 本进程的ID */

4.2 代码详细分析

4.2.1 main函数

main函数是整个程序的入口,其主要功能主要是解析ping命令参数,然后创建socket,设置socket选项。

首先是函数的定义,以及一些变量定义。定义了变量来存储主机信息,错误码 主机名字缓冲区等。

int
main(int argc, char **argv)
{
	struct hostent *hp;   //记录主机的信息
	int ch, hold, packlen;
	int socket_errno;
	u_char *packet;
	char *target;          //目标主机

	char hnamebuf[MAX_HOSTNAMELEN];
	char rspace[3 + 4 * NROUTES + 1];	/* record route space */

之后创建了ICMP套接字,其协议域为ipv4(AF_INT),socket类型原始套接字(SOCK_RAW),协议类型为ICMP(IPPROTO_ICMP)。并指定了socket的错误类型代码。

enable_capability_raw();

/* 创建ICMP套接字 ipv4 原始套接字 ICMP协议*/
icmp_sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
socket_errno = errno;

disable_capability_raw();

/* 源地址是ipv4类型 */
source.sin_family = AF_INET;

preload = 1; //接收回复报文前发送报文数为1

解析选项参数:使用一个while循环和switch结构来解析命令的选项参数,并将其保存在options参数中供后面使用。

while ((ch = getopt(argc, argv, COMMON_OPTSTR "bRT:")) != EOF) {
		switch(ch) {

            // 设置广播
            case 'b':
                broadcast_pings = 1;
                break;
            // 设置服务质量  
            case 'Q':
            // 测试反向路由
            case 'R':
                if (options & F_TIMESTAMP) {
                    fprintf(stderr, "Only one of -T or -R may be used\n");
                    exit(2);
				}
				options |= F_RROUTE;
                /* 其余每个case里面的代码省略 用此代码表示说明选项保存在全局变量option中*/
			break;
            case 'T':
            // 设置源主机的接口地址或者设备名
            case 'I':
            //设置时间间隔
            case 'M':
            // 打印版本信息
            case 'V':
                printf("ping utility, iputils-%s\n", SNAPSHOT);
                exit(0);//输出后退出
            // 共有参数
            COMMON_OPTIONS //ping.c 与 ping6.c 共有的参数解析
                common_options(ch);
                break;
            // 非法参数
            default:
                usage();//报错
		}
	}

解析目标主机地址参数,最多有10个,将其转换成统一 地址类型,并保存在route数组中。

//参数移动
argc -= optind;
argv += optind;

/*------------------------------------------------
    先对地址参数个数情况进行一个判断  
    不同的命令模式下有不同的要求
-------------------------------------------------*/
/* 没有地址参数 */
if (argc == 0)
    usage();//报错
/* 参数多于一个地址 */
if (argc > 1) {
    // F_RROUTE情况
    if (options & F_RROUTE)
        usage(); //报错
    // -T tsprespec [host1 [host2[host3 [host4]]]]
    else if (options & F_TIMESTAMP) {
        if (ts_type != IPOPT_TS_PRESPEC)
            usage();
        // 此情况最多4个参数
        if (argc > 5)
            usage();
    } else {
        if (argc > 10) //route[] 最多存储10个地址
            usage();
        options |= F_SOURCEROUTE;
    }
}

/*------------------------------------------------
     对地址参数进行转换并存储在route数组中
-------------------------------------------------*/
while (argc > 0) {

    //目标主机为参数值
    target = *argv;
	
    //where_to 设置为0
    memset((char *)&whereto, 0, sizeof(whereto));
    whereto.sin_family = AF_INET;

    // 地址转换 将目标数字点形式地址转化为 struct in_addr结构
    if (inet_aton(target, &whereto.sin_addr) == 1) {
        hostname = target;
        if (argc == 1)
            options |= F_NUMERIC;
    } else {
        char *idn;
        idn = target;

        // 不是ipv4类型地址 通过域名查询DNS获取地址
        hp = gethostbyname(idn);
        // 无法获取正确地址
        if (!hp) {
            fprintf(stderr, "ping: unknown host %s\n", target);
            exit(2);
        }

        //将获得得到的 hp->h_addr复制到 whereto中
        memcpy(&whereto.sin_addr, hp->h_addr, 4);

        //将 h_name复制到 hnamebuf中 h_name: 官方名字
        strncpy(hnamebuf, hp->h_name, sizeof(hnamebuf) - 1);
        hostname = hnamebuf;
    }
    if (argc > 1)
        route[nroute++] = whereto.sin_addr.s_addr; //将主机地址存储在route数组中
    //处理下一个
    argc--;
    argv++;
}

如果没有设置源主机地址(source.sin_addr.s_addr == 0),则申请一个UDP类型的主机探针来探测目标主机信息。

//没有设置源主机地址
if (source.sin_addr.s_addr == 0) {
    socklen_t alen;
    struct sockaddr_in dst = whereto; //目标主机地址

    /*------------------------------------ 
		  UDP 类型的socket描述符
          AFINT: ipv4
          SOCK_DGRAM: 无连接 不可靠

          探针 用来探测目标主机的基本情况
		--------------------------------------*/
    int probe_fd = socket(AF_INET, SOCK_DGRAM, 0);

    if (probe_fd < 0) { //申请失败
        perror("socket");
        exit(2);
    }

    // 如果设置了网络设备的名字
    if (device) {
        struct ifreq ifr;
        int rc;

        memset(&ifr, 0, sizeof(ifr));
        strncpy(ifr.ifr_name, device, IFNAMSIZ-1);

        enable_capability_raw();
        //设置socket选项
        // SO_BINDTODEVICE: 绑定socket到一个网络设备
        rc = setsockopt(probe_fd, SOL_SOCKET, SO_BINDTODEVICE, device, strlen(device)+1);
        disable_capability_raw();

        if (rc == -1) {
            // 测试 是不是一个多播地址
            if (IN_MULTICAST(ntohl(dst.sin_addr.s_addr))) {
                struct ip_mreqn imr;
                if (ioctl(probe_fd, SIOCGIFINDEX, &ifr) < 0) {
                    fprintf(stderr, "ping: unknown iface %s\n", device);
                    exit(2);
                }
                memset(&imr, 0, sizeof(imr));

                // 组播
                // IP_MULTICAST_IF: 为组播socket设置一个当地设备 
                imr.imr_ifindex = ifr.ifr_ifindex;
                if (setsockopt(probe_fd, SOL_IP, IP_MULTICAST_IF, &imr, sizeof(imr)) == -1) {
                    perror("ping: IP_MULTICAST_IF");
                    exit(2);
                }
            } else {
                perror("ping: SO_BINDTODEVICE");
                exit(2);
            }
        }
    }

    // 如果设置了服务质量参数  则对socket选项设置
    if (settos &&
        setsockopt(probe_fd, IPPROTO_IP, IP_TOS, (char *)&settos, sizeof(int)) < 0)
        perror("Warning: error setting QOS sockopts");

    // 将主机字节序转换为网络的字节序 (因为主机有不同的大小端)
    dst.sin_port = htons(1025);

    // 多个route
    if (nroute)
        dst.sin_addr.s_addr = route[0];

    // 试探主机的基本情况 connect
    if (connect(probe_fd, (struct sockaddr*)&dst, sizeof(dst)) == -1) {

        // 出错: 尝试连接一个广播地址而没有设置socket广播标志
        if (errno == EACCES) {

            // 确定没有设置广播 提示后退出
            if (broadcast_pings == 0) {
                fprintf(stderr, "Do you want to ping broadcast? Then -b\n");
                exit(2);
            }
            fprintf(stderr, "WARNING: pinging broadcast address\n");

            // 否则,设置广播标志
            // SO_BROADCAST: 设置广播标志
            if (setsockopt(probe_fd, SOL_SOCKET, SO_BROADCAST,
                           &broadcast_pings, sizeof(broadcast_pings)) < 0) {
                perror ("can't set broadcasting");
                exit(2);
            }

            // 尝试再次连接
            if (connect(probe_fd, (struct sockaddr*)&dst, sizeof(dst)) == -1) {
                perror("connect");
                exit(2);
            }
        } else {//其他错误
            perror("connect");
            exit(2);
        }
    }

    // 将当前socket名字复制给 source
    alen = sizeof(source);
    if (getsockname(probe_fd, (struct sockaddr*)&source, &alen) == -1) {
        perror("getsockname");
        exit(2);
    }
    // 设置source端口号为0
    source.sin_port = 0;
    
    close(probe_fd);//关闭探针
} while (0)

之后根据option中的标志来设置套接字选项,main函数中为IPV4特有的,之后会调用 setup设置公共的。

/******************************************************
/*			根据option标志情况对socket进行设置
/*		
*******************************************************/


// 广播ping  -b 参数
if (broadcast_pings || IN_MULTICAST(ntohl(whereto.sin_addr.s_addr))) {
    if (uid) {

        // 间隔时间太短
        if (interval < 1000) {
            fprintf(stderr, "ping: broadcast ping with too short interval.\n");
            exit(2);
        }
        // 非超级用户不允许在ping广播地址时进行分段
        /*-----------------------------------------------------
			/* pmtudisc: 
			     IP_PMTUDISC_DO   2   Always DF 不允许分段 
			     IP_PMTUDISC_DONT 1   User per route hints
			     IP_PMTUDISC_WANT 0   Never send DF frame
			------------------------------------------------------------*/
        if (pmtudisc >= 0 && pmtudisc != IP_PMTUDISC_DO) {
            fprintf(stderr, "ping: broadcast ping does not fragment.\n");
            exit(2);
        }
    }
    // 未设置-M参数
    if (pmtudisc < 0)
        pmtudisc = IP_PMTUDISC_DO;
}

// 设置了-M参数 IP_PMTUDISC
if (pmtudisc >= 0) {

    // 设置IP_MTU_DISCOVER 在ip 标志第二位设置为1 即不可以分段
    if (setsockopt(icmp_sock, SOL_IP, IP_MTU_DISCOVER, &pmtudisc, sizeof(pmtudisc)) == -1) {
        perror("ping: IP_MTU_DISCOVER");
        exit(2);
    }
}

// 设置了 -I  参数 给套接字绑定一个协议地址
if ((options&F_STRICTSOURCE) &&
    bind(icmp_sock, (struct sockaddr*)&source, sizeof(source)) == -1) { //绑定
    perror("bind");
    exit(2);
}

// 设置过滤器
if (1) {
    struct icmp_filter filt;

    /*------------结构体原型---------------------
			struct icmp_filter {
				__u32	data;
			};
		每种消息对应一位 能把ICMP消息过滤掉
		---------------------------------------------*/
    filt.data = ~((1<<ICMP_SOURCE_QUENCH)|
                  (1<<ICMP_DEST_UNREACH)|
                  (1<<ICMP_TIME_EXCEEDED)|
                  (1<<ICMP_PARAMETERPROB)|
                  (1<<ICMP_REDIRECT)|
                  (1<<ICMP_ECHOREPLY));
    // 设置socket的过滤器选项
    if (setsockopt(icmp_sock, SOL_RAW, ICMP_FILTER, (char*)&filt, sizeof(filt)) == -1)
        perror("WARNING: setsockopt(ICMP_FILTER)");
}

hold = 1;
// 设置socket 允许传递拓展的可靠错误信息
if (setsockopt(icmp_sock, SOL_IP, IP_RECVERR, (char *)&hold, sizeof(hold)))
    fprintf(stderr, "WARNING: your kernel is veeery old. No problems.\n");

/* record route option */
//route选项
if (options & F_RROUTE) {
    memset(rspace, 0, sizeof(rspace));
    rspace[0] = IPOPT_NOP;            //IPOPT_NOP 没有操作
    rspace[1+IPOPT_OPTVAL] = IPOPT_RR;//IPOPT_RR 路由信息
    rspace[1+IPOPT_OLEN] = sizeof(rspace)-1;//IPOPT_OLEN 选项长度
    rspace[1+IPOPT_OFFSET] = IPOPT_MINOFF;//
    optlen = 40;

    //设置socket的IP选项
    if (setsockopt(icmp_sock, IPPROTO_IP, IP_OPTIONS, rspace, sizeof(rspace)) < 0) {
        perror("ping: record route");
        exit(2);
    }
}

// 时间戳
if (options & F_TIMESTAMP) {
    memset(rspace, 0, sizeof(rspace));
    rspace[0] = IPOPT_TIMESTAMP;// 指明IP选项的类型是时间戳
    rspace[1] = (ts_type==IPOPT_TS_TSONLY ? 40 : 36);
    rspace[2] = 5;
    rspace[3] = ts_type; //将标志字段设置为对应的时间戳选项

    //如果是-T tsprespec [host1 [host2 [host3 [host4]]]] 选项预先存入各主机地址
    if (ts_type == IPOPT_TS_PRESPEC) {
        int i;
        rspace[1] = 4+nroute*8;
        for (i=0; i<nroute; i++)
            *(__u32*)&rspace[4+i*8] = route[i];
    }

    //设置socket 接口设置
    if (setsockopt(icmp_sock, IPPROTO_IP, IP_OPTIONS, rspace, rspace[1]) < 0) {
        rspace[3] = 2;
        if (setsockopt(icmp_sock, IPPROTO_IP, IP_OPTIONS, rspace, rspace[1]) < 0) {
            perror("ping: ts option");
            exit(2);
        }
    }
    optlen = 40;
}

// source route
if (options & F_SOURCEROUTE) {
    int i;
    memset(rspace, 0, sizeof(rspace));
    rspace[0] = IPOPT_NOOP;
    rspace[1+IPOPT_OPTVAL] = (options & F_SO_DONTROUTE) ? IPOPT_SSRR
        : IPOPT_LSRR;
    rspace[1+IPOPT_OLEN] = 3 + nroute*4;
    rspace[1+IPOPT_OFFSET] = IPOPT_MINOFF;
    for (i=0; i<nroute; i++)
        *(__u32*)&rspace[4+i*4] = route[i];

    if (setsockopt(icmp_sock, IPPROTO_IP, IP_OPTIONS, rspace, 4 + nroute*4) < 0) {
        perror("ping: record route");
        exit(2);
    }
    optlen = 40;
}

/* Estimate memory eaten by single packet. It is rough estimate.
	 * Actually, for small datalen's it depends on kernel side a lot. */
hold = datalen + 8;/*ICMP报文的大小*/
hold += ((hold+511)/512)*(optlen + 20 + 16 + 64 + 160);
sock_setbufs(icmp_sock, hold);

//广播数据报
if (broadcast_pings) {
    // 设置socket 运行发送广播数据报
    if (setsockopt(icmp_sock, SOL_SOCKET, SO_BROADCAST,
                   &broadcast_pings, sizeof(broadcast_pings)) < 0) {
        perror ("ping: can't set broadcasting");
        exit(2);
    }
}

// 多播数据报回填
if (options & F_NOLOOP) {
    int loop = 0;
    //设置socket 禁止多播数据包回填
    if (setsockopt(icmp_sock, IPPROTO_IP, IP_MULTICAST_LOOP,
                   &loop, 1) == -1) {
        perror ("ping: can't disable multicast loopback");
        exit(2);
    }
}

// TTL设置
if (options & F_TTL) {
    int ittl = ttl;

    //设置输出组播数据的TTL值
    //IP_MULTICAST_TTL: 为输出组播数据报设置TTL值
    if (setsockopt(icmp_sock, IPPROTO_IP, IP_MULTICAST_TTL,
                   &ttl, 1) == -1) {
        perror ("ping: can't set multicast time-to-live");
        exit(2);
    }
    //设置socket指定TTL存活时间
    if (setsockopt(icmp_sock, IPPROTO_IP, IP_TTL,
                   &ittl, sizeof(ittl)) == -1) {
        perror ("ping: can't set unicast time-to-live");
        exit(2);
    }
}

之后判断ICMP报文的长度是否超出了最大长度。

/******************************************************
	/*			最后对判断ICMP数据报是否超出长度
	/*		
	*******************************************************/

// 超出了最大长度
// 2^16(长度字段16bit)-8(ICMP头部)-20(IP固定头部)-optlen(IP选项长度)
if (datalen > 0xFFFF - 8 - optlen - 20) {

    // 超级用户可以超过上面那个长度设置 但是不能超过 outpack-8
    if (uid || datalen > sizeof(outpack)-8) {
        fprintf(stderr, "Error: packet size %d is too large. Maximum is %d\n", datalen, 0xFFFF-8-20-optlen);
        exit(2);
    }
    /* Allow small oversize to root yet. It will cause EMSGSIZE. */
    fprintf(stderr, "WARNING: packet size %d is too large. Maximum is %d\n", datalen, 0xFFFF-8-20-optlen);
}

if (datalen >= sizeof(struct timeval))	/* can we time transfer */
    timing = 1;
packlen = datalen + MAXIPLEN + MAXICMPLEN;
if (!(packet = (u_char *)malloc((u_int)packlen))) {
    fprintf(stderr, "ping: out of memory.\n");
    exit(2);
}

最后调用setup函数设置公共的套接字选项,再调用main_loop进入循环执行发送和接收报文程序。

    //其他与协议无关的参数设置和选项设置  【后面不再详细分析】
	setup(icmp_sock);

	//进入main_loop 主循环 发送和接收报文
	main_loop(icmp_sock, packet, packlen);
}

4.2.2 main_loop函数

该函数是在报文参数等已经设置好之后,进入主要的逻辑。进行报文的发送和回复报文的接收。

首先其参数有 icmp套接字,包内容packet 和长度 packlen。

void main_loop(int icmp_sock, __u8 *packet, int packlen)
{
	char addrbuf[128];
	char ans_data[4096];  //回答数据
	struct iovec iov;     //io向量
	struct msghdr msg;     //消息头部
	struct cmsghdr *c;
	int cc;                //用于接受recvmsg返回值
	int next;              // 下一个报文
	int polling;           //是否阻塞

	iov.iov_base = (char *)packet; //将包内容转化为io向量

之后是一个for(;;)无限循环主体,循环主体中首先是判断退出条件。

for (;;) {

		/*-----------------------------------
		/*          检查退出
		/*各种退出原因 1.中断 2.接收包超过限制 3.超出时间限制 4.SIGQUIT
		-------------------------------------*/
		
		/* Check exit conditions. */
		// 检查中断 有中断退出
		if (exiting)
			break;
		// 接收回复超线 退出
		if (npackets && nreceived + nerrors >= npackets)
			break;
		// 超出时间限制 退出
		if (deadline && nerrors)
			break;
		/* Check for and do special actions. */
		// SIGQUIT中断 退出
		if (status_snapshot)
			status(); // 打印信息 退出

如果没有退出,则调用pinger函数来发送报文。

/* Send probes scheduled to this time. */
do {
    next = pinger();/*编写和传输ICMP echo 请求数据包*/
    next = schedule_exit(next); /*计算下一次发送探针的时间*/
} while (next <= 0);//如果时间紧迫 尽快发送

报文发送默认为阻塞模式,但是在自适应模式下等情况下需要进行调整。

polling = 0;// 默认为阻塞
if ((options & (F_ADAPTIVE|F_FLOOD_POLL)) || next<SCHINT(interval)) {
    int recv_expected = in_flight();

    /* If we are here, recvmsg() is unable to wait for
			 * required timeout. */
    if (1000 % HZ == 0 ? next <= 1000 / HZ : (next < INT_MAX / HZ && next * HZ <= 1000)) {
        /* Very short timeout... So, if we wait for
				 * something, we sleep for MININTERVAL.
				 * Otherwise, spin! */
        if (recv_expected) {
            next = MININTERVAL;
        } else {
            next = 0;
            /* When spinning, no reasons to poll.
					 * Use nonblocking recvmsg() instead. */
            polling = MSG_DONTWAIT; //设置为不可阻断运行
            /* But yield yet. */
            sched_yield();
        }
    }

    if (!polling &&
        ((options & (F_ADAPTIVE|F_FLOOD_POLL)) || interval)) {
        struct pollfd pset;
        pset.fd = icmp_sock;
        pset.events = POLLIN|POLLERR;
        pset.revents = 0;
        if (poll(&pset, 1, next) < 1 ||
            !(pset.revents&(POLLIN|POLLERR)))
            continue;
        polling = MSG_DONTWAIT;
    }
}

之后是又一个循环,用来接收回复报文。

for (;;) {
    struct timeval *recv_timep = NULL;
    struct timeval recv_time; //接收到的时间
    int not_ours = 0; //其他进程的 接收到的
	
    /* 对msg消息进行设置*/
    iov.iov_len = packlen;
    memset(&msg, 0, sizeof(msg));
    msg.msg_name = addrbuf; 
    msg.msg_namelen = sizeof(addrbuf);
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = ans_data;
    msg.msg_controllen = sizeof(ans_data);


    //接收消息
    cc = recvmsg(icmp_sock, &msg, polling);
    polling = MSG_DONTWAIT;// 第二次设置为不可阻断运作

    // 没有接收到消息
    if (cc < 0) {
        // 错误类型为 EAGAIN: 再试一次 EINTR: 数据准备好之前 接受者被中断
        if (errno == EAGAIN || errno == EINTR)
            break;
        // 调用receive_error_msg()处理错误报文
        if (!receive_error_msg()) {
            if (errno) {
                perror("ping: recvmsg");
                break;
            }
            not_ours = 1;
        }
    } else {
        //F_LATENCY 延迟 ipv6
        if ((options&F_LATENCY) || recv_timep == NULL) {
            if ((options&F_LATENCY) ||
                ioctl(icmp_sock, SIOCGSTAMP, &recv_time))
                gettimeofday(&recv_time, NULL);
            recv_timep = &recv_time;
        }

        not_ours = parse_reply(&msg, cc, addrbuf, recv_timep);
    }

    // 还有其他人再使用 多用户系统
    if (not_ours)
        install_filter();
    if (in_flight() == 0)
        break;//返回到pinger
    }
}

如果程序跳出,则调用finish函数进行打印统计信息之后进行退出。

	/*------------------------------------------------
	        输出数据然后退出
	-------------------------------------------------*/
	finish(); //逻辑简单 后面不再详细分析
}

4.2.3 pinger函数

pinger函数用来编写和发送ICMP报文。

/* 编写和传输ICMP echo 请求数据包*/

int pinger(void)
{
	static int oom_count;
	static int tokens;
	int i;

首先先对时间片进行判断处理。

/****************************************************
/*               时间间隔 时间片
/*
****************************************************/

/* Have we already sent enough? If we have, return an arbitrary positive value. */
/* 我们已经送够了吗?如果有,返回一个任意的正值。*/

// 返回一个超级大的数 确保main_loop有时间来判断是否退出
if (exiting || (npackets && ntransmitted >= npackets && !deadline))
    return 1000;

/* Check that packets < rate*time + preload */
/* 检查数据包<速率*时间+预加载*/

if (cur_time.tv_sec == 0) {
    // 第一次执行 
    gettimeofday(&cur_time, NULL);//初始化cur_time 为当前时间
    tokens = interval*(preload-1);//初始化时间片 发送一个报文需要的时间
} else {
    // 不是第一次
    long ntokens;
    struct timeval tv;

    // 当前时刻与上一次报文的时间间隔 没有接收报文则被忽略
    gettimeofday(&tv, NULL);
    ntokens = (tv.tv_sec - cur_time.tv_sec)*1000 +
        (tv.tv_usec-cur_time.tv_usec)/1000;

    /* interval 默认值1000 -i 参数*/
    if (!interval) {
        /* Case of unlimited flood is special;
			 * if we see no reply, they are limited to 100pps */
        // 如果没有等到等到间隔时间 并且有preload个报文在传输
        // 则等待一会在发送
        if (ntokens < MININTERVAL && in_flight() >= preload)
            return MININTERVAL-ntokens;
    }
    ntokens += tokens;

    // 分配的时间片不能发送超过preload-1个报文
    if (ntokens > interval*preload)
        ntokens = interval*preload;
    // 剩下的时间片不足一个间隔
    if (ntokens < interval)
        return interval - ntokens;// 不再发送 开始接收报文

    cur_time = tv;
    tokens = ntokens - interval;
}

处理好之后调用send_probe函数来发送ICMP报文探针,使用i来接收返回值。其在标签resend下,可以进行重试。

resend:
	i = send_probe(); //发送ICMP消息

如果返回值正确,如果非静默模式且洪泛模式下输出一堆点。之后返回退出。

    //发送成功
	if (i == 0) {
		oom_count = 0;
		advance_ntransmitted();
		// 非静默模式 且洪泛模式
		if (!(options & F_QUIET) && (options & F_FLOOD)) {
			/* Very silly, but without this output with
			 * high preload or pipe size is very confusing. */
			if ((preload < screen_width && pipesize < screen_width) ||
			    in_flight() < screen_width)
				write_stdout(".", 1); // 输出一堆点 来代表多少个报文没有回答
		}
		return interval - tokens;// 是否还有多余时间片
	}

如果返回值不正确,则根据具体情况处理。errno保存了一些错误信息。如果是错误报文,则使用receive_error_msg函数来处理。

// i>0 致命的BUG
if (i > 0) {
    /* Apparently, it is some fatal bug. */
    abort();
} 
//ENOBUFS:输出网络接口缓存满了
//ENOMEM:没有内存了
else if (errno == ENOBUFS || errno == ENOMEM) {
    int nores_interval;

    /* Device queue overflow or OOM. Packet is not sent. */
    /* 内存不够或者缓冲满了*/
    tokens = 0;
    /* Slowdown. This works only in adaptive mode (option -A) */
    /* 减慢发送速度 TTL适应模式*/
    rtt_addend += (rtt < 8*50000 ? rtt/8 : 50000);
    if (options&F_ADAPTIVE)
        update_interval();
    nores_interval = SCHINT(interval/2);
    if (nores_interval > 500)
        nores_interval = 500;
    oom_count++;
    if (oom_count*nores_interval < lingertime)
        return nores_interval;
    i = 0;
}
// socket 缓冲区满了
else if (errno == EAGAIN) {
    /* Socket buffer is full. */
    tokens += interval;
    return MININTERVAL;
} else {
    // ICMP错误报文
    if ((i=receive_error_msg()) > 0) {
        /* An ICMP error arrived. */
        tokens += interval;
        return MININTERVAL;
    }
    /* Compatibility with old linuces. */
    if (i == 0 && confirm_flag && errno == EINVAL) {
        confirm_flag = 0;
        errno = 0;
    }
    if (!errno)
        goto resend; //重新发送
}

之后进行一些结尾操作结束发送报文。

/* 本地错误 发送了数据包*/
	advance_ntransmitted();

	//静默模式
	if (i == 0 && !(options & F_QUIET)) {
		if (options & F_FLOOD)
			write_stdout("E", 1);
		else
			perror("ping: sendmsg");
	}
	tokens = 0;
	return SCHINT(interval);
}

4.2.4 recive_error_msg函数

该函数用来处理ICMP错误报文。

/* 接收错误报文*/
int receive_error_msg()
{
	int res;
	char cbuf[512];
	struct iovec  iov;  //io向量
	struct msghdr msg;  //消息
	struct cmsghdr *cmsg;
	struct sock_extended_err *e; //套接字错误
	struct icmphdr icmph;  //icmp报文头部
	struct sockaddr_in target;// 目标主机地址
	int net_errors = 0;  //计数 错误次数
	int local_errors = 0;//计数 本地错误次数
	int saved_errno = errno; // 保存错误编码
	
    /* 设置msg*/
	iov.iov_base = &icmph;
	iov.iov_len = sizeof(icmph);
	msg.msg_name = (void*)&target;
	msg.msg_namelen = sizeof(target);
	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;
	msg.msg_flags = 0;
	msg.msg_control = cbuf;
	msg.msg_controllen = sizeof(cbuf);

使用recvmsg函数来接收icmp_sockt回复消息,保存在msg结构体中。

//接收消息 MSG_DONTWAIT 不可打断
res = recvmsg(icmp_sock, &msg, MSG_ERRQUEUE|MSG_DONTWAIT);
if (res < 0)
    goto out;

之后读取错误信息类型,取最后一个。

e = NULL;
for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
    if (cmsg->cmsg_level == SOL_IP) {
        if (cmsg->cmsg_type == IP_RECVERR)
            // sock_extended_err: 错误描述
            // 通过 IP_RECVERR SOL_IP信息来进行传递
            e = (struct sock_extended_err *)CMSG_DATA(cmsg);
    }
}
if (e == NULL)
    abort();

之后根据不同的错误类型来进行不同的错误处理。主要为本地错误和ICMP差错报文错误。

// 本地产生错误
if (e->ee_origin == SO_EE_ORIGIN_LOCAL) {
    local_errors++;
    if (options & F_QUIET)
        goto out;
    if (options & F_FLOOD)
        write_stdout("E", 1);
    else if (e->ee_errno != EMSGSIZE)
        fprintf(stderr, "ping: local error: %s\n", strerror(e->ee_errno));
    else
        fprintf(stderr, "ping: local error: Message too long, mtu=%u\n", e->ee_info);
    nerrors++;
} 
// ICMP差错报文
else if (e->ee_origin == SO_EE_ORIGIN_ICMP) {
    struct sockaddr_in *sin = (struct sockaddr_in*)(e+1);

    if (res < sizeof(icmph) ||
        target.sin_addr.s_addr != whereto.sin_addr.s_addr ||
        icmph.type != ICMP_ECHO ||
        icmph.un.echo.id != ident) {
        /* Not our error, not an error at all. Clear. */
        saved_errno = 0;
        goto out;
    }

    //ICMP差错报文的源主机地址和本机主机地址、ICMP类型和ICMP_ECHO、报文中的标识符和本进程ID都符合
    acknowledge(ntohs(icmph.un.echo.sequence));

    if (!working_recverr) {
        struct icmp_filter filt;
        working_recverr = 1;
        /* OK, it works. Add stronger filter. */
        // 如果是网络出错则安装一个更加严格的过滤器
        filt.data = ~((1<<ICMP_SOURCE_QUENCH)|
                      (1<<ICMP_REDIRECT)|
                      (1<<ICMP_ECHOREPLY));
        if (setsockopt(icmp_sock, SOL_RAW, ICMP_FILTER, (char*)&filt, sizeof(filt)) == -1)
            perror("\rWARNING: setsockopt(ICMP_FILTER)");
    }

    net_errors++;
    nerrors++;

    /*********************
    *       不同模式
    **********************/

    //静默模式 退出
    if (options & F_QUIET)
        goto out;
    // 洪泛模式 
    if (options & F_FLOOD) {
        write_stdout("\bE", 2);
    } else {
        print_timestamp();
        // ICMP差错报文源主机地址,序列号
        printf("From %s icmp_seq=%u ", pr_addr(sin->sin_addr.s_addr), ntohs(icmph.un.echo.sequence));
        //分析并打印出网络出错原因
        pr_icmph(e->ee_type, e->ee_code, e->ee_info, NULL);
        fflush(stdout);
    }
}

最后部分为退出处理,将errno恢复为开始的值 并返回错误类型。

out:
	errno = saved_errno;
	return net_errors ? : -local_errors;
}

4.2.5 parse_reply函数

该函数用来解析ICMP回复报文。

int parse_reply(struct msghdr *msg, int cc, void *addr, struct timeval *tv)
{
	struct sockaddr_in *from = addr; //来源地址
	__u8 *buf = msg->msg_iov->iov_base; //设置buf位置
	struct icmphdr *icp;  //ICMP报文头部
	struct iphdr *ip;     //IP报文头部
	int hlen;
	int csfailed;

首先先提取IP头部信息,检查报文长度正不正确。

//检查IP头部
ip = (struct iphdr *)buf;
hlen = ip->ihl*4;//ip报文长度
if (cc < hlen + 8 || ip->ihl < 5) {
    if (options & F_VERBOSE)
        fprintf(stderr, "ping: packet too short (%d bytes) from %s\n", cc,
                pr_addr(from->sin_addr.s_addr));
    return 1;
}

之后提取ICMP报文头部信息,检查校验和。

// 检测ICMP 校验和
cc -= hlen;
icp = (struct icmphdr *)(buf + hlen);
csfailed = in_cksum((u_short *)icp, cc, 0);

如果是ICMP回复报文类型,则依据是不是回复本进程ID来进行不同的处理。之后进行退出。

/*-------------------------------------------
/*	是ICMP的回复
-------------------------------------------**/
if (icp->type == ICMP_ECHOREPLY) {
    //ICMP的回复的ID不是本进程的ID
    if (icp->un.echo.id != ident)
        return 1;			/* 'Twas not our ECHO */
    //是本进程的ICMP的回复
    if (gather_statistics((__u8*)icp, sizeof(*icp), cc,
                          ntohs(icp->un.echo.sequence),
                          ip->ttl, 0, tv, pr_addr(from->sin_addr.s_addr),
                          pr_echo_reply))
        return 0;
}

如果不是ICMP回复报文类型,则使用switch结构来依据不同的报文类型来进行处理。

/*-------------------------------------------
/*	如果不是ICMP的回复
-------------------------------------------**/
	
else {
    switch (icp->type) {
        case ICMP_ECHO:
            /* MUST NOT */
            return 1;
        case ICMP_SOURCE_QUENCH:
        case ICMP_REDIRECT:
        case ICMP_DEST_UNREACH:
        case ICMP_TIME_EXCEEDED:
        case ICMP_PARAMETERPROB:
            {
               /*具体处理省略*/
            }
        default:
            /* MUST NOT */
            break;
    }

最后根据不同的模式进行结尾处理退出。

	if (!(options & F_FLOOD)) {
        //非洪泛类型
		pr_options(buf + sizeof(struct iphdr), hlen);

		if (options & F_AUDIBLE)
			putchar('\a');
		putchar('\n');
		fflush(stdout);
	} else {
		putchar('\a');
		fflush(stdout);//校验和错误
	}
	return 0;
}

你可能感兴趣的:(计算机网络梳理,网络,linux)