大家好,我是春哥,一名拥有10多年Linux后端研发经验的BAT互联网老兵。
在之前一篇文章《长短连接的区别这么答,让面试官眼前一亮!》中春哥挖了一个坑,今天来填坑了。春哥将通过ICMP协议来实现ping的功能。
1.ICMP协议
我们先来回顾一下ICMP协议。
网际控制报文协议(Internet Control Message Protocol,简称ICMP)用于网络差错的报告和网络信息的查询。虽然它是位于IP协议之上的高层协议,但由于它能使网络层及时感知网络的异常,并能对网络中的主机和通讯路由进行探索,因此ICMP协议通常也被归为网络层的协议。
由于ICMP协议是位于IP协议之上,因此ICMP报文是封装在IP数据报内部的,如图1-1所示。
图1-1 封装在IP数据报中的ICMP报文
ICMP报文的格式如图1-2所示。所有的ICMP报文的前4个字节包含着相同的3个字段:1个字节的类型字段、1个字节的代码字段和2个字节的校验和字段。除去前4个字节,ICMP报文的其余内容会因类型和代码的不同而有所不同。
图1-2 ICMP报文格式
常见ICMP报文
ICMP报文可分为查询报文和差错报文两类,常见的ICMP报文如下表1-1所示。
表1-1 ICMP常见报文
| 类型值 | ICMP报文类型 |
|---|---|
| 3 | 目的不可达(差错报文) |
| 4 | 源点抑制(差错报文) |
| 5 | 路由重定向(差错报文) |
| 11 | 超时(差错报文) |
| 12 | 参数错误(差错报文) |
| 9 | 路由信息通告(查询报文) |
| 10 | 路由信息请求(查询报文) |
| 8/0 | 回显请求/应答(查询报文) |
| 13/14 | 时间戳请求/应答(查询报文) |
| 17/18 | 地址掩码请求/应答(查询报文) |
ICMP差错报文能够让网络层及时感知到IP数据报无法交付(目的不可达)、IP数据报被丢弃(源点抑制)、IP路由改变(路由重定向)、IP数据报发送超时(超时)以及IP数据报不合法(参数错误)等异常情况。ICMP差错报文的格式是统一的,具体格式如图1-3所示。它包含出错的IP数据报的首部和IP数据报载荷数据部分的前8个字节。接收到ICMP差错报文的系统可以根据IP数据报首部确定出错的上层协议(是TCP还是UDP),并根据IP数据报载荷数据部分的前8个字节确定系统上关联的用户进程(该部分包含TCP或UDP的源端口)。
图1-3 ICMP差错报文
为了防止ICMP差错报文产生广播风暴,以下几种情况不会产生ICMP差错报文。
- 对ICMP差错报文不再产生ICMP差错报文。
- 如果源地址为"0.0.0.0"、环回地址、多播地址或广播地址,则IP数据报不会产生ICMP差错报文。
- 如果目的地址为多播或广播地址,则IP数据报不会产生ICMP差错报文。
- 如果带有分片标识的IP数据报不是第一个分片,则不会产生ICMP差错报文。
ICMP查询报文使网络层具备以下能力:主机探测(回显请求/应答)、路由更新与查询(路由信息通告/路由信息请求)、时间查询(时间戳请求/应答)以及IP地址掩码查询(地址掩码请求/应答)。
2.实现ping
我们平时用于探测主机是否在线的ping程序,就是通过ICMP协议的回显请求/应答来实现的。ping程序为客户端,被ping的主机为服务端,现在的主机都在内核中直接支持ping服务,接下来让我们自己来实现一个简单的ping程序,在编码之前先看一下ICMP回显请求/应答的报文格式,报文格式如图2-1所示。
图2-1 ICMP回显请求/应答的报文格式
ping客户端程序的逻辑是每隔1秒发送一个ICMP回显请求报文,在接收到ICMP回显应答报文时,将在终端打印应答报文信息。退出时,在终端打印ICMP回显请求与应答的汇总统计信息。
由于ICMP回显应答报文中会原封不动地返回ICMP回显请求报文中的标识符、序号和选项数据,因此在ICMP回显请求报文中,标识符字段使用客户端进程ID填写,用于判断接收到的ICMP回显应答是否为之前发送的ICMP回显请求的应答;序号字段从1开始,每发送一个回显请求报文累加1,用于统计发送ICMP回显请求的个数;至于选项数据则填写一个4字节大小的当前系统时间,用于计算ICMP数据报的RTT(Round Trip Time)大小。
简单ping工具的实现代码在myping.cpp文件中,内容如下。
#include <arpa/inet.h>
#include <assert.h>
#include <errno.h>
#include <netdb.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
typedef void (*signalHanler)(int signo);
const int16_t ICMP_ECHO_TYPE_REQ{8};
const int16_t ICMP_ECHO_TYPE_RESP{0};
const int16_t ICMP_ECHO_CODE{0};
const size_t IP_PROTO_MAX_SIZE{1500};
const size_t ICMP_ECHO_PKT_SIZE{8 + 4 + 4};
bool running{true};
char ipStr[1024]{0};
class PingBase {
public:
static void sigHandler(int signum) { running = false; }
static void signalDeal(int signum, signalHanler handler) {
struct sigaction act;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
assert(0 == sigaction(signum, &act, NULL));
}
static void signalDealReg() {
signalDeal(SIGTERM, sigHandler); // kill进程时,触发的信号
signalDeal(SIGINT, sigHandler); // 进程前台运行时,按ctrl + C触发的信号
signalDeal(SIGQUIT, sigHandler); // 进程前台运行时,按ctrl + \触发的信号
}
static bool getAddrInfo(char *host, struct sockaddr_in *addr) {
if (NULL == host || NULL == addr) return false;
in_addr_t inaddr;
struct hostent *he = NULL;
bzero(addr, sizeof(sockaddr_in));
addr->sin_family = AF_INET;
inaddr = inet_addr(host);
if (inaddr != INADDR_NONE) {
memcpy(&addr->sin_addr, &inaddr, sizeof(inaddr));
} else {
he = gethostbyname(host);
if (NULL == he) {
return false;
} else {
memcpy(&addr->sin_addr, he->h_addr, he->h_length);
}
}
return true;
}
uint16_t getCheckSum(uint8_t *pkt, size_t size) {
uint32_t sum = 0;
uint16_t checkSum = 0;
while (size > 1) {
sum += (*(uint16_t *)pkt);
pkt += 2;
size -= 2;
}
if (1 == size) {
*(uint8_t *)(&checkSum) = *pkt;
sum += checkSum;
}
sum = (sum >> 16) + (sum & 0xffff);
sum += (sum >> 16);
checkSum = ~sum;
return checkSum;
}
void setPid(pid_t pid) { this->pid = pid; }
public:
uint16_t pid{0};
uint16_t sendCount{0};
uint16_t recvCount{0};
double rttMin{0.0};
double rttMax{0.0};
vector<double> rtts{};
struct timeval beginTime;
};
class PingSend : public PingBase {
public:
void setIcmpPkt(uint8_t *pkt) {
uint8_t *checkSum = NULL;
*pkt = ICMP_ECHO_TYPE_REQ; //设置类型字段
++pkt;
*pkt = ICMP_ECHO_CODE; //设置代码字段
++pkt;
checkSum = pkt;
*(uint16_t *)pkt = 0; //校验和先设置为0
pkt += 2;
*(uint16_t *)pkt = htons(pid); //设置标识符
pkt += 2;
*(uint16_t *)pkt = htons(++sendCount); //设置序号
pkt += 2;
//设置选项数据
struct timeval tv;
gettimeofday(&tv, NULL);
*(uint32_t *)pkt = htonl((uint32_t)tv.tv_sec); //设置秒
pkt += 4;
*(uint32_t *)pkt = htonl(tv.tv_usec); //设置微妙
//重新设置校验和
*(uint16_t *)checkSum = getCheckSum(checkSum - 2, ICMP_ECHO_PKT_SIZE);
}
void run(struct sockaddr_in *addr, int32_t wFd) {
int sockFd = 0;
uint8_t pkt[ICMP_ECHO_PKT_SIZE];
sockFd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (sockFd < 0) {
perror("call socket failed!");
write(wFd, &sendCount, sizeof(sendCount));
return;
}
while (running) {
setIcmpPkt(pkt);
sendto(sockFd, pkt, ICMP_ECHO_PKT_SIZE, 0, (struct sockaddr *)addr, (socklen_t)sizeof(*addr));
sleep(1);
}
write(wFd, &sendCount, sizeof(sendCount));
}
};
class PingRecv : public PingBase {
public:
void respStat(double rtt) {
if (rtts.size() <= 0) {
rttMin = rtt;
rttMax = rtt;
}
rttMin = rtt < rttMin ? rtt : rttMin;
rttMax = rtt > rttMax ? rtt : rttMax;
rtts.push_back(rtt);
}
double getIntervalMs(struct timeval begin, struct timeval end) {
if ((end.tv_usec -= begin.tv_usec) < 0) {
end.tv_usec += 1000000;
end.tv_sec -= 1;
}
end.tv_sec -= begin.tv_sec;
return end.tv_sec * 1000.0 + end.tv_usec / 1000.0;
}
double getRtt(uint8_t *icmpOpt) {
struct timeval current;
struct timeval reqSendTime;
gettimeofday(¤t, NULL);
reqSendTime.tv_sec = ntohl(*(uint32_t *)icmpOpt);
reqSendTime.tv_usec = ntohl(*(uint32_t *)(icmpOpt + 4));
return getIntervalMs(reqSendTime, current);
}
double getTotal() {
struct timeval current;
gettimeofday(¤t, NULL);
return getIntervalMs(beginTime, current);
}
void dealResp(uint8_t *pkt, ssize_t len) {
if (len <= 0) return;
//取IP首部长度
ssize_t ipHeaderLen = ((*pkt) & 0x0f) << 2;
//判断IP数据报的协议字段是否为ICMP包
if (IPPROTO_ICMP != *(pkt + 9)) return;
//校验ICMP报文长度
if (len - ipHeaderLen != ICMP_ECHO_PKT_SIZE) return;
uint8_t *pIcmp = pkt + ipHeaderLen;
if (*pIcmp != ICMP_ECHO_TYPE_RESP) return;
uint16_t tempPid = ntohs(*(uint16_t *)(pIcmp + 4));
uint16_t sendId = ntohs(*(uint16_t *)(pIcmp + 6));
if (tempPid != pid) return;
uint8_t ttl = *(pkt + 8);
double rtt = getRtt(pIcmp + 8);
respStat(rtt); //统计icmp应答
printf("%d bytes from %s: icmp_seq=%u, ttl=%u, rtt=%.3f ms\n", ICMP_ECHO_PKT_SIZE, ipStr, sendId, ttl, rtt);
}
void printReport() {
double sum = std::accumulate(rtts.begin(), rtts.end(), 0.0);
double avg = sum / rtts.size();
double loss = 0;
if (sendCount > 0) {
loss = (sendCount - (uint16_t)rtts.size()) / (double)sendCount;
loss *= 100;
}
int64_t totalTime = (int64_t)getTotal();
printf("\n-- %s ping statistics ---\n", ipStr);
printf("%u packets transmitted, %u received, %.2f%% packet loss, time %ldms\n", sendCount, rtts.size(), loss,
totalTime);
if (rtts.size() > 0) {
printf("rtt min/avg/max = %.3f/%.3f/%.3f ms\n", rttMin, avg, rttMax);
}
}
void run(struct sockaddr_in *addr, int32_t rFd) {
int sockFd = 0;
uint8_t pkt[IP_PROTO_MAX_SIZE];
socklen_t len = sizeof(*addr);
gettimeofday(&beginTime, NULL);
sockFd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
while (running) {
ssize_t n = recvfrom(sockFd, pkt, IP_PROTO_MAX_SIZE, 0, (struct sockaddr *)addr, (socklen_t *)&len);
if (n < 0) {
if (EINTR == errno) { //调用被中断则继续
continue;
} else {
perror("call recvfrom failed!");
}
}
dealResp(pkt, n); //这里收到的包为IP数据报,包含IP数据报的首部
}
read(rFd, &sendCount, sizeof(sendCount));
printReport();
}
};
int main(int argc, char *argv[]) {
if (argc != 2) {
cout << "param invalid!" << endl;
cout << "Usage: myping www.baidu.com" << endl;
return -1;
}
int fd[2];
int ret = 0;
struct sockaddr_in addr;
pid_t childPid = 0;
if (!PingBase::getAddrInfo(argv[1], &addr)) {
cout << "myping: unknown host " << argv[1] << endl;
return -1;
}
ret = pipe(fd); //创建匿名管道用于父子进程间通信
if (ret != 0) {
cout << "call pipe() failed! error msg:" << strerror(errno) << endl;
return -1;
}
inet_ntop(AF_INET, &addr.sin_addr, ipStr, 1024);
cout << "ping " << argv[1] << " (" << ipStr << ") " << endl;
childPid = fork(); //创建子进程
if (childPid < 0) {
cout << "call fork() failed! error msg:" << strerror(errno) << endl;
return -1;
}
PingBase::signalDealReg();
if (0 == childPid) { //子进程
close(fd[0]); //关闭匿名管道读端
PingSend pingSend;
pingSend.setPid(getpid() & 0xffff); // ICMP的标识符只有16位,故这里只取子进程的pid的低16位
pingSend.run(&addr, fd[1]); //子进程用于发送icmp回显请求
} else { //父进程
close(fd[1]); //关闭匿名管道写端
PingRecv pingRecv;
pingRecv.setPid(childPid & 0xffff); // ICMP的标识符只有16位,故这里只取子进程的pid的低16位
pingRecv.run(&addr, fd[0]); //父进程用于接收icmp回显应答
}
return 0;
}
由于只有root账号才有权限创建原始socket,因此需要使用root账号编译和运行myping程序。编译和运行的结果如下。
[root@VM-114-245-centos ~]# g++ -std=c++11 -o myping myping.cpp
[root@VM-114-245-centos ~]# ./myping www.baidu.com
ping www.baidu.com (14.215.177.38)
24 bytes from 14.215.177.38: icmp_seq=1, ttl=54, rtt=4.502 ms
24 bytes from 14.215.177.38: icmp_seq=2, ttl=54, rtt=4.808 ms
24 bytes from 14.215.177.38: icmp_seq=3, ttl=54, rtt=4.801 ms
24 bytes from 14.215.177.38: icmp_seq=4, ttl=54, rtt=4.890 ms
24 bytes from 14.215.177.38: icmp_seq=5, ttl=54, rtt=4.761 ms
24 bytes from 14.215.177.38: icmp_seq=6, ttl=54, rtt=4.765 ms
24 bytes from 14.215.177.38: icmp_seq=7, ttl=54, rtt=4.792 ms
24 bytes from 14.215.177.38: icmp_seq=8, ttl=54, rtt=4.788 ms
24 bytes from 14.215.177.38: icmp_seq=9, ttl=54, rtt=4.797 ms
24 bytes from 14.215.177.38: icmp_seq=10, ttl=54, rtt=4.875 ms
^C
-- 14.215.177.38 ping statistics ---
10 packets transmitted, 10 received, 0.00% packet loss, time 9432ms
rtt min/avg/max = 4.502/4.778/4.890 ms
[root@VM-114-245-centos ~]#
myping程序的逻辑非常简单,主要由PingBase、PingSend和PingRecv这三个类来实现相关功能。在main函数中,首先校验参数的有效性,然后调用fork函数创建一个子进程。子进程陷入死循环每隔1秒发送一个ICMP回显请求,而父进程陷入死循环并一直在接收ICMP回显应答。每当父进程接收到一个回显应答,就会将相关信息输出到终端上。当父子进程接收到需要退出的信号时,子进程会通过匿名管道将发送ICMP回显请求的个数发送给父进程,然后退出;父进程会读取匿名管道中的数据,确认子进程发送ICMP回显请求的个数,并最终将统计信息输出到终端上。
3.写最后
今天分享的内容就到这里,『如果本文对你有所帮助,记得关注我,我将持续在掘金上分享技术干货,关注我,下期分享不迷路』。
硬核爆肝码字,跪求一赞!!!