用Go实现Ping操作

这次我们来看一下什么是 Ping 操作,以及它有什么用处,并且我们来动手实现一个简易版的 Ping 工具。

Ping 是什么?

ping 是一个计算机网络工具,通常用于测试网络连接的可达性和测量往返时间。在大多数操作系统中,ping 命令是一个内置的命令行工具,可以通过命令行终端使用。例如,在 Windows 操作系统中,你可以在命令提示符中运行 ping 命令,而在类 Unix 操作系统(如 Linux 和 macOS)中,你可以在终端中使用 ping 命令。通常,命令的语法是 ping 目标主机或 IP,然后命令将输出与目标主机的通信状态和 RTT 相关的信息。

Ping 有什么用处?

Ping 工具主要有以下几个主要用途:

  1. 测试主机的可达性ping 命令用于检查另一个主机是否可以在网络上访问。它向目标主机发送一个小的数据包(通常是 ICMP Echo Request),如果目标主机正常工作,它将响应一个回复数据包(通常是 ICMP Echo Reply)。如果没有响应,那么目标主机可能无法访问或处于离线状态。
  2. 测量往返时间(RTT)ping 命令通常会显示每次请求和响应之间的时间差,这被称为往返时间(RTT)。这个值表示了数据从发送端到接收端的往返延迟,通常以毫秒为单位。测量 RTT 对于评估网络性能和延迟非常有用。
  3. 网络故障排除ping 是网络故障排除的有用工具之一。通过检查 ping 的输出,网络管理员可以确定网络连接是否正常,以及延迟是否在可接受范围内。如果 ping 失败,管理员可以进一步调查网络故障的原因。
  4. 监测网络稳定性ping 命令还可以用于监测网络的稳定性。通过连续地向目标主机发送 ping 请求,可以了解网络连接的质量和稳定性。如果出现不稳定性,管理员可以及时采取措施。

动手实现一个 Ping 工具

​ 首先,我们要了解一下 Ping 操作的工作原理:向网络上的另一个主机系统发送 ICMP 报文,如果指定系统得到了报文,它将把回复报文传回给发送者。

​ ICMP 报文由 ICMP 报文头 和 数据包组成,其报文头包含 Type、Code、Checksum、ID、SequenceNum 字段。因此,我们需要先在本地主机上定义 ICMP 请求报文结构体:

go 复制代码
type ICMP struct {
    Type        uint8  // 类型
    Code        uint8  // 代码
    CheckSum    uint16 // 校验和
    ID          uint16 // ID
    SequenceNum uint16 // 序号
}

​ 上面只是 ICMP 的报文头,我们在后面还需要为这个报文构建请求数据。需要注意的是,定义的顺序不能乱,因为我们发送数据包是按字节发送的,所以获取对应的字段的时候,也是按照对应字段的位置去获取的,如果顺序乱了,获取到的数据就会出错。

​ 在构建数据之前,我们先设置好命令行参数,以获取对应参数和目标 IP,同时需要定义全局变量,将命令行参数绑定到对应的变量中,方便使用:

go 复制代码
var (
    helpFlag bool
    timeout  int64 // 耗时
    size     int   // 大小
    count    int   // 请求次数
)

func GetCommandArgs() {
    flag.Int64Var(&timeout, "w", 1000, "请求超时时间")
    flag.IntVar(&size, "l", 32, "发送字节数")
    flag.IntVar(&count, "n", 4, "请求次数")
    flag.BoolVar(&helpFlag, "h", false, "显示帮助信息")
    flag.Parse()
}

​ 在 main 函数中,启用命令行参数设置:

scss 复制代码
func main() {
    GetCommandArgs()
}

​ 在发送报文前,我们需要先建立连接,此时需要先获取目标 IP,这个由命令行参数中获取:

