自学内容网 自学内容网

用Go语言重写Linux系统命令 -- ping

用Go语言重写Linux系统命令 – ping

1. 引言

说到网络诊断工具,ping绝对是居家旅行、修电脑、撕运维的必备神器!它通过ICMP协议测试目标主机的连通性,简单却无比实用。那么,为什么不尝试自己实现一个呢?用Go语言重写ping不仅能学到网络编程的核心技能,还能装作不经意地向同事炫耀:“哦,这个ping,我自己写的。”


2. 基础概念与原理

在动手之前,咱们得先补补课,不然敲代码就像闭着眼玩俄罗斯方块。

咱这里只是简单介绍下, 详细的原理可以参考 ICMP协议详解与实践指南

2.1 什么是ICMP协议?

ICMP(Internet Control Message Protocol)是一种网络层协议,专门用来发送控制消息,比如告诉你“哎,目标主机不可达”之类的坏消息。ping命令正是通过ICMP的“回显请求”和“回显应答”来测试连通性。

2.2 ping命令的工作原理

  1. 发送一个ICMP回显请求包到目标主机。
  2. 等待目标主机回一个ICMP回显应答包。
  3. 记录时间,计算往返时延(RTT)。
  4. 根据结果计算丢包率、平均时延等统计信息。

2.3 ICMP包的结构

一个典型的ICMP回显请求包结构如下:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     Type      |     Code      |          Checksum             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           Identifier          |        Sequence Number        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     Data ...
+-+-+-+-+-+-+-+-+-+-

3. 项目结构与代码设计

在设计项目时,划分清晰的模块可以让代码更加可维护、可扩展。我们将把整个项目划分为几个核心功能模块,确保逻辑清晰、职责分明。


3.1 项目初始化

在开始编码前,先初始化一个Go项目。我们使用go mod来管理依赖,创建一个干净的工作目录:

mkdir go-ping
cd go-ping
go mod init go-ping

这样做的好处是,即使你日后引入其他依赖包,也可以轻松管理和更新。


3.2 代码模块划分

为了避免代码成了“一锅乱炖”,我们将功能拆分为几个模块:

1. ICMP包构造模块

职责:构建符合ICMP协议的请求包。
该模块主要负责:

  • 构造ICMP包的头部和数据部分。
  • 计算ICMP校验和。

相关函数

  • calculateChecksum(data []byte) uint16:计算校验和。

构造ICMP包的示例代码:

{
msg := make([]byte, 8+56) // 8字节头部 + 56字节数据
msg[0] = icmpEchoRequest  // Type: 回显请求
msg[1] = 0                // Code: 无特定代码
msg[4] = byte(id >> 8)    // Identifier (高字节)
msg[5] = byte(id & 0xff)  // Identifier (低字节)
msg[6] = byte(seq >> 8)   // Sequence number (高字节)
msg[7] = byte(seq & 0xff) // Sequence number (低字节)

// 填充数据部分
for i := 8; i < len(msg); i++ {
msg[i] = byte(i - 8)
}

// 计算校验和并填充
checksum := calculateChecksum(msg)
msg[2] = byte(checksum >> 8)
msg[3] = byte(checksum & 0xff)

}

2. 网络通信模块

职责:负责发送和接收ICMP包,处理超时与错误。
该模块主要负责:

  • 与目标主机建立ICMP连接。
  • 发送构造好的ICMP包。
  • 接收ICMP响应包,并计算往返时间(RTT)。

相关函数

  • PingWithTimeout(ip string, timeout int, seq int) error:发送ICMP包并接收响应。

核心网络操作示例:

func PingWithTimeout(ip string, timeout int, seq int) error {
conn, err := net.Dial("ip4:icmp", ip)
if err != nil {
return fmt.Errorf("无法连接到目标主机: %v", err)
}
defer conn.Close()

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

// 构建并发送ICMP请求包
msg := makeICMPRequest(seq, os.Getpid() & 0xffff)
start := time.Now()
_, err = conn.Write(msg)
if err != nil {
return fmt.Errorf("发送ICMP请求失败: %v", err)
}
// 接收响应
buffer := make([]byte, 1024)
n, err := conn.Read(buffer)
elapsed := time.Since(start)
if err != nil {
return fmt.Errorf("接收超时或错误: %v", err)
}
// 解析TTL和RTT信息
ttl := int(buffer[8])
fmt.Printf("%d bytes from %s: icmp_seq=%d ttl=%d time=%.3f ms\n", n, ip, seq, ttl, float64(elapsed.Microseconds())/1000)

return nil
}

3. 统计模块

职责:收集并输出统计信息,包括发送包、接收包、丢包率以及RTT统计。
该模块主要负责:

  • 记录发送和接收的包数量。
  • 计算丢包率、最小/最大/平均RTT。

相关结构和函数

  • PingStats结构体:存储统计数据。
  • PingStatistics()函数:输出统计结果。

统计信息示例:

