用Go实现Ping操作

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: **摘要:**本文介绍了 Ping 操作的原理和用途,并展示了如何用 Go 语言实现一个简单的 Ping 工具。Ping 是一个用于测试网络连接可达性和测量往返时间(RTT)的工具,常用于网络故障排查和监测网络稳定性。文中详细讲解了 ICMP 报文的结构,以及如何构建和发送 ICMP 请求报文,包括设置类型、代码、校验和、ID 和序列号等。通过示例代码,演示了如何编写 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 工具,在这个过程中大家可以收获到很多东西,希望大家能够自己动手实现一下,结果一定不会让你失望。


转载来源:https://juejin.cn/post/7357142305423933494

相关文章
|
7月前
|
Go 索引
掌握Go语言:Go语言范围,优雅遍历数据结构,简化代码操作实战解析(24)
掌握Go语言:Go语言范围,优雅遍历数据结构,简化代码操作实战解析(24)
|
6月前
|
SQL 关系型数据库 MySQL
go 通过sql操作mysql
go 通过sql操作mysql
30 1
|
7月前
|
存储 缓存 NoSQL
【Go语言专栏】Go语言中的Redis操作与缓存应用
【4月更文挑战第30天】本文探讨了在Go语言中使用Redis进行操作和缓存应用的方法。文章介绍了Redis作为高性能键值存储系统,用于提升应用性能。推荐使用`go-redis/redis`库,示例代码展示了连接、设置、获取和删除键值对的基本操作。文章还详细阐述了缓存应用的步骤及常见缓存策略,包括缓存穿透、缓存击穿和缓存雪崩的解决方案。利用Redis和合适策略可有效优化应用性能。
140 0
|
6月前
|
监控 Go
go语言并发实战——日志收集系统(八) go语言操作etcd以及利用watch实现对键值的监控
go语言并发实战——日志收集系统(八) go语言操作etcd以及利用watch实现对键值的监控
go语言并发实战——日志收集系统(八) go语言操作etcd以及利用watch实现对键值的监控
|
7月前
|
存储 安全 编译器
go语言中进行不安全的类型操作
【5月更文挑战第10天】Go语言中的`unsafe`包提供了一种不安全但强大的方式来处理类型转换和底层内存操作。包含两个文档用途的类型和八个函数,本文也比较了不同变量和结构体的大小与对齐系数,强调了字段顺序对内存分配的影响。
124 8
go语言中进行不安全的类型操作
|
5月前
|
分布式计算 大数据 Go
MaxCompute操作报错合集之使用go sdk调用GetTunnelEndpoint出现报错:InvalidAction.NotFoundSpecified api is not found,该如何解决
MaxCompute是阿里云提供的大规模离线数据处理服务,用于大数据分析、挖掘和报表生成等场景。在使用MaxCompute进行数据处理时,可能会遇到各种操作报错。以下是一些常见的MaxCompute操作报错及其可能的原因与解决措施的合集。
|
5月前
|
运维 Devops API
阿里云云效操作报错合集之直接用API调用可以使用,但是本地用Go代码调用失败,是什么导致的?
本合集将整理呈现用户在使用过程中遇到的报错及其对应的解决办法,包括但不限于账户权限设置错误、项目配置不正确、代码提交冲突、构建任务执行失败、测试环境异常、需求流转阻塞等问题。阿里云云效是一站式企业级研发协同和DevOps平台,为企业提供从需求规划、开发、测试、发布到运维、运营的全流程端到端服务和工具支撑,致力于提升企业的研发效能和创新能力。
|
6月前
|
关系型数据库 MySQL Go
Go语言介绍以及如何在Go语言中操作MySQL数据库
Go语言介绍以及如何在Go语言中操作MySQL数据库
60 3
|
6月前
|
NoSQL 安全 Go
Go 语言 mongox 库:简化操作、安全、高效、可扩展、BSON 构建
go mongox 是一个基于泛型的库,扩展了 MongoDB 的官方库。通过泛型技术,它实现了结构体与 MongoDB 集合的绑定,旨在提供类型安全和简化的数据操作。 go mongox 还引入链式调用,让文档操作更流畅,并且提供了丰富的 BSON 构建器和内置函数,简化了 BSON 数据的构建。 此外,它还支持插件化编程和内置多种钩子函数,为数据库操作前后的自定义逻辑提供灵活性,增强了应用的可扩展性和可维护性。
100 6
|
6月前
|
存储 NoSQL Go
轻松上手,使用Go语言操作Redis数据库
轻松上手,使用Go语言操作Redis数据库
下一篇
无影云桌面