用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
命令的工作原理
- 发送一个ICMP回显请求包到目标主机。
- 等待目标主机回一个ICMP回显应答包。
- 记录时间,计算往返时延(RTT)。
- 根据结果计算丢包率、平均时延等统计信息。
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
程序后,我们可以编写一个简单的测试脚本来验证它的功能。这个脚本将执行以下操作:
- 给
goping
程序添加所需的cap_net_raw
权限,以便它能够使用原始套接字(raw socket)发送ICMP请求。 - 调用
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
脚本说明
-
设置
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系统
-
检查权限是否添加成功:
脚本使用getcap
命令验证是否成功为goping
程序添加了cap_net_raw
权限。如果没有成功添加,脚本会输出错误信息并退出。 -
运行
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)!