ping 是網路診斷最基礎工具,原理簡單卻蘊含核心網路知識:發送 ICMP 回顯請求 (Type 8),目標主機回應 ICMP 回顯應答 (Type 0)。測量往返時間 (RTT)、觀察 TTL、統計丟包率。本文完整實現一個極簡版 myping,涵蓋原始套接字、校驗和計算、時間戳、訊號處理等關鍵技術。
乙太網幀頭
IP 頭 (20位元組) ← 核心欄位:版本、IHL、TTL、協議(1=ICMP)、來源/目的IP、檢查和
ICMP 頭 (8位元組) ← 類型、代碼、檢查和、識別碼、序號
ICMP 資料 (可變) ← 時間戳 (struct timeval) 用於 RTT 計算
<netinet/ip.h>, <netinet/ip_icmp.h>)/* IP 頭關鍵欄位 */
struct ip {
uint8_t ip_hl:4, ip_v:4; // 頭長(4位元組單位)、版本
uint8_t ip_tos; // 服務類型
uint16_t ip_len; // 總長度
uint16_t ip_id; // 識別碼
uint16_t ip_off; // 片偏移/標誌
uint8_t ip_ttl; // 生存時間 (TTL)
uint8_t ip_p; // 協議 (IPPROTO_ICMP = 1)
uint16_t ip_sum; // 頭檢查和
struct in_addr ip_src, ip_dst; // 來源/目的 IP
};
/* ICMP 頭關鍵欄位 (回顯請求/應答) */
struct icmp {
uint8_t icmp_type; // 類型: 8=請求, 0=應答
uint8_t icmp_code; // 代碼: 0
uint16_t icmp_cksum; // 檢查和
uint16_t icmp_id; // 識別碼 (通常用 PID)
uint16_t icmp_seq; // 序號
// 後接資料:struct timeval 發送時間戳
};
將資料視為 16 位整數序列累加,溢出回卷,最後取反碼:
unsigned short cal_chksum(unsigned short *addr, int len) {
int nleft = len;
int sum = 0;
unsigned short *w = addr;
unsigned short answer = 0;
while (nleft > 1) { // 以 16 位為單位累加
sum += *w++;
nleft -= 2;
}
if (nleft == 1) { // 奇數位元組:高位補 0
*(unsigned char*)(&answer) = *(unsigned char*)w;
sum += answer;
}
sum = (sum >> 16) + (sum & 0xFFFF); // 回卷進位
sum += (sum >> 16); // 再次回卷
answer = ~sum; // 取反碼
return answer;
}
/***********************************************************
* myping.c - 極簡 Ping 實現
* 編譯: gcc -o myping myping.c
* 執行: sudo ./myping <主機名/IP> (需 root 權限建立原始套接字)
***********************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <errno.h>
#define PACKET_SIZE 4096
#define MAX_WAIT_TIME 5
#define MAX_PACKETS 3
#define DATA_LEN 56
char send_pkt[PACKET_SIZE];
char recv_pkt[PACKET_SIZE];
int sockfd;
int nsent = 0, nrecv = 0;
struct sockaddr_in dest_addr;
pid_t pid;
struct sockaddr_in from;
struct timeval tv_recv;
void statistics(int signo);
unsigned short cal_chksum(unsigned short *addr, int len);
int pack(int pkt_no);
void send_packet(void);
void recv_packet(void);
int unpack(char *buf, int len);
void tv_sub(struct timeval *out, struct timeval *in);
int main(int argc, char *argv[]) {
struct hostent *host;
struct protoent *proto;
unsigned long inaddr = 0;
int size = 50 * 1024;
if (argc < 2) {
fprintf(stderr, "Usage: %s <hostname/IP>\n", argv[0]);
exit(1);
}
/* 1. 取得 ICMP 協議號 */
if ((proto = getprotobyname("icmp")) == NULL) {
perror("getprotobyname");
exit(1);
}
/* 2. 建立原始套接字 (需 root) */
if ((sockfd = socket(AF_INET, SOCK_RAW, proto->p_proto)) < 0) {
perror("socket");
exit(1);
}
/* 3. 降權:捨棄 root 權限,保留建立的 socket */
setuid(getuid());
/* 4. 擴大接收緩衝區,防止廣播風暴溢出 */
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));
/* 5. 解析目標地址 */
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.sin_family = AF_INET;
if ((inaddr = inet_addr(argv[1])) == INADDR_NONE) {
if ((host = gethostbyname(argv[1])) == NULL) {
perror("gethostbyname");
exit(1);
}
memcpy(&dest_addr.sin_addr, host->h_addr, host->h_length);
} else {
memcpy(&dest_addr.sin_addr, &inaddr, sizeof(inaddr));
}
pid = getpid(); // 用 PID 作為 ICMP 識別碼
printf("PING %s (%s): %d bytes data in ICMP packets.\n",
argv[1], inet_ntoa(dest_addr.sin_addr), DATA_LEN);
send_packet(); // 發送階段
recv_packet(); // 接收階段
statistics(SIGALRM); // 統計輸出
return 0;
}
/* 封裝 ICMP 回顯請求封包 */
int pack(int pkt_no) {
struct icmp *icmp = (struct icmp*)send_pkt;
struct timeval *tval = (struct timeval*)icmp->icmp_data;
icmp->icmp_type = ICMP_ECHO; // 8
icmp->icmp_code = 0;
icmp->icmp_cksum = 0; // 先歸零計算
icmp->icmp_seq = pkt_no;
icmp->icmp_id = pid;
gettimeofday(tval, NULL); // 記錄發送時間戳
int packsize = 8 + DATA_LEN; // ICMP頭(8) + 資料
icmp->icmp_cksum = cal_chksum((unsigned short*)icmp, packsize);
return packsize;
}
/* 發送多個封包,間隔 1 秒 */
void send_packet() {
while (nsent < MAX_PACKETS) {
nsent++;
int pktsize = pack(nsent);
if (sendto(sockfd, send_pkt, pktsize, 0,
(struct sockaddr*)&dest_addr, sizeof(dest_addr)) < 0) {
perror("sendto");
continue;
}
sleep(1);
}
}
/* 接收並處理回應 */
void recv_packet() {
int n, fromlen = sizeof(from);
signal(SIGALRM, statistics); // 超時處理
while (nrecv < nsent) {
alarm(MAX_WAIT_TIME);
if ((n = recvfrom(sockfd, recv_pkt, sizeof(recv_pkt), 0,
(struct sockaddr*)&from, &fromlen)) < 0) {
if (errno == EINTR) continue; // alarm 中斷
perror("recvfrom");
continue;
}
gettimeofday(&tv_recv, NULL); // 記錄接收時間
if (unpack(recv_pkt, n) == -1) continue;
nrecv++;
}
}
/* 解析接收封包:剝離 IP 頭,驗證 ICMP 回顯應答 */
int unpack(char *buf, int len) {
struct ip *ip = (struct ip*)buf;
int ip_hdr_len = ip->ip_hl << 2; // IHL * 4
struct icmp *icmp = (struct icmp*)(buf + ip_hdr_len);
len -= ip_hdr_len;
if (len < 8) { // ICMP 頭至少 8 位元組
fprintf(stderr, "ICMP packet too short\n");
return -1;
}
/* 驗證:類型=0(應答) 且 ID=我們的 PID */
if (icmp->icmp_type == ICMP_ECHOREPLY && icmp->icmp_id == pid) {
struct timeval *tv_send = (struct timeval*)icmp->icmp_data;
tv_sub(&tv_recv, tv_send); // 計算時間差
double rtt = tv_recv.tv_sec * 1000.0 + tv_recv.tv_usec / 1000.0; // 毫秒
printf("%d byte from %s: icmp_seq=%u ttl=%d rtt=%.3f ms\n",
len, inet_ntoa(from.sin_addr), icmp->icmp_seq, ip->ip_ttl, rtt);
return 0;
}
return -1; // 非我們的回應
}
/* 統計輸出 */
void statistics(int signo) {
printf("\n--- PING statistics ---\n");
printf("%d packets transmitted, %d received, %d%% lost\n",
nsent, nrecv, nsent ? (nsent - nrecv) * 100 / nsent : 0);
close(sockfd);
exit(0);
}
/* timeval 減法:out -= in */
void tv_sub(struct timeval *out, struct timeval *in) {
if ((out->tv_usec -= in->tv_usec) < 0) {
--out->tv_sec;
out->tv_usec += 1000000;
}
out->tv_sec -= in->tv_sec;
}
socket(AF_INET, SOCK_RAW, IPPROTO_ICMP) 繞過傳輸層,直接收發 IP 負載。setuid(getuid()) 降權。icmp_id 過濾屬於本程式的回應。icmp_id = getpid():進程唯一標識,多進程並發不衝突。icmp_seq = 1,2,3...:單調遞增,對應請求/回應配對。icmp_id 和 icmp_seq 會原樣返回。gettimeofday() 寫入 ICMP 資料區 (struct timeval 8 位元組)。gettimeofday() 取當前時間。tv_sub() 處理微秒借位,精確到微秒級。alarm(MAX_WAIT_TIME) 設定定時器,到期送 SIGALRM。recvfrom() 被中斷返回 EINTR,循環繼續。statistics() 輸出統計並退出。icmp_cksum 必須先歸零。ip_sum),由核心自動填寫/驗證,用戶態無需處理。$ gcc -o myping myping.c
$ sudo chown root:root myping
$ sudo chmod u+s myping # 設定 SUID,普通用戶也能執行
$ ./myping www.google.com
PING www.google.com (142.250.190.4): 56 bytes data in ICMP packets.
64 byte from 142.250.190.4: icmp_seq=1 ttl=115 rtt=12.345 ms
64 byte from 142.250.190.4: icmp_seq=2 ttl=115 rtt=11.876 ms
64 byte from 142.250.190.4: icmp_seq=3 ttl=115 rtt=12.001 ms
--- PING statistics ---
3 packets transmitted, 3 received, 0% lost
AF_INET6, IPPROTO_ICMPV6, struct icmp6_hdr。alarm/signal,支援高併發。cap_net_raw+ep) 替代 SUID。實現 ping 是學習 Linux 系統網路程式設計的最佳切入點:
這 100 行代碼串聯了網路協議棧從應用層到驅動層的關鍵路徑,是網路工程師必修的實戰課題。