ping 命令与基于 Go 的简单实现

在日常工作中,我们经常需要确认与远程主机的连接状态,而 ping 就是一种常用的网络工具,用于测试与目标主机之间的网络连接。它通过发送 ICMP 回显请求报文,并等待目标主机返回 ICMP 回显应答报文来判断连接是否正常,以及网络延迟和丢包情况。

步骤

  1. 发送 ICMP 回显请求消息:
    • 操作系统构建一个 ICMP 回显请求消息数据包。
    • 该数据包包含:
      • ICMP Type (8,表示 Echo request)
      • ICMP Code (0)
      • 校验和 (确保数据完整性)
      • 标识符 (用于匹配请求和回复)
      • 序列号 (用于跟踪发送的请求)
      • 可选的数据负载 (通常是一串字符)
    • 数据包被封装到 IP 数据报中,并发送到目标主机。
  2. 目标主机接收 ICMP 回显请求消息:
    • 目标主机网络接口接收到 IP 数据报。
    • IP 数据报被解封装,提取出 ICMP 回显请求消息。
    • 目标主机操作系统识别 ICMP 类型和代码。
  3. 目标主机发送 ICMP 回显回复消息:
    • 目标主机操作系统构建一个 ICMP 回显回复消息数据包。
    • 该数据包包含:
      • ICMP Type (0,表示 Echo reply)
      • ICMP Code (0)
      • 校验和
      • 标识符 (与请求消息中的相同)
      • 序列号 (与请求消息中的相同)
      • 请求消息中的数据负载
    • 数据包被封装到 IP 数据报中,并发送回源主机。
  4. 源主机接收 ICMP 回显回复消息:
    • 源主机网络接口接收到 IP 数据报。
    • IP 数据报被解封装,提取出 ICMP 回显回复消息。
    • 操作系统计算往返时间 (RTT)。

基于 Go 的 ping 命令简单实现

这里使用了 golang.org/x/net/icmp 扩展库来实现一个简单的 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
57
58
59
60
61
func Ping(addr string) error {
timeout := 5 * time.Second

conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
if err != nil {
return fmt.Errorf("error listening for ICMP packets: %w", err)
}
defer conn.Close()

destinationAddress, err := net.ResolveIPAddr("ip4", addr)
if err != nil {
return fmt.Errorf("error resolving hostname: %w", err)
}

message := icmp.Message{
Type: ipv4.ICMPTypeEcho,
Code: 0,
Body: &icmp.Echo{
ID: os.Getpid() & 0xffff,
Seq: 1,
Data: []byte("hello"),
},
}

messageBytes, err := message.Marshal(nil)
if err != nil {
return fmt.Errorf("error marshaling ICMP message: %w", err)
}

currentTime := time.Now()
if _, err := conn.WriteTo(messageBytes, destinationAddress); err != nil {
return fmt.Errorf("error sending ICMP message: %w", err)
}

conn.SetReadDeadline(currentTime.Add(timeout))

responseBytes := make([]byte, 1500)
n, remoteAddress, err := conn.ReadFrom(responseBytes)
if err != nil {
return fmt.Errorf("error reading ICMP response: %w", err)
}

responseMessage, err := icmp.ParseMessage(ipv4.ICMPTypeEchoReply.Protocol(), responseBytes[:n])
if err != nil {
return fmt.Errorf("error parsing ICMP response: %w", err)
}

switch responseMessage.Type {
case ipv4.ICMPTypeEchoReply:
fmt.Printf("来自 '%s' 的回复: bytes=%d time=%v\n", remoteAddress, n, time.Since(currentTime))
echo, ok := responseMessage.Body.(*icmp.Echo)
if !ok {
return fmt.Errorf("can not convert message body to icmp.Echo type")
}
fmt.Printf("Body Data: %s\n", string(echo.Data))
default:
return fmt.Errorf("unexpected ICMP message type: %v, code: %v", responseMessage.Type, responseMessage.Code)
}

return nil
}

留意一点区别:在 Windows 下编译后生成的程序可以直接运行,但是在 Linux 下编译生成的程序需要 root 权限,这跟不同操作系统的原始套接字操作权限相关。

参考

使用 Go 实现 ping 工具