使用 Raw Socket (SOCK_RAW, IPPROTO_ICMP) 手工構造 ICMP Echo Request 封包 (Type 8, Code 0),發送至目標主機,接收 Echo Reply (Type 0) 計算往返時間。並示範 IP 選項 Record Route (RR) 記錄路徑 (最多 9 跳)。
// IP 標頭 (位元欄位對應 RFC 791)
struct IpHeader {
unsigned int h_len:4; // 標頭長度 (4 bytes 為單位)
unsigned int version:4; // 版本 (4)
unsigned char tos; // 服務類型
unsigned short total_len; // 總長度
unsigned short ident; // 識別碼
unsigned short frag_and_flags; // 片段旗標/位移
unsigned char ttl; // 生存時間
unsigned char proto; // 協定 (ICMP=1)
unsigned short checksum; // 標頭檢驗和
unsigned int sourceIP; // 來源 IP
unsigned int destIP; // 目的 IP
};
// ICMP 標頭 (擴充 timestamp 欄位用於計算 RTT)
struct IcmpHeader {
BYTE i_type; // 8=Echo Request, 0=Echo Reply
BYTE i_code; // 0
USHORT i_cksum; // 檢驗和
USHORT i_id; // 行程 ID 識別
USHORT i_seq; // 序號
ULONG timestamp; // 發送時間 (GetTickCount)
};
// IP 選項標頭 - Record Route
struct IpOptionHeader {
unsigned char code; // 0x07 (RR)
unsigned char len; // 選項總長度 (39)
unsigned char ptr; // 目前指標位移 (起始 4)
unsigned long addr[9]; // 最多記錄 9 個 IP
};
USHORT checksum(USHORT* buffer, int size)
{
unsigned long cksum = 0;
while (size > 1) { cksum += *buffer++; size -= sizeof(USHORT); }
if (size) cksum += *(UCHAR*)buffer;
cksum = (cksum >> 16) + (cksum & 0xffff);
cksum += (cksum >> 16);
return (USHORT)(~cksum); // 反碼
}
SOCKET sock = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP, NULL, 0, WSA_FLAG_OVERLAPPED);
// 設定接收/發送逾時 1 秒
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout));
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (char*)&timeout, sizeof(timeout));
// 啟用 Record Route 選項
if (bRecordRoute) {
IpOptionHeader opt = {0}; opt.code = 0x07; opt.ptr = 4; opt.len = 39;
setsockopt(sock, IPPROTO_IP, IP_OPTIONS, (char*)&opt, sizeof(opt));
}
for (int i=0; i<4; i++) {
icmp_hdr->i_cksum = 0;
icmp_hdr->timestamp = GetTickCount();
icmp_hdr->i_seq = seq++;
icmp_hdr->i_cksum = checksum((USHORT*)icmp_data, data_size);
sendto(sock, icmp_data, data_size, 0, (sockaddr*)&dest, sizeof(dest));
recvfrom(sock, recv_buf, MAX_PACKET, 0, (sockaddr*)&from, &fromlen);
DecodeICMPHeader(recv_buf, bytes, &from); // 解析回應
Sleep(1000);
}
void DecodeICMPHeader(char* buf, int bytes, sockaddr_in* from)
{
IpHeader* ip = (IpHeader*)buf;
int ip_hdr_len = ip->h_len * 4;
if (ip_hdr_len == 60 && !icmp_count) DecodeIPOptions(buf, bytes); // 解析 RR
IcmpHeader* icmp = (IcmpHeader*)(buf + ip_hdr_len);
if (icmp->i_type != 0 || icmp->i_id != GetCurrentProcessId()) return;
printf("%d bytes from %s: icmp_seq=%d time=%d ms\n",
bytes, inet_ntoa(from->sin_addr), icmp->i_seq,
GetTickCount() - icmp->timestamp);
icmp_count++;
}
// 編譯指令 (VC6/VC++)
cl Ping.c ws2_32.lib /Zp1
// 執行
Ping [-r] <主機名或IP> [封包大小<1024>
-r : 啟用 Record Route 記錄路徑
WSA_FLAG_OVERLAPPED 必須指定,否則同步 Socket 配合 SO_RCVTIMEO 會導致核心永久阻塞。