3

使用Go实现ping工具

 1 year ago
source link: https://colobu.com/2023/04/26/write-the-ping-tool-in-Go/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

使用Go实现ping工具

ping是一个网络工具,它被广泛地用于测试网络连接的质量和稳定性。当我们想知道我们的电脑是否能够与其他设备或服务器进行通信时,ping就是我们最好的朋友。当我们想侦测网络之间的连通性和网络质量的时候,也常常使用ping工具测量,因为它是操作系统常带的一个网络诊断工具,小而强大。

ping最初是由Mike Muuss在1983年为Unix系统开发的。它的名字是来自于海军潜艇的声纳系统,声纳系统通过发送一个声波并测量其返回时间来确定目标的位置。Ping的工作原理类似,它发送一个小数据包到目标设备,然后等待该设备返回一个响应,以测量其响应时间和延迟。

当我们使用Ping测试网络连接时,它能够告诉我们两个重要的指标:延迟和丢包率。延迟是指从发送ping请求到接收到响应所需的时间,通常以毫秒为单位计算。丢包率则是指在ping请求和响应之间丢失的数据包的百分比。如果丢包率过高,说明网络连接可能存在问题,导致数据传输不稳定或者甚至无法连接。

除了基本的ping命令外,还有许多其他ping命令和选项可供使用。例如,可以使用“-c”选项指定发送ping请求的次数,使用“-i”选项指定Ping请求之间的时间间隔。此外,还可以使用“-s”选项指定发送ping请求的数据包大小。

尽管ping是一个非常有用的工具,但它也有一些限制。ping测试的结果可能会受到许多因素的影响,例如网络拥塞、防火墙、路由器丢弃等等。此外,一些设备或服务器可能已禁用对ping请求的响应,因此无法获得ping测试的结果。

尽管它有一些限制,但它仍然是网络管理员和用户必备的工具之一。

ping工具是基于rfc 792 (ICMP协议)来实现的。它是一份名为“Internet控制消息协议(ICMP)规范”的文件,由Jon Postel和J. Reynolds在1981年9月发布。该文档定义了ICMP协议,该协议是TCP/IP网络协议套件中的一个重要组成部分。

ICMP协议是一种网络层协议,用于传输与网络控制和错误处理相关的消息。该协议通常与IP协议一起使用,用于在Internet上交换信息。RFC 792详细介绍了ICMP协议中的不同消息类型及其用途。ping就是利用发送一个Echo请求得到一个Echo Reply实现的。

echo.png
  • type: 8代表echo消息, 0代表echo reply消息
  • code: 总是0
  • checksum:整个消息的校验和
  • Identifier:用来匹配echo和reply,我们常常使用进程ID
  • Sequence Number:序列号,也是用来匹配echo和reply, 比如同一个进程ID,不同的序列号代表不同的echo
  • Data: payload值。所以我们使用ping的时候是可以使用一定大小的数据的,用来测试MTU或者什么

ICMP是封装在IP包中传输的。

等下一篇介绍traceroute工具实现的时候,我们还会介绍ICMP。

对于ping来说,就是简单的发送一个echo消息,收到对应的echo reply消息后计算时延,如果超时未收到reply,就计算一次丢包。

比如我们常用的ping命令,可以显示收包情况和时延,你可以指定发送的总数,最后还会有一个统计信息:

ping.png

本文的介绍和例子都是针对IPV4的,IPv6类似但又有些不同。

你在网上搜ping.c,很容易搜到通过C语言实现的ping工具,如果使用Go语言,也有几个实现方式。

“作弊”方式

最容易的实现方式就是调用操作系统中自带的ping工具:

