C语言实现ICMP Ping程序 | AI生成和翻译
ICMP(互联网控制消息协议)是一种网络层协议,用于IP网络中的诊断和错误报告。它最著名的应用是通过 ping 工具测试主机之间的连通性。ICMP消息直接封装在IP数据包中(无TCP或UDP层),包含类型、代码、校验和与载荷。对于 ping 而言,”回显请求”(类型8)和”回显应答”(类型0)消息至关重要。
ICMP的工作流程概览如下:
- 发送方(例如您的机器)向目标IP发送ICMP回显请求
- 若目标可达且配置为响应,则返回ICMP回显应答
- 发送方根据应答到达时间计算往返时延(RTT)
要在C语言中实现类似 ping 的程序(不使用 libpcap 或 libnet 等库),我们将使用原始套接字(SOCK_RAW)直接构造和发送ICMP数据包。这需要root权限(例如使用 sudo),因为原始套接字绕过了高层抽象。
以下是一个简化的C程序,用于发送ICMP回显请求并监听回显应答,模拟基本的 ping 功能。
C语言实现ICMP Ping程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <time.h>
#include <errno.h>
// 计算ICMP校验和
unsigned short checksum(void *b, int len) {
unsigned short *buf = b;
unsigned int sum = 0;
unsigned short result;
for (sum = 0; len > 1; len -= 2) {
sum += *buf++;
}
if (len == 1) {
sum += *(unsigned char *)buf;
}
sum = (sum >> 16) + (sum & 0xFFFF);
sum += (sum >> 16);
result = ~sum;
return result;
}
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("用法:%s <目标IP>\n", argv[0]);
exit(EXIT_FAILURE);
}
int sock_fd;
struct sockaddr_in dest_addr;
char packet[64]; // ICMP头部+数据载荷
struct icmphdr *icmp;
char recv_buffer[1024];
struct timespec start, end;
// 创建ICMP原始套接字
sock_fd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (sock_fd < 0) {
perror("套接字创建失败(请以root权限运行)");
exit(EXIT_FAILURE);
}
// 设置目标地址
dest_addr.sin_family = AF_INET;
if (inet_pton(AF_INET, argv[1], &dest_addr.sin_addr) <= 0) {
perror("无效IP地址");
close(sock_fd);
exit(EXIT_FAILURE);
}
// 准备ICMP回显请求数据包
memset(packet, 0, sizeof(packet));
icmp = (struct icmphdr *)packet;
icmp->type = ICMP_ECHO; // 类型8:回显请求
icmp->code = 0;
icmp->un.echo.id = getpid(); // 使用进程ID作为标识符
icmp->un.echo.sequence = 1; // 序列号
for (int i = sizeof(*icmp); i < 64; i++) {
packet[i] = i; // 填充虚拟数据作为载荷
}
icmp->checksum = 0; // 填充数据包后计算校验和
icmp->checksum = checksum(packet, 64);
printf("正在ping %s...\n", argv[1]);
for (int i = 0; i < 4; i++) { // 发送4次ping
// 记录发送时间
clock_gettime(CLOCK_MONOTONIC, &start);
// 发送ICMP回显请求
if (sendto(sock_fd, packet, 64, 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr)) < 0) {
perror("发送失败");
close(sock_fd);
exit(EXIT_FAILURE);
}
// 接收ICMP回显应答
struct sockaddr_in from_addr;
socklen_t from_len = sizeof(from_addr);
int bytes_received = recvfrom(sock_fd, recv_buffer, sizeof(recv_buffer), 0,
(struct sockaddr *)&from_addr, &from_len);
if (bytes_received < 0) {
perror("接收失败");
continue;
}
// 记录接收时间
clock_gettime(CLOCK_MONOTONIC, &end);
// 解析接收的IP和ICMP头部
struct iphdr *ip = (struct iphdr *)recv_buffer;
struct icmphdr *icmp_reply = (struct icmphdr *)(recv_buffer + (ip->ihl * 4));
if (icmp_reply->type == ICMP_ECHOREPLY && icmp_reply->un.echo.id == getpid()) {
double rtt = (end.tv_sec - start.tv_sec) * 1000.0 +
(end.tv_nsec - start.tv_nsec) / 1000000.0;
printf("%d字节来自 %s:序列=%d 时延=%.2f毫秒\n",
bytes_received - (ip->ihl * 4), argv[1], icmp_reply->un.echo.sequence, rtt);
} else {
printf("意外ICMP响应:类型=%d\n", icmp_reply->type);
}
sleep(1); // 每次ping间隔1秒
icmp->un.echo.sequence++; // 递增序列号
icmp->checksum = 0;
icmp->checksum = checksum(packet, 64); // 重新计算校验和
}
close(sock_fd);
return 0;
}
实现原理
- 套接字创建:
socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)创建用于ICMP的原始套接字,允许手动构造ICMP数据包
- ICMP数据包构造:
- ICMP头部(
struct icmphdr)填充字段包括:type = ICMP_ECHO(8)表示回显请求code = 0id设置为进程ID用于标识ping请求sequence用于跟踪单个请求
- 添加载荷(虚拟数据)后,计算整个数据包的校验和
- ICMP头部(
- 发送过程:
sendto()将数据包发送至目标IP。由于ICMP在传输层之下操作,无需指定端口
- 接收过程:
recvfrom()捕获包含ICMP回显应答的原始IP数据包- 跳过IP头部(
ihl * 4字节)定位到ICMP头部 - 验证是否为回显应答(类型0)且标识符匹配
- 时延计算:
- 通过
clock_gettime()以毫秒为单位测量RTT
- 通过
- 校验和计算:
checksum()函数按ICMP要求计算16位二进制反码和
编译与使用
- 编译:
gcc ping.c -o ping - 运行(需root权限):
sudo ./ping 8.8.8.8 - 示例输出:
正在ping 8.8.8.8... 64字节来自 8.8.8.8:序列=1 时延=12.34毫秒 64字节来自 8.8.8.8:序列=2 时延=11.89毫秒 ...
注意事项
- Root权限:原始套接字需要
sudo权限,因其直接操作网络数据包 - 防火墙:目标主机需配置为响应ICMP回显请求(部分主机会屏蔽此类请求)
- 简化处理:此版本未完全处理超时、丢包或IP选项等边界情况
这是在用户空间实现ICMP通信的最底层方案。若需更底层操作,则需要编写内核级代码直接与IP栈交互。如有进一步调整需求,欢迎提出!