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报文有着不同的报文结构,但是都有着如下的共同结构:

  • 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 编译测试

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

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

2.5 分析对象

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

3. 源码概要分析

3.1 逻辑功能流程

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

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

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程序流程中的开始入口。其完成了如下的几个功能:

  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 主要函数间调用关系

#mermaid-svg-nZ8rqcD2kRBQOvyS .label{font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family);fill:#333;color:#333}#mermaid-svg-nZ8rqcD2kRBQOvyS .label text{fill:#333}#mermaid-svg-nZ8rqcD2kRBQOvyS .node rect,#mermaid-svg-nZ8rqcD2kRBQOvyS .node circle,#mermaid-svg-nZ8rqcD2kRBQOvyS .node ellipse,#mermaid-svg-nZ8rqcD2kRBQOvyS .node polygon,#mermaid-svg-nZ8rqcD2kRBQOvyS .node path{fill:#ECECFF;stroke:#9370db;stroke-width:1px}#mermaid-svg-nZ8rqcD2kRBQOvyS .node .label{text-align:center;fill:#333}#mermaid-svg-nZ8rqcD2kRBQOvyS .node.clickable{cursor:pointer}#mermaid-svg-nZ8rqcD2kRBQOvyS .arrowheadPath{fill:#333}#mermaid-svg-nZ8rqcD2kRBQOvyS .edgePath .path{stroke:#333;stroke-width:1.5px}#mermaid-svg-nZ8rqcD2kRBQOvyS .flowchart-link{stroke:#333;fill:none}#mermaid-svg-nZ8rqcD2kRBQOvyS .edgeLabel{background-color:#e8e8e8;text-align:center}#mermaid-svg-nZ8rqcD2kRBQOvyS .edgeLabel rect{opacity:0.9}#mermaid-svg-nZ8rqcD2kRBQOvyS .edgeLabel span{color:#333}#mermaid-svg-nZ8rqcD2kRBQOvyS .cluster rect{fill:#ffffde;stroke:#aa3;stroke-width:1px}#mermaid-svg-nZ8rqcD2kRBQOvyS .cluster text{fill:#333}#mermaid-svg-nZ8rqcD2kRBQOvyS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family);font-size:12px;background:#ffffde;border:1px solid #aa3;border-radius:2px;pointer-events:none;z-index:100}#mermaid-svg-nZ8rqcD2kRBQOvyS .actor{stroke:#ccf;fill:#ECECFF}#mermaid-svg-nZ8rqcD2kRBQOvyS text.actor>tspan{fill:#000;stroke:none}#mermaid-svg-nZ8rqcD2kRBQOvyS .actor-line{stroke:grey}#mermaid-svg-nZ8rqcD2kRBQOvyS .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333}#mermaid-svg-nZ8rqcD2kRBQOvyS .messageLine1{stroke-width:1.5;stroke-dasharray:2, 2;stroke:#333}#mermaid-svg-nZ8rqcD2kRBQOvyS #arrowhead path{fill:#333;stroke:#333}#mermaid-svg-nZ8rqcD2kRBQOvyS .sequenceNumber{fill:#fff}#mermaid-svg-nZ8rqcD2kRBQOvyS #sequencenumber{fill:#333}#mermaid-svg-nZ8rqcD2kRBQOvyS #crosshead path{fill:#333;stroke:#333}#mermaid-svg-nZ8rqcD2kRBQOvyS .messageText{fill:#333;stroke:#333}#mermaid-svg-nZ8rqcD2kRBQOvyS .labelBox{stroke:#ccf;fill:#ECECFF}#mermaid-svg-nZ8rqcD2kRBQOvyS .labelText,#mermaid-svg-nZ8rqcD2kRBQOvyS .labelText>tspan{fill:#000;stroke:none}#mermaid-svg-nZ8rqcD2kRBQOvyS .loopText,#mermaid-svg-nZ8rqcD2kRBQOvyS .loopText>tspan{fill:#000;stroke:none}#mermaid-svg-nZ8rqcD2kRBQOvyS .loopLine{stroke-width:2px;stroke-dasharray:2, 2;stroke:#ccf;fill:#ccf}#mermaid-svg-nZ8rqcD2kRBQOvyS .note{stroke:#aa3;fill:#fff5ad}#mermaid-svg-nZ8rqcD2kRBQOvyS .noteText,#mermaid-svg-nZ8rqcD2kRBQOvyS .noteText>tspan{fill:#000;stroke:none}#mermaid-svg-nZ8rqcD2kRBQOvyS .activation0{fill:#f4f4f4;stroke:#666}#mermaid-svg-nZ8rqcD2kRBQOvyS .activation1{fill:#f4f4f4;stroke:#666}#mermaid-svg-nZ8rqcD2kRBQOvyS .activation2{fill:#f4f4f4;stroke:#666}#mermaid-svg-nZ8rqcD2kRBQOvyS .mermaid-main-font{font-family:"trebuchet ms", verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-nZ8rqcD2kRBQOvyS .section{stroke:none;opacity:0.2}#mermaid-svg-nZ8rqcD2kRBQOvyS .section0{fill:rgba(102,102,255,0.49)}#mermaid-svg-nZ8rqcD2kRBQOvyS .section2{fill:#fff400}#mermaid-svg-nZ8rqcD2kRBQOvyS .section1,#mermaid-svg-nZ8rqcD2kRBQOvyS .section3{fill:#fff;opacity:0.2}#mermaid-svg-nZ8rqcD2kRBQOvyS .sectionTitle0{fill:#333}#mermaid-svg-nZ8rqcD2kRBQOvyS .sectionTitle1{fill:#333}#mermaid-svg-nZ8rqcD2kRBQOvyS .sectionTitle2{fill:#333}#mermaid-svg-nZ8rqcD2kRBQOvyS .sectionTitle3{fill:#333}#mermaid-svg-nZ8rqcD2kRBQOvyS .sectionTitle{text-anchor:start;font-size:11px;text-height:14px;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-nZ8rqcD2kRBQOvyS .grid .tick{stroke:#d3d3d3;opacity:0.8;shape-rendering:crispEdges}#mermaid-svg-nZ8rqcD2kRBQOvyS .grid .tick text{font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-nZ8rqcD2kRBQOvyS .grid path{stroke-width:0}#mermaid-svg-nZ8rqcD2kRBQOvyS .today{fill:none;stroke:red;stroke-width:2px}#mermaid-svg-nZ8rqcD2kRBQOvyS .task{stroke-width:2}#mermaid-svg-nZ8rqcD2kRBQOvyS .taskText{text-anchor:middle;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-nZ8rqcD2kRBQOvyS .taskText:not([font-size]){font-size:11px}#mermaid-svg-nZ8rqcD2kRBQOvyS .taskTextOutsideRight{fill:#000;text-anchor:start;font-size:11px;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-nZ8rqcD2kRBQOvyS .taskTextOutsideLeft{fill:#000;text-anchor:end;font-size:11px}#mermaid-svg-nZ8rqcD2kRBQOvyS .task.clickable{cursor:pointer}#mermaid-svg-nZ8rqcD2kRBQOvyS .taskText.clickable{cursor:pointer;fill:#003163 !important;font-weight:bold}#mermaid-svg-nZ8rqcD2kRBQOvyS .taskTextOutsideLeft.clickable{cursor:pointer;fill:#003163 !important;font-weight:bold}#mermaid-svg-nZ8rqcD2kRBQOvyS .taskTextOutsideRight.clickable{cursor:pointer;fill:#003163 !important;font-weight:bold}#mermaid-svg-nZ8rqcD2kRBQOvyS .taskText0,#mermaid-svg-nZ8rqcD2kRBQOvyS .taskText1,#mermaid-svg-nZ8rqcD2kRBQOvyS .taskText2,#mermaid-svg-nZ8rqcD2kRBQOvyS .taskText3{fill:#fff}#mermaid-svg-nZ8rqcD2kRBQOvyS .task0,#mermaid-svg-nZ8rqcD2kRBQOvyS .task1,#mermaid-svg-nZ8rqcD2kRBQOvyS .task2,#mermaid-svg-nZ8rqcD2kRBQOvyS .task3{fill:#8a90dd;stroke:#534fbc}#mermaid-svg-nZ8rqcD2kRBQOvyS .taskTextOutside0,#mermaid-svg-nZ8rqcD2kRBQOvyS .taskTextOutside2{fill:#000}#mermaid-svg-nZ8rqcD2kRBQOvyS .taskTextOutside1,#mermaid-svg-nZ8rqcD2kRBQOvyS .taskTextOutside3{fill:#000}#mermaid-svg-nZ8rqcD2kRBQOvyS .active0,#mermaid-svg-nZ8rqcD2kRBQOvyS .active1,#mermaid-svg-nZ8rqcD2kRBQOvyS .active2,#mermaid-svg-nZ8rqcD2kRBQOvyS .active3{fill:#bfc7ff;stroke:#534fbc}#mermaid-svg-nZ8rqcD2kRBQOvyS .activeText0,#mermaid-svg-nZ8rqcD2kRBQOvyS .activeText1,#mermaid-svg-nZ8rqcD2kRBQOvyS .activeText2,#mermaid-svg-nZ8rqcD2kRBQOvyS .activeText3{fill:#000 !important}#mermaid-svg-nZ8rqcD2kRBQOvyS .done0,#mermaid-svg-nZ8rqcD2kRBQOvyS .done1,#mermaid-svg-nZ8rqcD2kRBQOvyS .done2,#mermaid-svg-nZ8rqcD2kRBQOvyS .done3{stroke:grey;fill:#d3d3d3;stroke-width:2}#mermaid-svg-nZ8rqcD2kRBQOvyS .doneText0,#mermaid-svg-nZ8rqcD2kRBQOvyS .doneText1,#mermaid-svg-nZ8rqcD2kRBQOvyS .doneText2,#mermaid-svg-nZ8rqcD2kRBQOvyS .doneText3{fill:#000 !important}#mermaid-svg-nZ8rqcD2kRBQOvyS .crit0,#mermaid-svg-nZ8rqcD2kRBQOvyS .crit1,#mermaid-svg-nZ8rqcD2kRBQOvyS .crit2,#mermaid-svg-nZ8rqcD2kRBQOvyS .crit3{stroke:#f88;fill:red;stroke-width:2}#mermaid-svg-nZ8rqcD2kRBQOvyS .activeCrit0,#mermaid-svg-nZ8rqcD2kRBQOvyS .activeCrit1,#mermaid-svg-nZ8rqcD2kRBQOvyS .activeCrit2,#mermaid-svg-nZ8rqcD2kRBQOvyS .activeCrit3{stroke:#f88;fill:#bfc7ff;stroke-width:2}#mermaid-svg-nZ8rqcD2kRBQOvyS .doneCrit0,#mermaid-svg-nZ8rqcD2kRBQOvyS .doneCrit1,#mermaid-svg-nZ8rqcD2kRBQOvyS .doneCrit2,#mermaid-svg-nZ8rqcD2kRBQOvyS .doneCrit3{stroke:#f88;fill:#d3d3d3;stroke-width:2;cursor:pointer;shape-rendering:crispEdges}#mermaid-svg-nZ8rqcD2kRBQOvyS .milestone{transform:rotate(45deg) scale(0.8, 0.8)}#mermaid-svg-nZ8rqcD2kRBQOvyS .milestoneText{font-style:italic}#mermaid-svg-nZ8rqcD2kRBQOvyS .doneCritText0,#mermaid-svg-nZ8rqcD2kRBQOvyS .doneCritText1,#mermaid-svg-nZ8rqcD2kRBQOvyS .doneCritText2,#mermaid-svg-nZ8rqcD2kRBQOvyS .doneCritText3{fill:#000 !important}#mermaid-svg-nZ8rqcD2kRBQOvyS .activeCritText0,#mermaid-svg-nZ8rqcD2kRBQOvyS .activeCritText1,#mermaid-svg-nZ8rqcD2kRBQOvyS .activeCritText2,#mermaid-svg-nZ8rqcD2kRBQOvyS .activeCritText3{fill:#000 !important}#mermaid-svg-nZ8rqcD2kRBQOvyS .titleText{text-anchor:middle;font-size:18px;fill:#000;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-nZ8rqcD2kRBQOvyS g.classGroup text{fill:#9370db;stroke:none;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family);font-size:10px}#mermaid-svg-nZ8rqcD2kRBQOvyS g.classGroup text .title{font-weight:bolder}#mermaid-svg-nZ8rqcD2kRBQOvyS g.clickable{cursor:pointer}#mermaid-svg-nZ8rqcD2kRBQOvyS g.classGroup rect{fill:#ECECFF;stroke:#9370db}#mermaid-svg-nZ8rqcD2kRBQOvyS g.classGroup line{stroke:#9370db;stroke-width:1}#mermaid-svg-nZ8rqcD2kRBQOvyS .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5}#mermaid-svg-nZ8rqcD2kRBQOvyS .classLabel .label{fill:#9370db;font-size:10px}#mermaid-svg-nZ8rqcD2kRBQOvyS .relation{stroke:#9370db;stroke-width:1;fill:none}#mermaid-svg-nZ8rqcD2kRBQOvyS .dashed-line{stroke-dasharray:3}#mermaid-svg-nZ8rqcD2kRBQOvyS #compositionStart{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-nZ8rqcD2kRBQOvyS #compositionEnd{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-nZ8rqcD2kRBQOvyS #aggregationStart{fill:#ECECFF;stroke:#9370db;stroke-width:1}#mermaid-svg-nZ8rqcD2kRBQOvyS #aggregationEnd{fill:#ECECFF;stroke:#9370db;stroke-width:1}#mermaid-svg-nZ8rqcD2kRBQOvyS #dependencyStart{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-nZ8rqcD2kRBQOvyS #dependencyEnd{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-nZ8rqcD2kRBQOvyS #extensionStart{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-nZ8rqcD2kRBQOvyS #extensionEnd{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-nZ8rqcD2kRBQOvyS .commit-id,#mermaid-svg-nZ8rqcD2kRBQOvyS .commit-msg,#mermaid-svg-nZ8rqcD2kRBQOvyS .branch-label{fill:lightgrey;color:lightgrey;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-nZ8rqcD2kRBQOvyS .pieTitleText{text-anchor:middle;font-size:25px;fill:#000;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-nZ8rqcD2kRBQOvyS .slice{font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-nZ8rqcD2kRBQOvyS g.stateGroup text{fill:#9370db;stroke:none;font-size:10px;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-nZ8rqcD2kRBQOvyS g.stateGroup text{fill:#9370db;fill:#333;stroke:none;font-size:10px}#mermaid-svg-nZ8rqcD2kRBQOvyS g.statediagram-cluster .cluster-label text{fill:#333}#mermaid-svg-nZ8rqcD2kRBQOvyS g.stateGroup .state-title{font-weight:bolder;fill:#000}#mermaid-svg-nZ8rqcD2kRBQOvyS g.stateGroup rect{fill:#ECECFF;stroke:#9370db}#mermaid-svg-nZ8rqcD2kRBQOvyS g.stateGroup line{stroke:#9370db;stroke-width:1}#mermaid-svg-nZ8rqcD2kRBQOvyS .transition{stroke:#9370db;stroke-width:1;fill:none}#mermaid-svg-nZ8rqcD2kRBQOvyS .stateGroup .composit{fill:white;border-bottom:1px}#mermaid-svg-nZ8rqcD2kRBQOvyS .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px}#mermaid-svg-nZ8rqcD2kRBQOvyS .state-note{stroke:#aa3;fill:#fff5ad}#mermaid-svg-nZ8rqcD2kRBQOvyS .state-note text{fill:black;stroke:none;font-size:10px}#mermaid-svg-nZ8rqcD2kRBQOvyS .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.7}#mermaid-svg-nZ8rqcD2kRBQOvyS .edgeLabel text{fill:#333}#mermaid-svg-nZ8rqcD2kRBQOvyS .stateLabel text{fill:#000;font-size:10px;font-weight:bold;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-nZ8rqcD2kRBQOvyS .node circle.state-start{fill:black;stroke:black}#mermaid-svg-nZ8rqcD2kRBQOvyS .node circle.state-end{fill:black;stroke:white;stroke-width:1.5}#mermaid-svg-nZ8rqcD2kRBQOvyS #statediagram-barbEnd{fill:#9370db}#mermaid-svg-nZ8rqcD2kRBQOvyS .statediagram-cluster rect{fill:#ECECFF;stroke:#9370db;stroke-width:1px}#mermaid-svg-nZ8rqcD2kRBQOvyS .statediagram-cluster rect.outer{rx:5px;ry:5px}#mermaid-svg-nZ8rqcD2kRBQOvyS .statediagram-state .divider{stroke:#9370db}#mermaid-svg-nZ8rqcD2kRBQOvyS .statediagram-state .title-state{rx:5px;ry:5px}#mermaid-svg-nZ8rqcD2kRBQOvyS .statediagram-cluster.statediagram-cluster .inner{fill:white}#mermaid-svg-nZ8rqcD2kRBQOvyS .statediagram-cluster.statediagram-cluster-alt .inner{fill:#e0e0e0}#mermaid-svg-nZ8rqcD2kRBQOvyS .statediagram-cluster .inner{rx:0;ry:0}#mermaid-svg-nZ8rqcD2kRBQOvyS .statediagram-state rect.basic{rx:5px;ry:5px}#mermaid-svg-nZ8rqcD2kRBQOvyS .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#efefef}#mermaid-svg-nZ8rqcD2kRBQOvyS .note-edge{stroke-dasharray:5}#mermaid-svg-nZ8rqcD2kRBQOvyS .statediagram-note rect{fill:#fff5ad;stroke:#aa3;stroke-width:1px;rx:0;ry:0}:root{--mermaid-font-family: '"trebuchet ms", verdana, arial';--mermaid-font-family: "Comic Sans MS", "Comic Sans", cursive}#mermaid-svg-nZ8rqcD2kRBQOvyS .error-icon{fill:#522}#mermaid-svg-nZ8rqcD2kRBQOvyS .error-text{fill:#522;stroke:#522}#mermaid-svg-nZ8rqcD2kRBQOvyS .edge-thickness-normal{stroke-width:2px}#mermaid-svg-nZ8rqcD2kRBQOvyS .edge-thickness-thick{stroke-width:3.5px}#mermaid-svg-nZ8rqcD2kRBQOvyS .edge-pattern-solid{stroke-dasharray:0}#mermaid-svg-nZ8rqcD2kRBQOvyS .edge-pattern-dashed{stroke-dasharray:3}#mermaid-svg-nZ8rqcD2kRBQOvyS .edge-pattern-dotted{stroke-dasharray:2}#mermaid-svg-nZ8rqcD2kRBQOvyS .marker{fill:#333}#mermaid-svg-nZ8rqcD2kRBQOvyS .marker.cross{stroke:#333}:root { --mermaid-font-family: "trebuchet ms", verdana, arial;}#mermaid-svg-nZ8rqcD2kRBQOvyS {color: rgba(0, 0, 0, 0.75);font: ;}

