当前位置: 首页 > article >正文

用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多个目标,提高效率。


http://www.kler.cn/a/415809.html

相关文章:

  • Python读取摄像头视频并将其保存为MP4文件
  • 利用Java爬虫获得店铺详情:技术解析
  • KUKA机器人中断编程5—自动回原点功能的编程
  • 工程企业如何做好成本控制?该如何入手?
  • 和鲸科技创始人CEO范向伟出席首届工业智算产业发展研讨会,共话 AI 创新与产业化落地
  • 在windows系统中安装python并确认安装成功
  • 中信建投张青:以金融智慧点亮公益新篇章
  • Flink的双流join理解
  • 一次完整的CNAS软件测试实验室内部审核流程
  • Ubuntu-20.04安装 terminator
  • Spring Boot教程之十二: Spring – RestTemplate
  • [巅峰极客 2021]签到
  • 如何具体实现商品详情的提取?
  • 等保测评在云计算方面的应用讲解
  • 从覆盖到拼接:优化 onInput 事件的输入
  • uniapp开发微信小程序笔记8-uniapp使用vant框架
  • 3.26线性回归对率回归
  • 家校通小程序实战教程02口令管理
  • 【SCT61240QFJCR】用于超小型汽车摄像头模块的四通道电源管理IC,国产 车规
  • Git 提交代码日志信息