基于Go实现Ping命令指南

一、Ping命令原理介绍

  Ping是网络诊断中最基础的工具之一,其核心是ICMP协议(Internet Control Message Protocol),接下来简单解析一下ICMP协议结构,实现一个支持统计功能的Ping工具。

二、ICMP协议介绍及结构

  ICMPIP协议的辅助协议,用于传递控制信息和错误报告。Ping命令依赖其回显请求(Type 8)和回显应答(Type 0)功能。

  ICMP协议关键特性

  • 类型/代码系统:8/0表示请求,0/0表示应答
  • 校验机制:确保报文完整性
  • 标识序列:区分并发请求(Identifier+SequenceNum)
  • 无连接:无需建立TCP连接的轻量协议

  ICMP报文结构详解

字段名长度(比特)说明
Type8消息类型(请求=8,应答=0)
code8子类型(回显请求/应答中恒为0)
Checksum16头部与数据的校验和
Identifier16进程标识符(区分并发Ping)
Sequence16序列号(标识请求顺序)
Data可变长度负载(通常为时间戳或填充字节)

校验和计算是关键步骤:将报文每16位累加,结果取反后填充到Checksum字段。

  • 将整个ICMP报文(包括头部和数据部分)按16位(2字节)分组,若总长度为奇数,最后一个字节补 0 凑成16位
  • 将所有分组以二进制形式累加(按无符号整数处理),得到一个32位的中间结果(可能溢出)
  • 将32位结果的高16位与低16位相加(即 cksum = (cksum >> 16) + (cksum & 0xffff))。若相加后仍有进位(即高16位非零),重复此操作,直至高16位为0
  • 将最终结果的低16位按位取反(~cksum),结果存入校验和字段

    下面使用实际中的报文进行举例:
    使用wireshark抓包

提取报文信息:

type:8
code:0
checksum:0x4d58
identifier:1
sequence:3
data:0x6162636465666768696a6b6c6d6e6f7071727374757677616263646566676869

将信息拼接起来,按照报文结构按字节进行拼接,记得要将checksum0

08000000000100036162636465666768696a6b6c6d6e6f7071727374757677616263646566676869

使用python脚本验证

import numpy as np

# 原始数据(16进制字符串)
hex_str = "08000000000100036162636465666768696a6b6c6d6e6f7071727374757677616263646566676869"
data = bytes.fromhex(hex_str)  # 正确转换16进制字符串为字节

# 对齐到偶数长度(16位边界)
if len(data) % 2 != 0:
    data = data + b"\x00"

sum_val = np.uint32(0)

# 按16位字累加(步长=2)
for i in range(0, len(data), 2):
    # 读取16位字(大端序)
    word = (data[i] << 8) + data[i+1]
    sum_val += np.uint32(word)

# 处理进位(RFC 1071标准)
while sum_val >> 16:
    sum_val = (sum_val & 0xffff) + (sum_val >> 16)

# 取反码得到最终校验和
checksum = np.uint16(~sum_val)

print(f"计算过程累加和: 0x{sum_val:04X}")
print(f"最终校验和: 0x{checksum:04X}")

结果和预期一致

在这里插入图片描述

三、ICMP报文发送实现

  1. 定义ICMP报文结构
type ICMP struct {
	   Type        uint8  //消息类型(请求=8,应答=0)
	   Code        uint8  //子类型(回显请求/应答中恒为0)
	   Checksum    uint16 //头部与数据的校验和
	   Identifier  uint16 //进程标识符(区分并发Ping)
	   SequenceNum uint16 //序列号(标识请求顺序)
}
  1. 声明一个序列化函数,将结构体转化为字节流,使用的是大端序存储
func (icmp *ICMP) Serialize() []byte {
		data := make([]byte, 40)
		data[0] = icmp.Type
		data[1] = icmp.Code
	 	binary.BigEndian.PutUint16(data[2:], icmp.Checksum)
	 	binary.BigEndian.PutUint16(data[4:], icmp.Identifier)
	 	binary.BigEndian.PutUint16(data[6:], icmp.SequenceNum)
	 	return data
}
  1. 校验和计算go代码实现,和python代码基本一样:
