欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 文化 > 用Go语言重写Linux系统命令 -- ping

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

2024/11/30 10:04:27 来源:https://blog.csdn.net/weixin_47763623/article/details/144127452  浏览:    关键词:用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                   30 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     intpacketsReceived intrtt             []time.Duration
}func PingStatistics() {loss := float64(stats.packetsSent-stats.packetsReceived) / float64(stats.packetsSent) * 100rttMin, 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 mainimport ("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 intfor 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  // Typemsg[1] = 0                // Codemsg[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 = ipfor i := 0; i < count || count == 0; i++ {select {case <-stopCh:returndefault: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     intpacketsReceived intrtt             []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.Durationif 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 mainimport ("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)<-sigChclose(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"; thenecho "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多个目标,提高效率。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com