设置共有套接字选项
发送和接收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 设置为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*)&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));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源码分析(超详细,多图,带背景分析)相关推荐

  1. uni-app - 文本展开 / 收起折叠功能,支持自定义样式(当文本内容超出规定行数后,展开收起折叠的功能)兼容 H5 / App / 小程序且易用更容易修改的插件组件源码,超详细的示例代码及注释

    前言 网上的组件和教程代码都太乱了,根本无法按照自己的需求修改,而且基本上都有兼容性和功能性 BUG. 本文实现了 多行文本展开与折叠组件,灵活性非常高,只完成了核心功能,可随意自定义样式满足您的需求 ...

  2. vue3 - 【完整源码】超详细实现网站 / H5 在线预览 pdf 文件功能,支持缩放、旋转、全屏预览、打印、下载、内容检索、主题色定制、侧边缩略图、页码跳转等等(最好用的pdf预览器,注释详细!)

    效果图 在 Vue3.js 项目中,实现了快速高效的 pdf 预览器工具组件,附带详细的使用教程与详细的注释,保证一键复制轻松搞定! 详细的注释很容易二次修改,很多实用功能,你也可以自定义界面上的样式 ...

  3. 5自适应单页源码_超详细!如何建立一个CPA单页网站,附高转化CPA模板源码

    做CPA的老手一般都会建立一个CPA单页站,用来提升转化,提高推广质量. 今天教大家搭建一个完整的CPA单页站 搭建一个网站需要3样东西: 域名 服务器 网站源码 一.购买域名 域名 就是你网站的地址 ...

  4. YOLOv5源码逐行超详细注释与解读(3)——训练部分train.py

    前言 本篇文章主要是对YOLOv5项目的训练部分train.py.通常这个文件主要是用来读取用户自己的数据集,加载模型并训练. 文章代码逐行手打注释,每个模块都有对应讲解,一文帮你梳理整个代码逻辑! ...

  5. YOLOv5源码逐行超详细注释与解读(7)——网络结构(2)common.py

    前言 上一篇我们一起学习了YOLOv5的网络模型之一yolo.py,它这是YOLO的特定模块,而今天要学习另一个和网络搭建有关的文件--common.py,这个文件存放着YOLOv5网络搭建常见的通用 ...

  6. Vue - 实现图片裁剪功能,并上传到服务器(内置第三方最优秀的裁剪图片组件,上传到服务器功能)干净整洁无 BUG 的示例源码与超详细的注释,兼容任意浏览器

    前言 您可以滑动到文章最底部,直接克隆完整示例 Gitee 仓库,与本文教程最终效果一致. 在项目开发中,您难免会遇到图片上传到服务器之前,用户可进行裁剪的需求, 在看了网上大部分教程后,代码都非常乱 ...

  7. Java集合系列---HashMap源码解析(超详细)

    1 HashMap 1)特性: 底层数据结构是数组+链表+红黑树运行null键和null值,,非线程安全,不保证有序,插入和读取顺序不保证一致,不保证有序,在扩容时,元素的顺序会被重新打乱 实现原理: ...

  8. 【Android 性能优化】应用启动优化 ( 安卓应用启动分析 | Launcher 应用简介 | Launcher 应用源码简介 | Launcher 应用快捷方式图标点击方法分析 )

    文章目录 一. Launcher 应用简介 二. Launcher 应用源码简介 三. Launcher 图标点击方法分析 一. Launcher 应用简介 Launcher 应用 : Android ...

  9. CocosCreator像素鸟小游戏实现(有源码)超详细教程 TS实现小游戏 零基础开发

    CocosCreator像素鸟小游戏实现(有源码)超详细教程 TS实现小游戏 大家中秋国庆快乐哈 前言 老规矩先看效果 源码的获取方式在最下面 对于本游戏来说canvas这样设置最佳哦 游戏实现思路: ...

  10. 小程序源码:(自营)独家最新款带部分采集功能壁纸/头像/动态壁纸小程序上线超炫裂变超强支持投稿+视频教程

    新款壁纸表情包头像小程序(dcloud云开发) 支持微信QQ双端小程序也就是说可以打包成微信小程序也可以打包成QQ小程序 相当于一码二用,非常划算 无需授权,源码全开源,支持二开 无需服务器.无需域名 ...

