转自 https://network.fasionchan.com/zh_CN/latest/practices/ping-by-icmp-c.html
众所周知, ping 命令通过 ICMP 协议探测目标 IP 并计算 往返时间 。 本文使用 C 语言开发一个 ping 命令, 以演示如何通过 套接字 发送 和 接收 ICMP 协议报文。
注解
程序源码 可在本文末尾复制,或者在 Github 上下载: ping.c 。
报文封装
ICMP 报文承载在 IP 报文之上,头部结构非常简单:
注意到, ICMP 头部只有三个固定字段,其余部分因消息类型而异。固定字段如下:
- type , 消息类型 ;
- code , 代码 ;
- checksum , 校验和 ;
ICMP 报文有很多不同的类型,由 type 和 code 字段区分。 而 ping 命令使用其中两种:
ping命令原理
如上图,机器 A 通过 回显请求 ( Echo Request ) 询问机器 B ; 机器 B 收到报文后通过 回显答复 ( Echo Reply ) 响应机器 A 。 这两种报文的典型结构如下:
对应的 type 以及 code 字段值列举如下:
名称 | 类型 | ”代码“ |
---|---|---|
回显请求 | 8 | 0 |
回显答复 | 0 | 0 |
按照惯例,回显报文除了固定字段,其余部分组织成 3 个字段:
- 标识符 ( identifier ),一般填写进程 PID 以区分其他 ping 进程;
- 报文序号 ( sequence number ),用于编号报文序列;
- 数据 ( data ),可以是任意数据;
按 ICMP 规定, 回显答复 报文原封不动回传这些字段。 因此,可以将 发送时间 封装在 数据负载 ( payload )中, 收到答复后将其取出,用于计算 往返时间 ( round trip time )。
定义一个结构体用以封装报文:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
struct icmp_echo { // header uint8_t type; uint8_t code; uint16_t checksum; uint16_t ident; uint16_t seq; // data double sending_ts; char magic[MAGIC_LEN]; }; |
前 3 个字段为 ICMP 公共头部; 中间 2 个字段为 回显请求 、 回显答复 惯例头部; 其余字段为 数据负载 ,包括一个双精度 发送时间戳 以及一个固定的魔性字符串。
校验和
ICMP 报文校验和字段需要自行计算,计算步骤如下:
- 以 0 为校验和封装一个用于计算的 伪报文 ;
- 将报文分成两个字节一组,如果总字节数为奇数,则在末尾追加一个零字节;
- 对所有 双字节 进行按位求和;
- 将高于 16 位的进位取出相加,直到没有进位;
- 将校验和按位取反;
示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
uint16_t calculate_checksum(unsigned char* buffer, int bytes) { uint32_t checksum = 0; unsigned char* end = buffer + bytes; // odd bytes add last byte and reset end if (bytes % 2 == 1) { end = buffer + bytes - 1; checksum += (*end) << 8; } // add words of two bytes, one by one while (buffer < end) { checksum += buffer[0] << 8; checksum += buffer[1]; buffer += 2; } // add carry if any uint32_t carray = checksum >> 16; while (carray) { checksum = (checksum & 0xffff) + carray; carray = checksum >> 16; } // negate it checksum = ~checksum; return checksum & 0xffff; } |
套接字
编程实现网络通讯,离不开 套接字 ( socket ),收发 ICMP 报文当然也不例外:
#include int s = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
调用 sendto 系统调用发送 ICMP 报文:
struct icmp_echo icmp; struct sockaddr_in peer_addr; sendto(s, &icmp, sizeof(icmp), 0, peer_addr, sizeof(peer_addr));
其中,第一个参数为 套接字 ; 第二、三个参数为封装好的 ICMP 报文 及 长度 ; 第四、五个参数为 目的地址 及地址结构体长度。
调用 recvfrom 系统调用接收 ICMP 报文:
#define MTU 1500 char buffer[MTU]; struct sockaddr_in peer_addr; int addr_len = sizeof(peer_addr); recvfrom(s, buffer, MTU, 0, &peer_addr, &addr_len); struct icmp_echo *icmp = buffer + 20;
参数为接收缓冲区大小,这里用 1500 刚好是一个典型的 MTU 大小。 注意到, recvfrom 系统调用返回 IP 报文,去掉前 20 字节的 IP 头部便得到 ICMP 报文。
注解
注意,创建 原始套接字 ( SOCK_RAW )需要超级用户权限。
程序实现
掌握基本原理后,便可着手编写代码了。
首先,实现 send_echo_request 函数,用于发送 ICMP 回显请求 报文:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
int send_echo_request(int sock, struct sockaddr_in* addr, int ident, int seq) { // allocate memory for icmp packet struct icmp_echo icmp; bzero(&icmp, sizeof(icmp)); // fill header files icmp.type = 8; icmp.code = 0; icmp.ident = htons(ident); icmp.seq = htons(seq); // fill magic string strncpy(icmp.magic, MAGIC, MAGIC_LEN); // fill sending timestamp icmp.sending_ts = get_timestamp(); // calculate and fill checksum icmp.checksum = htons( calculate_checksum((unsigned char*)&icmp, sizeof(icmp)) ); // send it int bytes = sendto(sock, &icmp, sizeof(icmp), 0, (struct sockaddr*)addr, sizeof(*addr)); if (bytes == -1) { return -1; } return 0; } |
第 3-17 行封装用于计算校验和的 伪报文 , 注意到 类型 字段为 8 , 代码 字段为 0 , 校验和 字段为 0 , 标识符 以及 序号 由参数指定; 第 10 行调用 calculate_checksum 函数计算 校验和 ; 第 25-26 调 sendto 系统调用将报文发送出去。
对应地,实现 recv_echo_reply 用于接收 ICMP 回显答复 报文:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
int recv_echo_reply(int sock, int ident) { // allocate buffer char buffer[MTU]; struct sockaddr_in peer_addr; // receive another packet int addr_len = sizeof(peer_addr); int bytes = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&peer_addr, &addr_len); if (bytes == -1) { // normal return when timeout if (errno == EAGAIN || errno == EWOULDBLOCK) { return 0; } return -1; } // find icmp packet in ip packet struct icmp_echo* icmp = (struct icmp_echo*)(buffer + 20); // check type if (icmp->type != 0 || icmp->code != 0) { return 0; } // match identifier if (ntohs(icmp->ident) != ident) { return 0; } // print info printf("%s seq=%d %5.2fms\n", inet_ntoa(peer_addr.sin_addr), ntohs(icmp->seq), (get_timestamp() - icmp->sending_ts) * 1000 ); return 0; } |
第 3-5 行分配用于接收报文的 缓冲区 ; 第 9-10 行调用 recvfrom 系统调用 接收 一个 新报文 ; 第 13-15 接收报文 超时 ,正常返回; 第 21 行从 IP 报文中取出 ICMP 报文; 第 24-26 行检查 ICMP 报文类型 ; 第 29-31 检查 标识符 是否匹配; 第 32-38 行计算 往返时间 并打印提示信息。
最后,实现 ping 函数,循环发送并接收报文:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
int ping(const char *ip) { // for store destination address struct sockaddr_in addr; bzero(&addr, sizeof(addr)); // fill address, set port to 0 addr.sin_family = AF_INET; addr.sin_port = 0; if (inet_aton(ip, (struct in_addr*)&addr.sin_addr.s_addr) == 0) { return -1; }; // create raw socket for icmp protocol int sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); if (sock == -1) { return -1; } // set socket timeout option struct timeval tv; tv.tv_sec = 0; tv.tv_usec = RECV_TIMEOUT_USEC; int ret = setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); if (ret == -1) { return -1; } double next_ts = get_timestamp(); int ident = getpid(); int seq = 1; for (;;) { // time to send another packet if (get_timestamp() >= next_ts) { // send it ret = send_echo_request(sock, &addr, ident, seq); if (ret == -1) { perror("Send failed"); } // update next sendint timestamp to one second later next_ts += 1; // increase sequence number seq += 1; } // try to receive and print reply ret = recv_echo_reply(sock, ident); if (ret == -1) { perror("Receive failed"); } } return 0; } |
第 3-12 行,初始化 目的地址 结构体; 第 14-18 行,创建用于发送、接收 ICMP 报文的 套接字 ; 第 20-27 行,将套接字 接收超时时间 设置为 0.1 秒, 以便 等待答复报文 的同时有机会 发送请求报文 ; 第 30-31 行,获取进程 PID 作为 标识符 、同时初始化报文 序号 ; 接着,循环发送并接收报文; 第 35-46 行,当前时间达到发送时间则调用 send_echo_request 函数 发送请求报文 , 更新下次发送时间并自增序号; 第 48-52 行,调用 recv_echo_reply 函数 接收答复报文 。
将以上所有代码片段组装在一起,便得到 ping.c 命令。 迫不及待想运行一下:
$ gcc -o ping ping.c $ sudo ./ping 8.8.8.8 8.8.8.8 seq=1 25.70ms 8.8.8.8 seq=2 25.28ms 8.8.8.8 seq=3 25.26ms
It works!
程序源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 |
/** * FileName: ping.c * Author: Fasion Chan * @contact: fasionchan@gmail.com * @version: $Id$ * * Description: * * Changelog: * **/ #include #include #include #include #include #include #include #define MAGIC "1234567890" #define MAGIC_LEN 11 #define MTU 1500 #define RECV_TIMEOUT_USEC 100000 struct icmp_echo { // header uint8_t type; uint8_t code; uint16_t checksum; uint16_t ident; uint16_t seq; // data double sending_ts; char magic[MAGIC_LEN]; }; double get_timestamp() { struct timeval tv; gettimeofday(&tv, NULL); return tv.tv_sec + ((double)tv.tv_usec) / 1000000; } uint16_t calculate_checksum(unsigned char* buffer, int bytes) { uint32_t checksum = 0; unsigned char* end = buffer + bytes; // odd bytes add last byte and reset end if (bytes % 2 == 1) { end = buffer + bytes - 1; checksum += (*end) << 8; } // add words of two bytes, one by one while (buffer < end) { checksum += buffer[0] << 8; checksum += buffer[1]; buffer += 2; } // add carry if any uint32_t carray = checksum >> 16; while (carray) { checksum = (checksum & 0xffff) + carray; carray = checksum >> 16; } // negate it checksum = ~checksum; return checksum & 0xffff; } int send_echo_request(int sock, struct sockaddr_in* addr, int ident, int seq) { // allocate memory for icmp packet struct icmp_echo icmp; bzero(&icmp, sizeof(icmp)); // fill header files icmp.type = 8; icmp.code = 0; icmp.ident = htons(ident); icmp.seq = htons(seq); // fill magic string strncpy(icmp.magic, MAGIC, MAGIC_LEN); // fill sending timestamp icmp.sending_ts = get_timestamp(); // calculate and fill checksum icmp.checksum = htons( calculate_checksum((unsigned char*)&icmp, sizeof(icmp)) ); // send it int bytes = sendto(sock, &icmp, sizeof(icmp), 0, (struct sockaddr*)addr, sizeof(*addr)); if (bytes == -1) { return -1; } return 0; } int recv_echo_reply(int sock, int ident) { // allocate buffer char buffer[MTU]; struct sockaddr_in peer_addr; // receive another packet int addr_len = sizeof(peer_addr); int bytes = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&peer_addr, &addr_len); if (bytes == -1) { // normal return when timeout if (errno == EAGAIN || errno == EWOULDBLOCK) { return 0; } return -1; } // find icmp packet in ip packet struct icmp_echo* icmp = (struct icmp_echo*)(buffer + 20); // check type if (icmp->type != 0 || icmp->code != 0) { return 0; } // match identifier if (ntohs(icmp->ident) != ident) { return 0; } // print info printf("%s seq=%d %5.2fms\n", inet_ntoa(peer_addr.sin_addr), ntohs(icmp->seq), (get_timestamp() - icmp->sending_ts) * 1000 ); return 0; } int ping(const char *ip) { // for store destination address struct sockaddr_in addr; bzero(&addr, sizeof(addr)); // fill address, set port to 0 addr.sin_family = AF_INET; addr.sin_port = 0; if (inet_aton(ip, (struct in_addr*)&addr.sin_addr.s_addr) == 0) { return -1; }; // create raw socket for icmp protocol int sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); if (sock == -1) { return -1; } // set socket timeout option struct timeval tv; tv.tv_sec = 0; tv.tv_usec = RECV_TIMEOUT_USEC; int ret = setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); if (ret == -1) { return -1; } double next_ts = get_timestamp(); int ident = getpid(); int seq = 1; for (;;) { // time to send another packet if (get_timestamp() >= next_ts) { // send it ret = send_echo_request(sock, &addr, ident, seq); if (ret == -1) { perror("Send failed"); } // update next sendint timestamp to one second later next_ts += 1; // increase sequence number seq += 1; } // try to receive and print reply ret = recv_echo_reply(sock, ident); if (ret == -1) { perror("Receive failed"); } } return 0; } int main(int argc, const char* argv[]) { return ping(argv[1]); } |