package main
import (
"fmt"
"os/exec"
func main() {
host := os.Args[1]
output, err := exec.Command("ping", "-c", "3", host).CombinedOutput()
if err != nil {
panic(err)
fmt.Println(string(output))

简单几行代码。

使用 golang.org/x/net/icmp

Go的net扩展库专门实现了icmp协议。我们可以使用它来实现ping。

插入一个知识点。
如果使用SOCK_RAW实现ping,是需要cap_net_raw权限的,你可以通过下面的命令设置:

setcap cap_net_raw=+ep /path/to/your/compiled/binary

在Linux 3.0新实现了一种Socket方式,可以实现普通用户也能执行ping命令:

socket(PF_INET, SOCK_DGRAM, IPPROTO_ICMP)

不过你还需要设置:

sudo sysctl -w net.ipv4.ping_group_range="0 2147483647"

首先,我们实现non-privileged ping方式的ping, icmp包为我们做了封装,所以我们不必使用底层的socket,而是直接使用icmp.ListenPacket("udp4", "0.0.0.0")来实现。

完整的代码如下:

package main
import (
"fmt"
"log"
"net"
"time"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
const (
protocolICMP = 1
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s host\n", os.Args[0])
os.Exit(1)
host := os.Args[1]
// 使用icmp得到一个*packetconn,注意这里的network我们设置的`udp4`
c, err := icmp.ListenPacket("udp4", "0.0.0.0")
if err != nil {
log.Fatal(err)
defer c.Close()
// 生成一个Echo消息
msg := &icmp.Message{
Type: ipv4.ICMPTypeEcho,
Code: 0,
Body: &icmp.Echo{
ID: os.Getpid() & 0xffff,
Seq: 1,
Data: []byte("Hello, are you there!"),
wb, err := msg.Marshal(nil)
if err != nil {
log.Fatal(err)
// 发送,注意这里必须是一个UDP地址
start := time.Now()
if _, err := c.WriteTo(wb, &net.UDPAddr{IP: net.ParseIP(host)}); err != nil {
log.Fatal(err)
// 读取回包
reply := make([]byte, 1500)
err = c.SetReadDeadline(time.Now().Add(5 * time.Second))
if err != nil {
log.Fatal(err)
n, peer, err := c.ReadFrom(reply)
if err != nil {
log.Fatal(err)
duration := time.Since(start)
// 得到的回包是一个ICMP消息,先解析出来
msg, err = icmp.ParseMessage(protocolICMP, reply[:n])
if err != nil {
log.Fatal(err)
// 打印结果
switch msg.Type {
case ipv4.ICMPTypeEchoReply: // 如果是Echo Reply消息
echoReply, ok := msg.Body.(*icmp.Echo) // 消息体是Echo类型
if !ok {
log.Fatal("invalid ICMP Echo Reply message")
return
// 这里可以通过ID, Seq、远程地址来进行判断,下面这个只使用了两个判断条件,是有风险的
// 如果此时有其他程序也发送了ICMP Echo,序列号一样,那么就可能是别的程序的回包,只不过这个几率比较小而已
// 如果再加上ID的判断,就精确了
if peer.(*net.UDPAddr).IP.String() == host && echoReply.Seq == 1 {
fmt.Printf("Reply from %s: seq=%d time=%v\n", host, msg.Body.(*icmp.Echo).Seq, duration)
return
default:
fmt.Printf("Unexpected ICMP message type: %v\n", msg.Type)

关键代码都加了注释,主要注意回包的解析和回包的判断。尤其是回包的判断,我们在下一章实现traceroute的时候尤其需要注意这一点。

使用 ip4:icmp 实现

即使我们想实现privileged ping,我们也不需要直接使用raw socket,还是使用icmp包。

在这种场景下,我们的network需要是ip4:icmp,能够发送ICMP包,而不是上面的udp4

package main
import (
"fmt"
"log"
"net"
"time"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
const (
protocolICMP = 1
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "usage: %s host\n", os.Args[0])
os.Exit(1)
host := os.Args[1]
// 解析目标主机的 IP 地址
dst, err := net.ResolveIPAddr("ip", host)
if err != nil {
log.Fatal(err)
// 创建 ICMP 连接
conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
if err != nil {
log.Fatal(err)
defer conn.Close()
// 构造 ICMP 报文
msg := &icmp.Message{
Type: ipv4.ICMPTypeEcho,
Code: 0,
Body: &icmp.Echo{
ID: os.Getpid() & 0xffff,
Seq: 1,
Data: []byte("Hello, are you there!"),
msgBytes, err := msg.Marshal(nil)
if err != nil {
log.Fatal(err)
// 发送 ICMP 报文
start := time.Now()
_, err = conn.WriteTo(msgBytes, dst)
if err != nil {
log.Fatal(err)
// 接收 ICMP 报文
reply := make([]byte, 1500)
for i := 0; i < 3; i++ {
err = conn.SetReadDeadline(time.Now().Add(5 * time.Second))
if err != nil {
log.Fatal(err)
n, peer, err := conn.ReadFrom(reply)
if err != nil {
log.Fatal(err)
duration := time.Since(start)
// 解析 ICMP 报文
msg, err = icmp.ParseMessage(protocolICMP, reply[:n])
if err != nil {
log.Fatal(err)
// 打印结果
switch msg.Type {
case ipv4.ICMPTypeEchoReply:
echoReply, ok := msg.Body.(*icmp.Echo)
if !ok {
log.Fatal("invalid ICMP Echo Reply message")
return
if peer.String() == host && echoReply.ID == os.Getpid()&0xffff && echoReply.Seq == 1 {
fmt.Printf("reply from %s: seq=%d time=%v\n", dst.String(), msg.Body.(*icmp.Echo).Seq, duration)
return
default:
fmt.Printf("unexpected ICMP message type: %v\n", msg.Type)

和上面的例子比较,主要是发送的逻辑不一样,大同小异,发送额度内容都是ICMP Echo消息,只不过这次发送的失效,地址不是UDP地址,而是IP 地址。

使用go-ping

虽然Go net扩展库提供了icmp包,方便我们实现ping能力,但是代码还是有点偏底层的处理,网上有一个go-ping/ping库,还是被使用很多的,提供了更高级或者说更傻瓜的方法。

三年了,疫情给世界带来的影响已经潜移默化到影响到互联网,影响到开源社区。我看到很多的开源项目因为一些原因都不再维护了,包括这个go-ping项目,光靠作者用爱发电无法做到持久,有点遗憾,不过好歹是它已经比较成熟了,我们项目中使用没有问题。prometheus社区基于这个项目,维护了一个新的项目:pro-bing

它的README文档中的例子已经很少的解释了它的使用方法,你可以利用它实现一个类似ping工具的功能,如果想大批量实现ping的功能,这个库就不合适了。

下面代码就是一个ping的基本功能,没什么好说的,ping3次得到结果:

// ping 并收集结果
pinger, err := probing.NewPinger("github.com")
if err != nil {
panic(err)
// ping的次数
pinger.Count = 3
err = pinger.Run() // 阻塞直到完成或者超时
if err != nil {
panic(err)
stats := pinger.Statistics() // 得到统计结果
pretty.Println(stats)

如果要实现Linux下ping的功能,可以稍微复杂些:

pinger, err = probing.NewPinger("github.com")
if err != nil {
panic(err)
// Listen for Ctrl-C.
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
for _ = range c {
pinger.Stop()
pinger.OnRecv = func(pkt *probing.Packet) {
fmt.Printf("%d bytes from %s: icmp_seq=%d time=%v\n",
pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt)
pinger.OnDuplicateRecv = func(pkt *probing.Packet) {
fmt.Printf("%d bytes from %s: icmp_seq=%d time=%v ttl=%v (DUP!)\n",
pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt, pkt.TTL)
pinger.OnFinish = func(stats *probing.Statistics) {
fmt.Printf("\n--- %s ping statistics ---\n", stats.Addr)
fmt.Printf("%d packets transmitted, %d packets received, %v%% packet loss\n",
stats.PacketsSent, stats.PacketsRecv, stats.PacketLoss)
fmt.Printf("round-trip min/avg/max/stddev = %v/%v/%v/%v\n",
stats.MinRtt, stats.AvgRtt, stats.MaxRtt, stats.StdDevRtt)
fmt.Printf("PING %s (%s):\n", pinger.Addr(), pinger.IPAddr())
err = pinger.Run()
if err != nil {
panic(err)

前面也说了,处理返回的消息并和发送请求做匹配是一个技术点,那么go-ping是怎么实现的呢?主要是下面的代码:

switch pkt := m.Body.(type) {
case *icmp.Echo:
if !p.matchID(pkt.ID) {
return nil
if len(pkt.Data) < timeSliceLength+trackerLength {
return fmt.Errorf("insufficient data received; got: %d %v",
len(pkt.Data), pkt.Data)
pktUUID, err := p.getPacketUUID(pkt.Data)
if err != nil || pktUUID == nil {
return err
timestamp := bytesToTime(pkt.Data[:timeSliceLength])
inPkt.Rtt = receivedAt.Sub(timestamp)
inPkt.Seq = pkt.Seq
// 检查是否收到重复的包
if _, inflight := p.awaitingSequences[*pktUUID][pkt.Seq]; !inflight {
p.PacketsRecvDuplicates++
if p.OnDuplicateRecv != nil {
p.OnDuplicateRecv(inPkt)
return nil
// 已经得到返回结果
delete(p.awaitingSequences[*pktUUID], pkt.Seq)
p.updateStatistics(inPkt)
default:
// Very bad, not sure how this can happen
return fmt.Errorf("invalid ICMP echo reply; type: '%T', '%v'", pkt, pkt)

首先检查body必须是*icmp.Echo类型,这是基本操作。接着检查pkt.ID,这一下就把非本程序的包ICMP echo reply包过滤了。

这里它在发送的payload中还加上自己的uuid和发送的时间戳。

这里还处理了重复的包, uuid+seq标识同一个Echo请求。

通过这几个例子,你应该了解了ping工具的底层实现,收藏起来,遇到相关的问题的时候不妨返回来查一查。

下一篇,我们讲介绍traceroute工具的实现,相对ping来说更复杂了,但是都和ICMP协议相关。如果你有感兴趣的网络话题,“点击原文”进行讨论。

Go高级网络编程系列


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK