你知道什么是 Ping 吗?

欢迎到我的博客浏览 胤凯 (oyto.github.io)

这次我们来看一下什么是 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 报文由 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` 函数中,启用命令行参数设置:

Go 复制代码
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` 去做:

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

​ 通过百度百科可以查到,我们要使用的是 Ping 请求,即回显请求,其对应的 Type 和 Code 如下:

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

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

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

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

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

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

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

Go 复制代码
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个字节,即 data2 和 data3

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

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

Go 复制代码
// 设置超时时间
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 地址,就可以构建我们的打印信息了:

Go 复制代码
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 工具,在这个过程中大家可以收获到很多东西,希望大家能够自己动手实现一下,结果一定不会让你失望。

相关推荐
Scott9999HH1 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社2 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海2 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖3 小时前
JDK 26 新特性详解
java·开发语言
马优晨3 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区5 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大6 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai6 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜6 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa6 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio