Skip to content
This repository has been archived by the owner on May 9, 2023. It is now read-only.

Latest commit

 

History

History
564 lines (434 loc) · 14.4 KB

File metadata and controls

564 lines (434 loc) · 14.4 KB

第19节 go 实现 ping

❤️💕💕Go语言高级篇章,在此之前建议您先了解基础和进阶篇。Myblog:http://nsddd.top


[TOC]

ICMP

ICMPとは?

  1. ICMP(互联网控制消息协议)是 IP 协议的“错误通知”和“控制消息”
  2. 用于传输 的协议。 检查实现 TCP/IP 的计算机之间的通信状态
  3. 使用。 ICMP 是一种在互联网层(OSI 参考模型的网络层)上运行的协议。

::: tip 推荐 ICMP 的功能与使用

:::

报文头

ICMP报头从IP报头的第160位开始(IP首部20字节)(除非使用了IP报头的可选部分)。

Bits 160-167 168-175 176-183 184-191
160 Type Code 校验码(checksum) 校验码(checksum)
192 Rest of Header Rest of Header Rest of Header Rest of Header

TCP/IP协议族的网络层基础(5)——ICMP协议以及ping命令_TLpigff的博客-CSDN博客

ICMP 的格式

ICMP 消息由四个字段组成:类型、代码、校验和和和数据。

每个字段 英语符号 位数 每个字段的说明
类型 Type 8 bit 包含 ICMP 消息的功能类型值。 值见下表。
代码 Code 8 bit 包含 ICMP 消息的详细功能代码的值。 值见下表。
校验和 Checksum 16 bit 检查是否存在错误。
主题 Data 可变长度 长度因 ICMP 的“类型”而异。

Go语言实现 Ping 操作

::: tip 步骤

  1. ping 操作原理 ICMP
  2. 实现 ping 操作的两个关键点
  3. ping 操作

:::

我们希望可以发送 64 字节的数据 -l 64

flag 的作用

Go语言内置的flag包实现了命令行参数的解析,flag包使得开发命令行工具更为简单。

可以简单的获取命令行参数:

package main

import (
	"fmt"
	"os"
)

//os.Args demo
func main() {
	//os.Args是一个[]string
	if len(os.Args) > 0 {
		for index, arg := range os.Args {
			fmt.Printf("args[%d]=%v\n", index, arg)
		}
	}
}

🚀 编译结果如下:

PS D:\文\最近的\awesome-golang\docs\code\go-super> go run  .\74-main.go wefa     
args[0]=C:\Users\smile\AppData\Local\Temp\go-build1593224315\b001\exe\74-main.exe
args[1]=wefa

::: tip 是不是可以和我们之前的Cobra联合起来了,或许我们可以使用 Cobra 对吧~

:::

lag.Type()

基本格式如下:

flag.Type(flag名, 默认值, 帮助信息)*Type 例如我们要定义姓名、年龄、婚否三个命令行参数,我们可以按如下方式定义:

name := flag.String("name", "张三", "姓名")
age := flag.Int("age", 18, "年龄")
married := flag.Bool("married", false, "婚否")
delay := flag.Duration("delay", 0, "时间间隔")

需要注意的是,此时nameagemarrieddelay均为对应类型的指针。

flag.TypeVar()

基本格式如下: flag.TypeVar(Type指针, flag名, 默认值, 帮助信息) 例如我们要定义姓名、年龄、婚否三个命令行参数,我们可以按如下方式定义:

var name string
var age int
var married bool
var delay time.Duration
flag.StringVar(&name, "name", "张三", "姓名")
flag.IntVar(&age, "age", 18, "年龄")
flag.BoolVar(&married, "married", false, "婚否")
flag.DurationVar(&delay, "delay", 0, "时间间隔")

flag.Parse()

通过以上两种方法定义好命令行flag参数后,需要通过调用flag.Parse()来对命令行参数进行解析。