go 复制代码
// 获取目标 IP
desIP := os.Args[len(os.Args)-1]
// 构建连接
conn, err := net.DialTimeout("ip:icmp", desIP, time.Duration(timeout)*time.Millisecond)
if err != nil {
    log.Println(err.Error())
    return
}
defer conn.Close()
// 远程地址
remoteaddr := conn.RemoteAddr()

​ 连接建立后,我们需要根据参数中的发送次数 count 去发送对应次数的报文,因此需要用 for 去做:

css 复制代码
for i := 0; i < count; i ++ {
    ...
}

​ 同样,我们在全局变量中添加对应的值:

ini 复制代码
var (
    typ      uint8 = 8
    code     uint8 = 0
)

​ 做好前面的准备工作,我们就可以开始构建我们的 ICMP 请求报文了。我们这里以发送的第几次作为 ID 和序列号:

css 复制代码
icmp := &ICMP{
        Type:        typ,
        Code:        code,
        CheckSum:    uint16(0),
        ID:          uint16(i),
        SequenceNum: uint16(i),
    }

​ 由于 ICMP 是使用二进制进行传输的,所以我们需要将信息用二进制表示:

php 复制代码
var buffer bytes.Buffer
binary.Write(&buffer, binary.BigEndian, icmp)

​ 然后根据发送数据的大小 size 构建数据并写在 ICMP 报文后面:

arduino 复制代码
data := make([]byte, size)
buffer.Write(data)
data = buffer.Bytes()

​ 现在,就只差一个校验和字段了,计算 ICMP(Internet Control Message Protocol)报文的校验和字段遵循以下步骤:

  1. 将报文分为 16 位的字(两个字节)。
  2. 对所有字进行按位求和(二进制求和),包括数据部分和报文头。如果有剩余字节(奇数个字节),将其附加到最后一个字节。
  3. 将溢出的进位位(如果有)加回到结果中。
  4. 取结果的二进制反码(按位取反)

​ 代码实现如下:

go 复制代码
func checkSum(data []byte) uint16 {
    // 第一步:两两拼接并求和
    length := len(data)
    index := 0
    var sum uint32
    for length > 1 {
        // 拼接且求和
        sum += uint32(data[index])<<8 + uint32(data[index+1])
        length -= 2
        index += 2
    }
    // 奇数情况,还剩下一个,直接求和过去
    if length == 1 {
        sum += uint32(data[index])
    }

    // 第二部:高 16 位,低 16 位 相加,直至高 16 位为 0
    hi := sum >> 16
    for hi != 0 {
        sum = hi + uint32(uint16(sum))
        hi = sum >> 16
    }
    // 返回 sum 值 取反
    return uint16(^sum)
}

​ 接着再将算出来的校验和放到报文头对应的位置中去,这里需要计算一下位置。假设我们有以下 ICMP 报文:

