一、Ping命令原理介绍
Ping是网络诊断中最基础的工具之一,其核心是ICMP协议(Internet Control Message Protocol),接下来简单解析一下ICMP协议结构,实现一个支持统计功能的Ping工具。
二、ICMP协议介绍及结构
ICMP是IP协议的辅助协议,用于传递控制信息和错误报告。Ping命令依赖其回显请求(Type 8)和回显应答(Type 0)功能。
ICMP协议关键特性
- 类型/代码系统:8/0表示请求,0/0表示应答
- 校验机制:确保报文完整性
- 标识序列:区分并发请求(Identifier+SequenceNum)
- 无连接:无需建立TCP连接的轻量协议
ICMP报文结构详解
| 字段名 | 长度(比特) | 说明 |
|---|---|---|
| Type | 8 | 消息类型(请求=8,应答=0) |
| code | 8 | 子类型(回显请求/应答中恒为0) |
| Checksum | 16 | 头部与数据的校验和 |
| Identifier | 16 | 进程标识符(区分并发Ping) |
| Sequence | 16 | 序列号(标识请求顺序) |
| Data | 可变长度 | 负载(通常为时间戳或填充字节) |
校验和计算是关键步骤:将报文每16位累加,结果取反后填充到Checksum字段。
- 将整个
ICMP报文(包括头部和数据部分)按16位(2字节)分组,若总长度为奇数,最后一个字节补 0 凑成16位 - 将所有分组以二进制形式累加(按无符号整数处理),得到一个32位的中间结果(可能溢出)
- 将32位结果的高16位与低16位相加(即
cksum = (cksum >> 16) + (cksum & 0xffff))。若相加后仍有进位(即高16位非零),重复此操作,直至高16位为0 - 将最终结果的低16位按位取反(
~cksum),结果存入校验和字段
下面使用实际中的报文进行举例:

提取报文信息:
type:8
code:0
checksum:0x4d58
identifier:1
sequence:3
data:0x6162636465666768696a6b6c6d6e6f7071727374757677616263646566676869
将信息拼接起来,按照报文结构按字节进行拼接,记得要将checksum置0
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报文发送实现
- 定义
ICMP报文结构
type ICMP struct {
Type uint8 //消息类型(请求=8,应答=0)
Code uint8 //子类型(回显请求/应答中恒为0)
Checksum uint16 //头部与数据的校验和
Identifier uint16 //进程标识符(区分并发Ping)
SequenceNum uint16 //序列号(标识请求顺序)
}
- 声明一个序列化函数,将结构体转化为字节流,使用的是大端序存储
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
}
- 校验和计算
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)
}
- 报文初始化
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)
- 建立连接并发送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!")
}
}
- 编译运行查看
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)
}
}
结果:

五、 其他
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)
}
}
2370

被折叠的 条评论
为什么被折叠?



