Linux下实现ping需用原始套接字socket(AF_INET, SOCK_RAW, IPPROTO_ICMP),进程须具CAP_NET_RAW权限或以root运行;需手动构造IP/ICMP头、置零校验和字段后按16位折叠求反计算,注意网络字节序及奇数长度补零,并设SO_RCVTIMEO防阻塞。

Linux下用ping命令替代最简单,但程序里要发ICMP必须用原始套接字
普通用户态程序不能直接构造ICMP包,除非有CAP_NET_RAW能力或以root运行。glibc不提供标准API封装ICMP,得自己填IP头、ICMP头、计算校验和。别指望std::system("ping -c 1 host")能拿回结构化结果——它只给你字符串输出,解析麻烦还不可靠。
- Linux上必须用
socket(AF_INET, SOCK_RAW, IPPROTO_ICMP),且进程需有cap_net_raw+ep或root权限 - Windows不同:用
IcmpSendEcho(iphlpapi.h),无需管理员权限,但只支持IPv4 - macOS限制更严,从10.14开始默认禁止非特权进程创建
SOCK_RAW,即使sudo也不总生效,建议改用scutil --dns或调用系统ping并解析stdout
ICMP Echo Request的校验和怎么算才不出错
ICMP校验和是按16位字节求和后取反,但必须把校验和字段先置0再算——很多人漏这步,导致包被对方直接丢弃。还要注意字节序:IP头和ICMP头都用网络字节序(大端),htons()和ntohs()该用就得用。
- 校验和计算范围包括整个ICMP报文(类型、代码、校验和、标识符、序列号、可选数据),不含IP头
- 若长度为奇数,末尾补一个字节的0再计算,避免跨字边界错误
- 别用
~sum直接赋值——要先做sum = (sum & 0xffff) + (sum >> 16)折叠进16位,再~sum & 0xffff
用setsockopt控制超时和TTL容易被忽略的细节
原始套接字默认没超时机制,recvfrom可能永久阻塞。设SO_RCVTIMEO是必须的,但要注意:它的单位是微秒级的struct timeval,不是毫秒;TTL通过IP_TTL设置,但某些内核版本对SOCK_RAW支持不一致,建议在IP头里手动填TTL字段更稳妥。
struct timeval tv = {1, 0}; // 1秒超时,不是1000毫秒- 设置TTL前先确认
setsockopt(sock, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl))返回0,否则fallback到IP头第9字节 - Linux 4.17+支持
SO_BINDTODEVICE绑定网卡,但需CAP_NET_RAW,调试多网卡环境时很有用
Windows上用IcmpSendEcho比手写安全,但要注意内存管理和IPv6限制
IcmpSendEcho封装了底层细节,不用管校验和、序列号、超时重传,但它返回的是ICMP_ECHO_REPLY*指针,缓冲区由你分配且必须足够大——至少sizeof(ICMP_ECHO_REPLY) + ReplySize。它只支持IPv4,IPv6得用Icmp6SendEcho2,接口更复杂,且需要WSAStartup后显式加载iphlpapi.dll。
立即学习“C++免费学习笔记(深入)”;
- ReplyBuffer大小至少为
sizeof(ICMP_ECHO_REPLY) + 64(默认ICMP数据长度),否则dwStatus返回IP_REQ_TIMED_OUT而非真实错误 - 函数返回
NULL不代表失败,要检查GetLastError(),常见错误码:ERROR_INVALID_PARAMETER(参数错)、IP_DEST_HOST_UNREACHABLE(路由不通) - IPv6必须用
Icmp6SendEcho2,且Address参数是sockaddr_in6*,不能复用IPv4地址结构
ping命令加解析,或者用成熟库如libtins或poco。真要手写,重点不是“怎么发”,而是“怎么判断发没发出去、对方回没回、回的是不是你要的那个包”——序列号、标识符、ID校验、时间戳匹配,一个都不能少。


















