第19节 go 实现 ping

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

Go语言基础篇open in new window

Go语言100篇进阶open in new window


[TOC]

ICMP

ICMPとは?

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

报文头

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

Bits160-167168-175176-183184-191
160TypeCode校验码(checksum)校验码(checksum)
192Rest of HeaderRest of HeaderRest of HeaderRest of Header
TCP/IP协议族的网络层基础(5)——ICMP协议以及ping命令_TLpigff的博客-CSDN博客

ICMP 的格式

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

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

Go语言实现 Ping 操作

步骤

  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

提示

是不是可以和我们之前的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 链接