diff 复制代码
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|      Type       |      Code       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Checksum (2 bytes)       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           Identifier (2 bytes)   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|        Sequence Number (2 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           Data (variable length) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

​ 校验和属于报文的第3、4个字节,即 data[2] 和 data[3]。

scss 复制代码
data[2] = byte(checkSum >> 8)
data[3] = byte(checkSum)

​ 最后再设置一下超时时间,就可以将数据 data 写入连接中了:

scss 复制代码
// 设置超时时间
conn.SetDeadline(time.Now().Add(time.Duration(timeout) * time.Millisecond))

// 将 data 写入连接中,
n, err := conn.Write(data)
if err != nil {
    log.Println(err)
    continue
}

​ 发送完成后,再构建缓冲接收响应包,

go 复制代码
buf := make([]byte, 1024)
n, err = conn.Read(buf)
//fmt.Println(data)
if err != nil {
    log.Println(err)
    continue
}

​ 然后我们就可以从响应包中获取我们需要的数据,比如 IP 地址、TTL等:

​ 根据抓到的 ICMP 响应包,可以知道 IP 头共 20 个字节,源 IP 和 目标 IP 在我们接收的数据包的倒数 8 个字节里,所以我们可以推算出我们访问的 IP 地址,就可以构建我们的打印信息了:

perl 复制代码
fmt.Printf("来自 %d.%d.%d.%d 的回复:字节=%d 时间=%d TTL=%d\n", buf[12], buf[13], buf[14], buf[15], n-28, t, buf[8])

​ 至此,我们 Ping 工具的核心功能就实现了,还有一些统计信息,就不做具体的讲解了,感兴趣的可以从代码中看具体的实现。

完整代码如下:

go 复制代码
package main

import (
    "bytes"
    "encoding/binary"
    "flag"
    "fmt"
    "log"
    "math"
    "net"
    "os"
    "time"
)

// tcp 报文前20个是报文头,后面的才是 ICMP 的内容。
// ICMP:组建 ICMP 首部(8 字节) + 我们要传输的内容
// ICMP 首部:type、code、校验和、ID、序号,1 1 2 2 2
// 回显应答:type = 0,code = 0
// 回显请求:type = 8, code = 0

var (
    helpFlag bool
    timeout  int64 // 耗时
    size     int   // 大小
    count    int   // 请求次数
    typ      uint8 = 8
    code     uint8 = 0
    SendCnt  int                   // 发送次数
    RecCnt   int                   // 接收次数
    MaxTime  int64 = math.MinInt64 // 最大耗时
    MinTime  int64 = math.MaxInt64 // 最短耗时
    SumTime  int64                 // 总计耗时
)

// ICMP 序号不能乱
type ICMP struct {
    Type        uint8  // 类型
    Code        uint8  // 代码
    CheckSum    uint16 // 校验和
    ID          uint16 // ID
    SequenceNum uint16 // 序号
}

func main() {
    fmt.Println()
    log.SetFlags(log.Llongfile)
    GetCommandArgs()

    // 打印帮助信息
    if helpFlag {
        displayHelp()
        os.Exit(0)
    }

    // 获取目标 IP
    desIP := os.Args[len(os.Args)-1]
    //fmt.Println(desIP)
    // 构建连接
    conn, err := net.DialTimeout("ip:icmp", desIP, time.Duration(timeout)*time.Millisecond)
    if err != nil {
        log.Println(err.Error())
        return
    }
    defer conn.Close()
    // 远程地址
    remoteaddr := conn.RemoteAddr()
    fmt.Printf("正在 Ping %s [%s] 具有 %d 字节的数据:\n", desIP, remoteaddr, size)
    for i := 0; i < count; i++ {
        // 构建请求
        icmp := &ICMP{
            Type:        typ,
            Code:        code,
            CheckSum:    uint16(0),
            ID:          uint16(i),
            SequenceNum: uint16(i),
        }

        // 将请求转为二进制流
        var buffer bytes.Buffer
        binary.Write(&buffer, binary.BigEndian, icmp)
        // 请求的数据
        data := make([]byte, size)
        // 将请求数据写到 icmp 报文头后
        buffer.Write(data)
        data = buffer.Bytes()
        // ICMP 请求签名(校验和):相邻两位拼接到一起,拼接成两个字节的数
        checkSum := checkSum(data)
        // 签名赋值到 data 里
        data[2] = byte(checkSum >> 8)
        data[3] = byte(checkSum)
        startTime := time.Now()

        // 设置超时时间
        conn.SetDeadline(time.Now().Add(time.Duration(timeout) * time.Millisecond))

        // 将 data 写入连接中,
        n, err := conn.Write(data)
        if err != nil {
            log.Println(err)
            continue
        }
        // 发送数 ++
        SendCnt++
        // 接收响应
        buf := make([]byte, 1024)
        n, err = conn.Read(buf)
        //fmt.Println(data)
        if err != nil {
            log.Println(err)
            continue
        }
        // 接受数 ++
        RecCnt++
        //fmt.Println(n, err) // data:64,ip首部:20,icmp:8个 = 92 个
        // 打印信息
        t := time.Since(startTime).Milliseconds()
        fmt.Printf("来自 %d.%d.%d.%d 的回复:字节=%d 时间=%d TTL=%d\n", buf[12], buf[13], buf[14], buf[15], n-28, t, buf[8])
        MaxTime = Max(MaxTime, t)
        MinTime = Min(MinTime, t)
        SumTime += t
        time.Sleep(time.Second)
    }

    fmt.Printf("\n%s 的 Ping 统计信息:\n", remoteaddr)
    fmt.Printf("    数据包: 已发送 = %d,已接收 = %d,丢失 = %d (%.f%% 丢失),\n", SendCnt, RecCnt, count*2-SendCnt-RecCnt, float64(count*2-SendCnt-RecCnt)/float64(count*2)*100)
    fmt.Println("往返行程的估计时间(以毫秒为单位):")
    fmt.Printf("    最短 = %d,最长 = %d,平均 = %d\n", MinTime, MaxTime, SumTime/int64(count))
}

// 求校验和
func checkSum(data []byte) uint16 {
    // 第一步:两两拼接并求和
    length := len(data)
    index := 0
    var sum uint32
    for length > 1 {
        // 拼接且求和
        sum += uint32(data[index])<<8 + uint32(data[index+1])
        length -= 2
        index += 2
    }
    // 奇数情况,还剩下一个,直接求和过去
    if length == 1 {
        sum += uint32(data[index])
    }

    // 第二部:高 16 位,低 16 位 相加,直至高 16 位为 0
    hi := sum >> 16
    for hi != 0 {
        sum = hi + uint32(uint16(sum))
        hi = sum >> 16
    }
    // 返回 sum 值 取反
    return uint16(^sum)
}

// GetCommandArgs 命令行参数
func GetCommandArgs() {
    flag.Int64Var(&timeout, "w", 1000, "请求超时时间")
    flag.IntVar(&size, "l", 32, "发送字节数")
    flag.IntVar(&count, "n", 4, "请求次数")
    flag.BoolVar(&helpFlag, "h", false, "显示帮助信息")
    flag.Parse()
}

func Max(a, b int64) int64 {
    if a > b {
        return a
    }
    return b
}

func Min(a, b int64) int64 {
    if a < b {
        return a
    }
    return b
}

func displayHelp() {
    fmt.Println(`选项:
    -n count       要发送的回显请求数。
    -l size        发送缓冲区大小。
    -w timeout     等待每次回复的超时时间(毫秒)。
    -h            帮助选项`)
}

小结

​ 本文讲解了常用工具 Ping,并且从 ICMP 报文角度手把手教大家实现了一个简易版的 Ping 工具,在这个过程中大家可以收获到很多东西,希望大家能够自己动手实现一下,结果一定不会让你失望。

相关推荐
煎鱼eddycjy17 小时前
新提案:由迭代器启发的 Go 错误函数处理
go
煎鱼eddycjy17 小时前
Go 语言十五周年!权力交接、回顾与展望
go
不爱说话郭德纲1 天前
聚焦 Go 语言框架,探索创新实践过程
go·编程语言
0x派大星2 天前
【Golang】——Gin 框架中的 API 请求处理与 JSON 数据绑定
开发语言·后端·golang·go·json·gin
IT书架3 天前
golang高频面试真题
面试·go
郝同学的测开笔记3 天前
云原生探索系列(十四):Go 语言panic、defer以及recover函数
后端·云原生·go
秋落风声4 天前
【滑动窗口入门篇】
java·算法·leetcode·go·哈希表
0x派大星5 天前
【Golang】——Gin 框架中的模板渲染详解
开发语言·后端·golang·go·gin
0x派大星6 天前
【Golang】——Gin 框架中的表单处理与数据绑定
开发语言·后端·golang·go·gin
三里清风_7 天前
如何使用Casbin设计后台权限管理系统
golang·go·casbin