支持的命令行参数格式有以下几种:

  • -flag xxx (使用空格,一个-符号)
  • --flag xxx (使用空格,两个-符号)
  • -flag=xxx (使用等号,一个-符号)
  • --flag=xxx (使用等号,两个-符号)

其中,布尔类型的参数必须使用等号的方式指定。

Parse解析os.Args[1:]中的命令行标志。必须在定义所有标志之后和在程序访问标志之前调用。

else

flag.Args()  ////返回命令行参数后的其他参数,以[]string类型
flag.NArg()  //返回命令行参数后的其他参数个数
flag.NFlag() //返回使用的命令行参数个数

获取命令行参数

💡简单的一个案例如下:

/*
实现ping操作
*/

package main

import (
	"flag"
	"fmt"
)

var (
	timeout int64
	size    int
	count   int
)

func main() {
	getCommandArgs()
	fmt.Println(timeout, size, count)
}

func getCommandArgs() {
	//获取命令行参数
	flag.Int64Var(&timeout, "t", 1000, "请求超时时间,单位毫秒")
	flag.IntVar(&size, "s", 32, "请求数据包大小,单位字节")
	flag.IntVar(&count, "c", 4, "请求次数")
	flag.Parse() //解析命令行参数
}

🚀 编译结果如下:

PS D:\go-super> go run  .\73-main.go               
1000 32 4
PS D:\go-super> go run  .\73-main.go -t 1234 -s 128 -c 6
1234 128 6

获取请求

注意大小端区别:

/*
实现ping操作
*/

package main

import (
	"bytes"
	"encoding/binary"
	"flag"
	"fmt"
	"net"
	"os"
	"time"
)

type ICMP struct {
	Type     uint8  //消息类型
	Code     uint8  //类型子码
	Checksum uint16 //校验和
	//标识符和序列号
	ID  uint16 //标识符
	Seq uint16 //序列号
}

var (
	timeout int64
	size    int
	count   int
	icmp    *ICMP = &ICMP{
		Type:     8,
		Code:     0,
		Checksum: 3,
		ID:       6,
		Seq:      0,
	}
)

func main() {
	getCommandArgs()

	//目标ip地址
	desIp := os.Args[len(os.Args)-1] //获取最后一个参数

	//构建请求
	conn, err := net.DialTimeout("ip4:icmp", desIp, time.Duration(timeout)*time.Millisecond)
	if err != nil {
		fmt.Println("请求失败")
		return
	}
	defer conn.Close()
	data := make([]byte, size)
	fmt.Println("正在 Ping", desIp, " 具有 32 字节的数据:", "data:", data)

	var buffer bytes.Buffer
	fmt.Printf("大端:")
	binary.Write(&buffer, binary.BigEndian, &icmp) //写入类型
	fmt.Println("icmp:", icmp)

	var buffer2 bytes.Buffer
	fmt.Printf("小端:")
	binary.Write(&buffer2, binary.LittleEndian, &icmp) //写入类型
	/*
		binary.BigEndian: 大端序
		binary.LittleEndian: 小端序
	*/
	fmt.Println("icmp:", icmp)

	for i := 0; i < count; i++ {
		_, err := conn.Write(data)
		if err != nil {
			fmt.Println("发送失败")
			return
		}
		//设置超时时间
		conn.SetReadDeadline(time.Now().Add(time.Duration(timeout) * time.Millisecond))
		_, err = conn.Read(data)
		if err != nil {
			fmt.Println("接收失败")
			return
		}
		fmt.Println("接收成功")
	}
}

func getCommandArgs() {
	//获取命令行参数
	flag.Int64Var(&timeout, "t", 1000, "请求超时时间,单位毫秒")
	flag.IntVar(&size, "s", 32, "请求数据包大小,单位字节")
	flag.IntVar(&count, "c", 4, "请求次数")
	flag.Parse() //解析命令行参数
}

🚀 编译结果如下:

$ go run  73-main.go -t 128 -s 36 -c 6 www.baidu.com
正在 Ping www.baidu.com  具有 32 字节的数据: data: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
大端:icmp: &{8 0 0 1 1}
小端:icmp: &{8 0 0 1 1}
发送失败

