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报文主要分为两类:一类是差错报文,一类是查询报文。
查询报文:
- ping命令请求与回复报文
YPE | CODE | Description |
---|---|---|
0 | 0 | Echo Reply——回显应答(Ping应答) |
8 | 0 | Echo request——回显请求(Ping请求) |
- 时间戳请求和时间戳答复
YPE | CODE | Description |
---|---|---|
13 | 0 | Timestamp request (obsolete)——时间戳请求(作废不用) |
14 | 0 | Timestamp reply (obsolete)——时间戳应答(作废不用) |
- 路由器请求与通告
YPE | CODE | Description |
---|---|---|
9 | 0 | Router advertisement——路由器通告 |
10 | 0 | Route solicitation——路由器请求 |
- 地址码请求与答复
YPE | CODE | Description |
---|---|---|
17 | 0 | Address mask request——地址掩码请求 |
18 | 0 | Address mask reply——地址掩码应答 |
差错报文:
- 终点不可达,当数据包不能发送到目标主机或路由时,就会丢弃该数据包向源点发送终点不可达报文。
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——优先中止生效 |
- 源点抑制,用于告知源点应该降低发送数据包的速率。
YPE | CODE | Description |
---|---|---|
4 | 0 | Source quench——源端被关闭(基本流控制) |
- 超时 当路由器收到TTL值为0的数据包时,会丢弃该数据包并向源点发送超时报文。
YPE | CODE | Description |
---|---|---|
11 | 0 | TTL equals 0 during transit——传输期间生存时间为0 |
11 | 1 | TTL equals 0 during reassembly——在数据报组装期间生存时间为0 |
- 参数问题,可能是IP首部有的字段值是错误的或者IP首部被修改,破坏都有可能
YPE | CODE | Description |
---|---|---|
12 | 0 | IP header bad (catchall error)——坏的IP首部(包括各种差错) |
12 | 1 | Required options missing——缺少必需的选项 |
- 路由重定向
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报文有着不同的报文结构,但是都有着如下的共同结构:
- TYPE字段 8bits 类型字段
- CODE字段 8bits 提供更多的报文类型信息
- CHECKSUM字段 16bits 校验和字段
1.2.3 ICMP封装过程
ICMP协议是基于IP协议的。ICMP报文被封装在IP报文的数据段中,其封装原理图如下:
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 编译测试
在Linux环境下下载iputils源码。其目录结构如下:
使用make命令进行编译,编译完成后。 运行ping命令测试。可以看到编译通过且程序测试成功。
2.5 分析对象
本次实验以ipv4下的ping程序下的完整流程为分析对象,重点了解ping程序的完整流程以及学习其中网络编程的方法。
3. 源码概要分析
3.1 逻辑功能流程
代码的逻辑功能主要分为1. 使用UDP报文对目标主机进行一个connetc尝试 2. 时候使用循环发送ICMP报文并对回复报文进行处理 这两个主要部分。当遇到中断等条件时候打印信息后进行退出。
3.2 主要数据结构
3.2.1 网络通信地址sockaddr_in
用来表示socket的通信地址,其与sockaddr的区别是将端口和地址进行了区别。
struct sockaddr_in {sa_family_t sin_family; //地址种类 AF_INET代表IPV4u_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_INETint 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 <asm/byteorder.h>"
#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程序流程中的开始入口。其完成了如下的几个功能:
创建ICMP报文;
根据用户的选项参数来循环设置选项标志;
处理用户后面的地址参数,将其保存在route数组中;
对目标地址连接一个UDP报文,获知目标主机的基本情况;
之后设置ICMP报文选项(ipv4特有);
调用ping_common.c/setup()函数来对ICMP报文设置参数(与ipv6共用);
调用ping_common.c/main_loop()函数来完成探测
3.3.2 setup函数 (ping_common.c中)
该函数主要负责对ICMP报文进行设置选项,其是ipv4与ipv6的公共部分。
3.3.3 main_loop函数 (ping_common.c中)
该函数主要负责来循环发送报文,分析报文。在一个for无限循环中,完成了了如下的几个功能:
- 检查是否因为 中断,超出次数限制,超出时间限制,SIGQUIT中断而退出;
- 调用ping.c/pinger()函数来 发送ICMP报文;
- 调用recvmsg()函数来接收报文;
- 如果没有正确接收报文,就调用ping.c/receive_error_msg()函数来处理ICMP差错报文;
- 如果正确处理了接收报文,就调用ping.c/parse_reply()函数来解析ICMP回复报文;
- 如此循环知道不满足条件之后调用ping.c/finish()函数来打印统计信息之后退出;
3.3.4 pinger函数 (ping_common.c中)
该函数用来编写和发送ICMP数据包。
- 调用ping.c/send_probe()函数来组成并发送ICMP回显请求包;
- 发送成功则返回剩余时间并退出;
- 发送失败则根据各种情况处理失败:
- 非常见错误调用abort()函数退出;
- ENOBUFS:输出网络接口缓存满 或者 ENOMEM:没有内存。则减缓发送速度;
- EAGAIN:缓冲区满 则增加时间间隔返回;
- ICMP错误报文 使用 ping.c/receive_error_msg()函数来处理ICMP差错报文;
3.3.5 receive_error_msg函数 (ping.c中)
用来处理ICMP错误报文信息。
- 首先通过 recvmsg()函数来获得接收消息;
- 如果是本地错误,则根据不同的标志情况来处理;
- 否则则是ICMP差错报文信息,如果不是我们的错误,则退出。如果是网络出错,则安装一个更严格的过滤器。之后根据不同模式处理退出。
3.3.6 parse_reply函数(ping.c中)
该函数用来处理ICMP答复报文。
- 首先先检查IP头部 错误则退出;
- 检查ICMP头部,调用ping.c/in_cksum()函数来检测校验和;
- 如果是ICMP回复报文,则根据进程ID判断是不是回复本进程。并做出对应操作之后退出;
- 如果不是ICMP回复报文,则根据不同的报文类型来进行不同的操作。
3.3.7 send_prob函数 (ping.c中)
该函数用来发送ICMP报文。
首先先对ICMP报文头部进行了一些设置:
头部位 设置值 type ICMP_ECHO 回复报文类型 code 0 ckecksum 0 (后面计算) un.echo.sequence htons(ntransmitted+1) 将主机字节序转换为网络字节序 un.echo.id ident 进程ID 之后调用ping.c/in_cksum()函数来计算校验和并写入;
之后嗲用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 主要函数间调用关系
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 设置为0memset((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: ipv4SOCK_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);// 多个routeif (nroute)dst.sin_addr.s_addr = route[0];// 试探主机的基本情况 connectif (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名字复制给 sourcealen = sizeof(source);if (getsockname(probe_fd, (struct sockaddr*)&source, &alen) == -1) {perror("getsockname");exit(2);}// 设置source端口号为0source.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 hintsIP_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 <hostname> 参数 给套接字绑定一个协议地址
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-8if (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 延迟 ipv6if ((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);elseperror("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*)⌖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));elsefprintf(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不是本进程的IDif (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;
}
ping源码分析(超详细,多图,带背景分析)相关推荐
- uni-app - 文本展开 / 收起折叠功能,支持自定义样式(当文本内容超出规定行数后,展开收起折叠的功能)兼容 H5 / App / 小程序且易用更容易修改的插件组件源码,超详细的示例代码及注释
前言 网上的组件和教程代码都太乱了,根本无法按照自己的需求修改,而且基本上都有兼容性和功能性 BUG. 本文实现了 多行文本展开与折叠组件,灵活性非常高,只完成了核心功能,可随意自定义样式满足您的需求 ...
- vue3 - 【完整源码】超详细实现网站 / H5 在线预览 pdf 文件功能,支持缩放、旋转、全屏预览、打印、下载、内容检索、主题色定制、侧边缩略图、页码跳转等等(最好用的pdf预览器,注释详细!)
效果图 在 Vue3.js 项目中,实现了快速高效的 pdf 预览器工具组件,附带详细的使用教程与详细的注释,保证一键复制轻松搞定! 详细的注释很容易二次修改,很多实用功能,你也可以自定义界面上的样式 ...
- 5自适应单页源码_超详细!如何建立一个CPA单页网站,附高转化CPA模板源码
做CPA的老手一般都会建立一个CPA单页站,用来提升转化,提高推广质量. 今天教大家搭建一个完整的CPA单页站 搭建一个网站需要3样东西: 域名 服务器 网站源码 一.购买域名 域名 就是你网站的地址 ...
- YOLOv5源码逐行超详细注释与解读(3)——训练部分train.py
前言 本篇文章主要是对YOLOv5项目的训练部分train.py.通常这个文件主要是用来读取用户自己的数据集,加载模型并训练. 文章代码逐行手打注释,每个模块都有对应讲解,一文帮你梳理整个代码逻辑! ...
- YOLOv5源码逐行超详细注释与解读(7)——网络结构(2)common.py
前言 上一篇我们一起学习了YOLOv5的网络模型之一yolo.py,它这是YOLO的特定模块,而今天要学习另一个和网络搭建有关的文件--common.py,这个文件存放着YOLOv5网络搭建常见的通用 ...
- Vue - 实现图片裁剪功能,并上传到服务器(内置第三方最优秀的裁剪图片组件,上传到服务器功能)干净整洁无 BUG 的示例源码与超详细的注释,兼容任意浏览器
前言 您可以滑动到文章最底部,直接克隆完整示例 Gitee 仓库,与本文教程最终效果一致. 在项目开发中,您难免会遇到图片上传到服务器之前,用户可进行裁剪的需求, 在看了网上大部分教程后,代码都非常乱 ...
- Java集合系列---HashMap源码解析(超详细)
1 HashMap 1)特性: 底层数据结构是数组+链表+红黑树运行null键和null值,,非线程安全,不保证有序,插入和读取顺序不保证一致,不保证有序,在扩容时,元素的顺序会被重新打乱 实现原理: ...
- 【Android 性能优化】应用启动优化 ( 安卓应用启动分析 | Launcher 应用简介 | Launcher 应用源码简介 | Launcher 应用快捷方式图标点击方法分析 )
文章目录 一. Launcher 应用简介 二. Launcher 应用源码简介 三. Launcher 图标点击方法分析 一. Launcher 应用简介 Launcher 应用 : Android ...
- CocosCreator像素鸟小游戏实现(有源码)超详细教程 TS实现小游戏 零基础开发
CocosCreator像素鸟小游戏实现(有源码)超详细教程 TS实现小游戏 大家中秋国庆快乐哈 前言 老规矩先看效果 源码的获取方式在最下面 对于本游戏来说canvas这样设置最佳哦 游戏实现思路: ...
- 小程序源码:(自营)独家最新款带部分采集功能壁纸/头像/动态壁纸小程序上线超炫裂变超强支持投稿+视频教程
新款壁纸表情包头像小程序(dcloud云开发) 支持微信QQ双端小程序也就是说可以打包成微信小程序也可以打包成QQ小程序 相当于一码二用,非常划算 无需授权,源码全开源,支持二开 无需服务器.无需域名 ...
最新文章
- Python算法实战系列:栈
- 用计算机图形学画字母,r 语言快速出图——单因素方差带字母显著性标记
- 写给准备找工作的同志们!!!!(转载)
- 应用机器学习进行无人机航拍影像质量评估
- 【转】ASP.NET内幕 - IIS处理模型
- python -pymysql的操作
- 【网络流】 HDU 4309 Seikimatsu Occult Tonneru 状压枚举边
- 突破技术管理,IT人中年危机变契机
- 鸡头稳如云台_三轴增稳云台是怎么让相机、手机「稳如鸡头」的?
- 在Azure Data Studio中查看执行计划
- php yii结果集合并,PHP 基础之数组合并
- java中Cookie类详解
- 缠论入门到精通理论到实战
- 公约数和公倍数(Python)
- [975]python requests实现HTTPS客户端的证书导入
- matlab求偏迹,矩阵的偏迹
- 键盘属于计算机主机吗,这是键盘?不,这是一台电脑主机
- NETDMIS5.0手动测量——智能识别2023
- 计算机组织桌面不见了,教您如果计算机桌面图标不见了怎么办
- 5日均线在c语言中的写法,一文学会正确运用5日均线!(图解)