最新文章

  1. Python算法实战系列:栈
  2. 用计算机图形学画字母,r 语言快速出图——单因素方差带字母显著性标记
  3. 写给准备找工作的同志们!!!!(转载)
  4. 应用机器学习进行无人机航拍影像质量评估
  5. 【转】ASP.NET内幕 - IIS处理模型
  6. python -pymysql的操作
  7. 【网络流】 HDU 4309 Seikimatsu Occult Tonneru 状压枚举边
  8. 突破技术管理,IT人中年危机变契机
  9. 鸡头稳如云台_三轴增稳云台是怎么让相机、手机「稳如鸡头」的?
  10. 在Azure Data Studio中查看执行计划
  11. php yii结果集合并,PHP 基础之数组合并
  12. java中Cookie类详解
  13. 缠论入门到精通理论到实战
  14. 公约数和公倍数(Python)
  15. [975]python requests实现HTTPS客户端的证书导入
  16. matlab求偏迹,矩阵的偏迹
  17. 键盘属于计算机主机吗,这是键盘?不,这是一台电脑主机
  18. NETDMIS5.0手动测量——智能识别2023
  19. 计算机组织桌面不见了,教您如果计算机桌面图标不见了怎么办
  20. 5日均线在c语言中的写法,一文学会正确运用5日均线!(图解)

热门文章

  1. 基于栈的字节码解释执行引擎图解
  2. 19-离线词典生成原理、图像描述子用BoW转化为BoW向量和FeatureVe
  3. 国内从事机器视觉领域的公司
  4. BLP防数据泄露安全操作系统:道里云公司参展英特尔北京IDF峰会产品介绍(二)
  5. 推动Web开放生态持续发展 百度正式发布Lavas解决方案
  6. 微信小程序——全国城市列表
  7. 《虚幻4引擎快速入门》视频教程
  8. STM32 高级定时器 输出PWM波
  9. 软件工程课程设计 java_软件工程课程设计——实现一个基本的学生教务系统
  10. vue 微信公众号支付接口_vue项目中使用微信公众号支付的方法有哪些