校验:

func Checksum(data []byte) uint16 { //定义校验和函数:参数为字节切片,返回值为uint16
	var sum uint32
	for i := 0; i < len(data); i += 2 { //每次取两个字节
		sum += uint32(data[i])<<8 + uint32(data[i+1]) //将每两个字节转换为uint16类型,然后相加
	}
	sum = (sum >> 16) + (sum & 0xffff) //高16位与低16位相加
	sum += sum >> 16                   //将进位的1加到低16位
	return uint16(^sum)                //取反
}

案例

💡简单的一个案例如下:

package main

import (
	"bytes"
	"encoding/binary"
	"flag"
	"fmt"
	"log"
	"math"
	"net"
	"os"
	"time"
)

type ICMP struct {
	Type        uint8
	Code        uint8
	Checksum    uint16
	Identifier  uint16
	SequenceNum uint16
}

var (
	icmp    ICMP
	laddr   = net.IPAddr{IP: net.ParseIP("ip")}
	num     int
	timeout int64
	size    int
	stop    bool
)

func main() {
	ParseArgs()
	args := os.Args

	if len(args) < 2 { // 没有输入主机地址
		Usage()
	}
	desIp := args[len(args)-1]

	conn, err := net.DialTimeout("ip:icmp", desIp, time.Duration(timeout)*time.Millisecond)
	if err != nil {
		log.Fatal(err)
	}

	defer conn.Close()
	//icmp头部填充
	icmp.Type = 8
	icmp.Code = 0
	icmp.Checksum = 0
	icmp.Identifier = 1
	icmp.SequenceNum = 1

	fmt.Printf("\n正在 ping %s 具有 %d 字节的数据:\n", desIp, size)

	var buffer bytes.Buffer
	binary.Write(&buffer, binary.BigEndian, icmp) // 以大端模式写入
	data := make([]byte, size)                    //
	buffer.Write(data)
	data = buffer.Bytes()

	var SuccessTimes int // 成功次数
	var FailTimes int    // 失败次数
	var minTime int = int(math.MaxInt32)
	var maxTime int
	var totalTime int
	for i := 0; i < num; i++ {
		icmp.SequenceNum = uint16(1)
		// 检验和设为0
		data[2] = byte(0)
		data[3] = byte(0)

		data[6] = byte(icmp.SequenceNum >> 8)
		data[7] = byte(icmp.SequenceNum)
		icmp.Checksum = CheckSum(data)
		data[2] = byte(icmp.Checksum >> 8)
		data[3] = byte(icmp.Checksum)

		// 开始时间
		t1 := time.Now()
		conn.SetDeadline(t1.Add(time.Duration(time.Duration(timeout) * time.Millisecond)))
		n, err := conn.Write(data)
		if err != nil {
			log.Fatal(err)
		}
		buf := make([]byte, 65535)
		n, err = conn.Read(buf)
		if err != nil {
			fmt.Println("请求超时。")
			FailTimes++
			continue
		}
		et := int(time.Since(t1) / 1000000)
		if minTime > et {
			minTime = et
		}
		if maxTime < et {
			maxTime = et
		}
		totalTime += et
		fmt.Printf("来自 %s 的回复: 字节=%d 时间=%dms TTL=%d\n", desIp, len(buf[28:n]), et, buf[8])
		SuccessTimes++
		time.Sleep(1 * time.Second)
	}
	fmt.Printf("\n%s 的 Ping 统计信息:\n", desIp)
	fmt.Printf("    数据包: 已发送 = %d,已接收 = %d,丢失 = %d (%.2f%% 丢失),\n", SuccessTimes+FailTimes, SuccessTimes, FailTimes, float64(FailTimes*100)/float64(SuccessTimes+FailTimes))
	if maxTime != 0 && minTime != int(math.MaxInt32) {
		fmt.Printf("往返行程的估计时间(以毫秒为单位):\n")
		fmt.Printf("    最短 = %dms,最长 = %dms,平均 = %dms\n", minTime, maxTime, totalTime/SuccessTimes)
	}
}