func checkSum(data []byte) uint16 {
	var sum uint32
	for i := 0; i < len(data)-1; i += 2 {
		sum += uint32(data[i])<<8 | uint32(data[i+1])
	}
	if len(data)%2 == 1 {
		sum += uint32(data[len(data)-1]) << 8
	}
	for (sum >> 16) > 0 {
		sum = (sum & 0xFFFF) + (sum >> 16)
	}
	return ^uint16(sum)
}
  1. 报文初始化
icmp := ICMP{
		Type:        8,
		Code:        0,
		Checksum:    0,
		Identifier:  uint16(os.Getpid() & 0xffff),
		SequenceNum: seq + 1,
}

packet := append(icmp.Serialize(), []byte("hello world")...)//data字段可自定义

//计算校验和
checksum := icmp.checkSum(packet)
binary.BigEndian.PutUint16(packet[2:], checksum)
  1. 建立连接并发送ICMP报文
func ping(dst string) error {
	//建立连接
	conn, err := net.Dial("ip4:icmp", dst)
	if err != nil {
		return fmt.Errorf("创建连接失败:%v", err)
	}
	defer conn.Close()

	icmp := ICMP{
		Type:        8,
		Code:        0,
		Checksum:    0,
		Identifier:  uint16(os.Getpid() & 0xffff),
		SequenceNum: seq + 1,
	}

	packet := append(icmp.Serialize(), []byte("hello world")...)

	//计算校验和
	checksum := icmp.checkSum(packet)
	binary.BigEndian.PutUint16(packet[2:], checksum)

	//发送数据包
	timeTsart := time.Now()
	_, err = conn.Write(packet[:])
	if err != nil {
		return fmt.Errorf("发送数据包失败:%v", err)
	}

	//接收数据包
	_ = conn.SetReadDeadline(time.Now().Add(time.Second * 3))
	buf := make([]byte, 1024)
	n, err := conn.Read(buf[:])
	if err != nil {
		return fmt.Errorf("接收数据包失败:%v", err)
	}

	if n < 20+8 || buf[20] != 0 {
		return fmt.Errorf("接收到错误的数据包")
	}

	//打印结果
	fmt.Printf("Reply from %s: 耗时:%v\n", dst, time.Since(timeTsart))
	return nil
}

func main() {
	dst := os.Args[1]
	err := ping(dst)
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println("Ping Success!")
	}
}
  1. 编译运行查看
go build -o ping.exe ping.go | ./ping.exe www.baidu.com

四、功能扩展

添加-c指定ping次数,--always持续ping。这里使用cobra库进行命令行构建,也方便后续扩展。

var (
	ip string
	n  int
	T  bool
)

var pingCommand = &cobra.Command{
	Use:   "ping",
	Short: "ping命令",
	Run: func(cmd *cobra.Command, args []string) {
		cmd.Flags().Visit(func(f *pflag.Flag) {
			if f.Name == "ip" {
				ip = f.Value.String()
			}
			if f.Name == "count" {
				n, _ = cmd.Flags().GetInt("count")
			}
			if f.Name == "always" {
				T, _ = cmd.Flags().GetBool("always")
			}
		})
		if n != 0 && T {
			fmt.Println("参数错误:不能同时指定-c和-t参数")
			return
		}
		if err := ping(ip); err != nil {
			fmt.Println(err)
		}
		if n != 0 {
			for i := 1; i < n; i++ {
				if err := ping(ip); err != nil {
					fmt.Println(err)
				}
			}
		} else if T {
			for {
				if err := ping(ip); err != nil {
					fmt.Println(err)
				}
				time.Sleep(time.Second * 1)
			}
		}
	},
}

func init() {
	pingCommand.Flags().StringVarP(&ip, "ip", "i", "", "指定目标IP地址")
	pingCommand.Flags().IntVarP(&n, "count", "c", 0, "指定发送的次数")
	pingCommand.Flags().BoolVar(&T, "always", false, "持续发送")
}
func main() {
	_, err := pingCommand.ExecuteC()
	if err != nil {
		fmt.Println(err)
	}
}

结果:
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/22bf39924b6e43418a55993333dcf3a7.png

五、 其他

ICMP协议详细解析请看这里:ICMP协议以及报文讲解

六、完整代码

package main

