手把手教你用Go实现Ping操作

oYto · · 44605 次点击 · 开始浏览    置顶

> 欢迎大家到我的博客浏览 <a href="https://www.yinkai.cc/post/74f3b4b7d2699b0fb647468756e0475d">YinKai 's Blog | 手把手教你用Go实现Ping操作</a> ​ 这次我们来看一下什么是 Ping 操作,以及它有什么用处,并且我们来动手实现一个简易版的 Ping 工具。<!--more--> #### 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报文](/images/ICMP报文.png) ​ 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 如下: ![ICMPPing请求](/images/ICMPPing请求.png) ​ 同样,我们在全局变量中添加对应的值: ```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个字节,即 data[2] 和 data[3]。 ```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回复报文](/images/icmp回复报文.png) ​ 根据抓到的 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 工具,在这个过程中大家可以收获到很多东西,希望大家能够自己动手实现一下,结果一定不会让你失望。

有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

44605 次点击  ∙  1 赞  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传