type PingStats struct {
packetsSent     int
packetsReceived int
rtt             []time.Duration
}

func PingStatistics() {
loss := float64(stats.packetsSent-stats.packetsReceived) / float64(stats.packetsSent) * 100
rttMin, rttMax, rttAvg, rttSum := min_max_avg_sum(stats.rtt)

fmt.Printf("\n--- %s ping statistics ---\n", host)
fmt.Printf("%d packets transmitted, %d received, %.2f%% packet loss, time %dms\n",
stats.packetsSent, stats.packetsReceived, loss, rttSum.Microseconds())
fmt.Printf("rtt min/avg/max = %.3f/%.3f/%.3f ms\n",
float64(rttMin.Microseconds())/1000,
float64(rttAvg.Microseconds())/1000,
float64(rttMax.Microseconds())/1000)
}

3.3 数据流简图

为了帮助理解整个流程,我们可以绘制一个简单的数据流:

用户输入 -> 解析IP -> 构建ICMP包 -> 发送包 -> 接收包 -> 统计与输出

4. 完整代码

4.1 icmp.go

package main

import (
"fmt"
"log"
"net"
"os"
"time"
)

// ping统计信息
var stats PingStats
var host string

// ping请求
// ICMP Type: 8 (Echo Request)
const icmpEchoRequest = 8

// calculateChecksum 计算ICMP校验和
func calculateChecksum(data []byte) uint16 {
var sum int
for i := 0; i < len(data)-1; i += 2 {
sum += int(data[i])<<8 | int(data[i+1])
}
if len(data)%2 == 1 {
sum += int(data[len(data)-1]) << 8
}
for (sum >> 16) > 0 {
sum = (sum >> 16) + (sum & 0xffff)
}
return uint16(^sum)
}

// Ping 测试目标IP是否可达
func PingWithTimeout(ip string, timeout int, seq int) error {
conn, err := net.Dial("ip4:icmp", ip)
if err != nil {
log.Printf("Failed to connect to %s: %v\n", ip, err)
return err
}
defer conn.Close()

//
// 构造ICMP请求包
//
//     0                   1                   2                   3
//    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
//    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//    |     Type      |     Code      |          Checksum             |
//    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//    |           Identifier          |        Sequence Number        |
//    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//    |     Data ...
//    +-+-+-+-+-

id := os.Getpid() & 0xffff
// 构造ICMP请求包(8字节头 + 56字节数据)
msg := make([]byte, 8+56)
msg[0] = icmpEchoRequest  // Type
msg[1] = 0                // Code
msg[4] = byte(id >> 8)    // Identifier (高字节)
msg[5] = byte(id & 0xff)  // Identifier (低字节)
msg[6] = byte(seq >> 8)   // Sequence number (高字节)
msg[7] = byte(seq & 0xff) // Sequence number (低字节)

// 填充数据部分(56字节)为递增字节或其他占位符
for i := 8; i < len(msg); i++ {
msg[i] = byte(i - 8) // 示例填充数据
}
// 最后填充校验和
checksum := calculateChecksum(msg)
msg[2] = byte(checksum >> 8)
msg[3] = byte(checksum & 0xff)

// 设置写超时
conn.SetDeadline(time.Now().Add(time.Duration(timeout) * time.Second))
start := time.Now()
_, err = conn.Write(msg)
if err != nil {
log.Printf("Failed to send ICMP request: %v\n", err)
return err
}
stats.packetsSent++

// 接收ICMP响应
buffer := make([]byte, 1024)
n, err := conn.Read(buffer)
elapsed := time.Since(start)
if err != nil {
fmt.Printf("Request timeout for icmp_seq %d\n", seq)
return err
}
// 获取ttl值
ttl := int(buffer[8])
// 更新统计信息
stats.rtt = append(stats.rtt, elapsed)
stats.packetsReceived++

fmt.Printf("%d bytes from %s: icmp_seq=%d ttl=%d time=%.3f ms\n", n, ip, seq, ttl, float64(elapsed.Microseconds())/1000)
return nil
}

// ping指定count个数据包
func Pings(ip string, count int, stopCh <-chan struct{}) {
host = ip

for i := 0; i < count || count == 0; i++ {
select {
case <-stopCh:
return
default:
err := PingWithTimeout(ip, 5, i+1)
if err != nil {
fmt.Printf("Failed to ping %s: %v\n", ip, err)
}
time.Sleep(1 * time.Second)
}
}

PingStatistics()
}

// Ping 统计信息
type PingStats struct {
packetsSent     int
packetsReceived int
rtt             []time.Duration
}