import (
	"encoding/binary"
	"fmt"
	"net"
	"os"
	"time"

	"github.com/spf13/cobra"
	"github.com/spf13/pflag"
)

var seq uint16 = 0

type ICMP struct {
	Type        uint8  //消息类型(请求=8,应答=0)
	Code        uint8  //子类型(回显请求/应答中恒为0)
	Checksum    uint16 //头部与数据的校验和
	Identifier  uint16 //进程标识符(区分并发Ping)
	SequenceNum uint16 //序列号(标识请求顺序)
}

// Serialize 序列化
func (icmp *ICMP) Serialize() []byte {
	data := make([]byte, 40)

	data[0] = icmp.Type
	data[1] = icmp.Code
	binary.BigEndian.PutUint16(data[2:], icmp.Checksum)
	binary.BigEndian.PutUint16(data[4:], icmp.Identifier)
	binary.BigEndian.PutUint16(data[6:], icmp.SequenceNum)
	return data
}

// 校验和
func (icmp *ICMP) checkSum(data []byte) uint16 {
	var sum uint32
	for i := 0; i < len(data)-1; i += 2 {
		sum += uint32(data[i])<<8 | uint32(data[i+1])
	}
	if len(data)%2 == 1 {
		sum += uint32(data[len(data)-1]) << 8
	}
	for (sum >> 16) > 0 {
		sum = (sum & 0xFFFF) + (sum >> 16)
	}
	return ^uint16(sum)
}

func ping(dst string) error {
	//建立连接
	conn, err := net.Dial("ip4:icmp", dst)
	if err != nil {
		return fmt.Errorf("创建连接失败:%v", err)
	}
	defer conn.Close()

	icmp := ICMP{
		Type:        8,
		Code:        0,
		Checksum:    0,
		Identifier:  uint16(os.Getpid() & 0xffff),
		SequenceNum: seq + 1,
	}

	packet := append(icmp.Serialize(), []byte("hello world")...)

	//计算校验和
	checksum := icmp.checkSum(packet)
	binary.BigEndian.PutUint16(packet[2:], checksum)

	//发送数据包
	timeTsart := time.Now()
	_, err = conn.Write(packet[:])
	if err != nil {
		return fmt.Errorf("发送数据包失败:%v", err)
	}

	//接收数据包
	_ = conn.SetReadDeadline(time.Now().Add(time.Second * 3))
	buf := make([]byte, 1024)
	n, err := conn.Read(buf[:])
	if err != nil {
		return fmt.Errorf("接收数据包失败:%v", err)
	}

	if n < 20+8 || buf[20] != 0 {
		return fmt.Errorf("接收到错误的数据包")
	}

	//打印结果
	fmt.Printf("Reply from %s: 耗时:%v\n", dst, time.Since(timeTsart))
	return nil
}

var (
	ip string
	n  int
	T  bool
)

var pingCommand = &cobra.Command{
	Use:   "ping",
	Short: "ping命令",
	Run: func(cmd *cobra.Command, args []string) {
		cmd.Flags().Visit(func(f *pflag.Flag) {
			if f.Name == "ip" {
				ip = f.Value.String()
			}
			if f.Name == "count" {
				n, _ = cmd.Flags().GetInt("count")
			}
			if f.Name == "always" {
				T, _ = cmd.Flags().GetBool("always")
			}
		})
		if n != 0 && T {
			fmt.Println("参数错误:不能同时指定-c和-t参数")
			return
		}
		if err := ping(ip); err != nil {
			fmt.Println(err)
		}
		if n != 0 {
			for i := 1; i < n; i++ {
				if err := ping(ip); err != nil {
					fmt.Println(err)
				}
			}
		} else if T {
			for {
				if err := ping(ip); err != nil {
					fmt.Println(err)
				}
				time.Sleep(time.Second * 1)
			}
		}
	},
}

func init() {
	pingCommand.Flags().StringVarP(&ip, "ip", "i", "", "指定目标IP地址")
	pingCommand.Flags().IntVarP(&n, "count", "c", 0, "指定发送的次数")
	pingCommand.Flags().BoolVar(&T, "always", false, "持续发送")
}

func main() {
	_, err := pingCommand.ExecuteC()
	if err != nil {
		fmt.Println(err)
	}
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

清心←_←

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值