func CheckSum(data []byte) uint16 {
	var sum uint32
	var length = len(data)
	var index int
	for length > 1 { // 溢出部分直接去除
		sum += uint32(data[index])<<8 + uint32(data[index+1])
		index += 2
		length -= 2
	}
	if length == 1 {
		sum += uint32(data[index])
	}
	// CheckSum的值是16位,计算是将高16位加低16位,得到的结果进行重复以该方式进行计算,直到高16位为0
	/*
	   sum的最大情况是:ffffffff
	   第一次高16位+低16位:ffff + ffff = 1fffe
	   第二次高16位+低16位:0001 + fffe = ffff
	   即推出一个结论,只要第一次高16位+低16位的结果,再进行之前的计算结果用到高16位+低16位,即可处理溢出情况
	*/
	sum = uint32(sum>>16) + uint32(sum)
	sum = uint32(sum>>16) + uint32(sum)
	return uint16(^sum)
}

func ParseArgs() {
	flag.Int64Var(&timeout, "w", 1500, "等待每次回复的超时时间(毫秒)")
	flag.IntVar(&num, "n", 4, "要发送的请求数")
	flag.IntVar(&size, "l", 32, "要发送缓冲区大小")
	flag.BoolVar(&stop, "t", false, "Ping 指定的主机,直到停止")

	flag.Parse()
}

func Usage() {
	argNum := len(os.Args)
	if argNum < 2 {
		fmt.Print(
			`
用法: ping [-t] [-a] [-n count] [-l size] [-f] [-i TTL] [-v TOS]
            [-r count] [-s count] [[-j host-list] | [-k host-list]]
            [-w timeout] [-R] [-S srcaddr] [-c compartment] [-p]
            [-4] [-6] target_name
选项:
    -t             Ping 指定的主机,直到停止。
                   若要查看统计信息并继续操作,请键入 Ctrl+Break;
                   若要停止,请键入 Ctrl+C。
    -a             将地址解析为主机名。
    -n count       要发送的回显请求数。
    -l size        发送缓冲区大小。
    -f             在数据包中设置“不分段”标记(仅适用于 IPv4)。
    -i TTL         生存时间。
    -v TOS         服务类型(仅适用于 IPv4。该设置已被弃用,
                   对 IP 标头中的服务类型字段没有任何
                   影响)。
    -r count       记录计数跃点的路由(仅适用于 IPv4)。
    -s count       计数跃点的时间戳(仅适用于 IPv4)。
    -j host-list   与主机列表一起使用的松散源路由(仅适用于 IPv4)。
    -k host-list    与主机列表一起使用的严格源路由(仅适用于 IPv4)。
    -w timeout     等待每次回复的超时时间(毫秒)。
    -R             同样使用路由标头测试反向路由(仅适用于 IPv6)。
                   根据 RFC 5095,已弃用此路由标头。
                   如果使用此标头,某些系统可能丢弃
                   回显请求。
    -S srcaddr     要使用的源地址。
    -c compartment 路由隔离舱标识符。
    -p             Ping Hyper-V 网络虚拟化提供程序地址。
    -4             强制使用 IPv4。
    -6             强制使用 IPv6。
`)
	}
}

🚀 编译结果如下:

$ go run  76-main.go -t 128 -s 36 -c 6 www.baidu.com     
正在 ping www.baidu.com 具有 32 字节的数据:
来自 www.baidu.com 的回复: 字节=32 时间=25ms TTL=55
来自 www.baidu.com 的回复: 字节=32 时间=26ms TTL=55
来自 www.baidu.com 的回复: 字节=32 时间=21ms TTL=55

www.baidu.com 的 Ping 统计信息:
    数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0.00% 丢失),
往返行程的估计时间(以毫秒为单位):
    最短 = 21ms,最长 = 29ms,平均 = 25ms

END 链接