// PingStatistics 输出ping统计信息
func PingStatistics() {
// 计算丢包率
loss := float64(0)
if stats.packetsSent != 0 {
loss = float64(stats.packetsSent-stats.packetsReceived) / float64(stats.packetsSent) * 100
}
// 计算最小/最大/平均/总延迟
rttMin, rttMax, rttAvg, rttSum := min_max_avg_sum(stats.rtt)

fmt.Printf("\n--- %s ping statistics ---\n", host)
fmt.Printf("%d packets transmitted, %d received, %.2f%% packet loss, time %dms\n",
stats.packetsSent, stats.packetsReceived, loss, rttSum.Microseconds())
fmt.Printf("rtt min/avg/max = %.3f/%.3f/%.3f ms\n",
float64(rttMin.Microseconds())/1000,
float64(rttAvg.Microseconds())/1000,
float64(rttMax.Microseconds())/1000)
}

func min_max_avg_sum(values []time.Duration) (time.Duration, time.Duration, time.Duration, time.Duration) {
var min, max, sum time.Duration
if len(values) == 0 {
return 0, 0, 0, 0
}
min = values[0]
max = values[0]
sum = values[0]
for _, value := range values[1:] {
if value < min {
min = value
}
if value > max {
max = value
}
sum += value
}
avg := sum / time.Duration(len(values))
return min, max, avg, sum
}

4.2 main.go

package main

import (
"flag"
"fmt"
"log"
"os"
)

// 自定义 Usage 函数
func customUsage() {
fmt.Fprintf(os.Stderr, "Usage: %s [options] target\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Options:\n")
flag.PrintDefaults()
}

func main() {
// 自定义 Usage 函数
flag.Usage = customUsage
// 定义命令行参数
count := flag.Int("c", 4, "Number of ICMP requests to send")
flag.Parse()

// 获取剩余的非标志参数
args := flag.Args()
if len(args) == 0 {
log.Println("Target address is required.")
flag.Usage()
os.Exit(1)
}

// 第一个非标志参数为目标地址
target := args[0]

// 解析目标地址
ip, err := Resolve(target)
if err != nil {
log.Fatalf("Failed to resolve target: %v", err)
}

// 打印解析后的参数
fmt.Printf("PING %s (%s) with 56(64) bytes of data.\n", target, ip)

// 信号处理
stopCh := make(chan struct{})
go HandleSignals(stopCh)

// 发送ICMP请求
Pings(ip, *count, stopCh)
}

// Resolve 将域名解析为IP地址
func Resolve(domain string) (string, error) {
ips, err := net.LookupHost(domain)
if err != nil {
return "", err
}
if len(ips) == 0 {
return "", errors.New("no IP addresses found for the domain")
}
return ips[0], nil
}

// HandleSignals 监听中断信号
func HandleSignals(stop chan struct{}) {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)

<-sigCh
close(stop)

PingStatistics()
}

4.3 测试脚本和注意事项

在开发并编译了goping程序后,我们可以编写一个简单的测试脚本来验证它的功能。这个脚本将执行以下操作:

  1. goping程序添加所需的cap_net_raw权限,以便它能够使用原始套接字(raw socket)发送ICMP请求。
  2. 调用goping程序,发送指定数量的ping请求,测试目标主机的连通性。
测试脚本
#!/bin/bash

# goping 程序的路径
goping="./goping"

# 给 goping 程序添加 cap_net_raw 权限, 因为它需要使用 raw socket 发送 ICMP 请求
sudo setcap cap_net_raw+ep $goping

# 检查程序是否成功添加了权限
if ! getcap $goping | grep -q "cap_net_raw"; then
    echo "Error: Failed to set the required capabilities for $goping"
    exit 1
fi

# 运行 goping 程序,指定测试 3 次 ICMP 请求,目标主机为 192.168.100.1
echo "Running ping test..."
$goping -c 3 192.168.100.1

脚本说明
  1. 设置cap_net_raw权限
    goping程序需要使用原始套接字来发送ICMP请求,因此需要为程序添加cap_net_raw权限。这一步是确保程序能够在不依赖root权限的情况下使用原始套接字功能。命令sudo setcap cap_net_raw+ep $goping会为goping程序添加该权限。

    注意:在某些Linux发行版中,setcap命令可能未安装,您可以通过以下命令安装它:

    sudo apt-get install libcap2-bin  # 对于Debian/Ubuntu系统
    
  2. 检查权限是否添加成功
    脚本使用getcap命令验证是否成功为goping程序添加了cap_net_raw权限。如果没有成功添加,脚本会输出错误信息并退出。

  3. 运行goping进行ping测试
    脚本使用$goping -c 3 192.168.100.1命令运行goping程序,并指定发送3个ping请求,目标IP为192.168.100.1。您可以根据需要修改目标IP地址和发送的次数。


5. 优化与改进方向

虽然我们已经实现了一个基本的ping工具,但还有很多改进空间:

  • 更多的命令行参数支持:目前我们只支持-c参数指定ping的次数, 你还可以参考系统的ping命令增加更多参数。
  • IPv6支持:当前实现只支持IPv4,IPv6的ICMP包结构稍有不同。
  • 并发优化:可以使用Go协程同时ping多个目标,提高效率。


原文地址:https://blog.csdn.net/weixin_47763623/article/details/144127452

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!