go语言实现Ping操作

发布于 2023-04-01  802 次阅读


go语言实现Ping操作

前置知识

ICMP

ICMP(Internet Control Message Protocol)Internet控制报文协议。它是TCP/IP协议簇的一个子协议,用于在IP主机路由器之间传递控制消息。控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用。 [1]

ICMP使用IP的基本支持,就像它是一个更高级别的协议,但是,ICMP实际上是IP的一个组成部分,必须由每个IP模块实现

ICMP基本格式

ICMP报文包含在IP数据报中,属于IP的一个用户,IP头部就在ICMP报文的前面,所以一个ICMP报文包括IP头部、ICMP头部和ICMP报文,IP头部的Protocol值为1就说明这是一个ICMP报文,ICMP头部中的类型(Type)域用于说明ICMP报文的作用及格式,此外还有一个代码(Code)域用于详细说明某种ICMP报文的类型,所有数据都在ICMP头部后面。 [3]

ICMP报文格式具体由RFC 777 [4] ,RFC 792 [2] 规范。

简单来说,ICMP报文包括两部分,分别是首部和数据报部分

ICMP

其中首部固定部分长度为20字节

格式为

报文部分结构如下图

ICMP

其中类型的格式如下

表1 ICMP类型


TYPE CODE Description Query Error
0 0 Echo Reply——回显应答(Ping应答) x
3 0 Network Unreachable——网络不可达 x
3 1 Host Unreachable——主机不可达 x
3 2 Protocol Unreachable——协议不可达 x
3 3 Port Unreachable——端口不可达 x
3 4 Fragmentation needed but no frag. bit set——需要进行分片但设置不分片比特 x
3 5 Source routing failed——源站选路失败 x
3 6 Destination network unknown——目的网络未知 x
3 7 Destination host unknown——目的主机未知 x
3 8 Source host isolated (obsolete)——源主机被隔离(作废不用) x
3 9 Destination network administratively prohibited——目的网络被强制禁止 x
3 10 Destination host administratively prohibited——目的主机被强制禁止 x
3 11 Network unreachable for TOS——由于服务类型TOS,网络不可达 x
3 12 Host unreachable for TOS——由于服务类型TOS,主机不可达 x
3 13 Communication administratively prohibited by filtering——由于过滤,通信被强制禁止 x
3 14 Host precedence violation——主机越权 x
3 15 Precedence cutoff in effect——优先中止生效 x
4 0 Source quench——源端被关闭(基本流控制)
5 0 Redirect for network——对网络重定向
5 1 Redirect for host——对主机重定向
5 2 Redirect for TOS and network——对服务类型和网络重定向
5 3 Redirect for TOS and host——对服务类型和主机重定向
8 0 Echo request——回显请求(Ping请求) x
9 0 Router advertisement——路由器通告
10 0 Route solicitation——路由器请求
11 0 TTL equals 0 during transit——传输期间生存时间为0 x
11 1 TTL equals 0 during reassembly——在数据报组装期间生存时间为0 x
12 0 IP header bad (catchall error)——坏的IP首部(包括各种差错) x
12 1 Required options missing——缺少必需的选项 x
13 0 Timestamp request (obsolete)——时间戳请求(作废不用) x
14 Timestamp reply (obsolete)——时间戳应答(作废不用) x
15 0 Information request (obsolete)——信息请求(作废不用) x
16 0 Information reply (obsolete)——信息应答(作废不用) x
17 0 Address mask request——地址掩码请求 x
18 0 Address mask reply——地址掩码应答

回送请求或回答
回送请求和回送回答报文是为诊断网络而设计的,我们使用这对报文来发现网络的问题,回送请求和回送回答报文组合起来就可以确定两个网络设备之间彼此是否能够通信。回收请求和回送回答报文可以直接确定两台主机的IP协议是否能够正常通信,这是因为ICMP报文是被封装在IP数据报中发送的,发送请求的主机能收到回答报文,证明两台主机之间能够使用IP协议进行通信,同时也能证明源主机与目的主机之间的所有路由器的接收、处理、转发功能正常。

其他的icmp请求头部如目的站不可达或数据报超时请自行查阅资料

ICMP校验和签名算法

校验和说明

1.报文内容,相邻两个字节拼接到一起组成一个16bit数,将这些数累加求和

2.若长度为奇数,则将剩余的1个字节,也累加到求和
3.得出总和之后,将和值的高16位与低16位不断求和,直到高16位为0

4.以上三步得出结果后,取度,即为校验和

具体实现

参数定义

首先定义ICMP数据头信息以及发送的一些基本信息

一些参数值请参考上图

var (
   timeout      int64 = 1000 //超时时间
   size         int   = 326  //数据大小
   count        int   = 8    //请求次数
   typ          uint8 = 8    //请求类型,ping请求为8
   code         uint8 = 0    //ping请求为0
   sendCount    int
   successCount int
   failCount    int
   minTs        int64 = math.MaxInt32
   maxTs        int64
   totalTs      int64
   desIp        string
)

结构体定义

这一步的主要作用是构建ICMP报文的前8个字节的数据

/*
*报文首部结构体
 */

type ICMP struct {
   Type        uint8  //请求类型
   Code        uint8  //请求代码
   CheckSum    uint16 //校验和字段
   ID          uint16 //id
   SequenceNum uint16 //序列号
}

一些可选参数定义

类似于其他系统,在ping的时候是可以有参数选择的,所以加入自定义参数的设置同时给一些初始值

func getCommandArgs() {
   flag.Int64Var(&timeout, "w", 2000, "请求超时时长,单位毫秒")
   flag.IntVar(&size, "l", 32, "请求发送缓冲区大小,单位字节")
   flag.IntVar(&count, "n", 4, "请求发送次数")
   flag.StringVar(&desIp, "ip", "www.baidu.com", "请求发送次数")
   flag.Parse()
}

建立连接

getCommandArgs()
conn, err := net.DialTimeout("ip:icmp", desIp, time.Duration(timeout)*time.Millisecond)
if err != nil {
   log.Fatal(err)
   return
}
defer conn.Close() //方法使用完之后执行

校验和算法

func checkSum(data []byte) uint16 {
   length := len(data)
   index := 0
   var sum uint32 = 0
   for length > 1 { //排除奇数
      sum += uint32(data[index])<<8 + uint32(data[index+1]) //拼接相邻的两个字节,所以前一个做高位<<8
      length -= 2
      index += 2
   }
   if length != 0 {
      sum += uint32(data[index]) //如果为奇数就加上最后一个数
   }
   hi16 := sum >> 16 //得出总和之后,将和值的高16位与低16位不断求和,直到高16位为0
   for hi16 != 0 {
      sum = hi16 + uint32(uint16(sum))
      hi16 = sum >> 16
   }
   return uint16(^sum)

}

完整代码

package main

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

var (
   timeout      int64  = 1000          //超时时间
   size         int    = 326           //数据大小
   count        int    = 8             //请求次数
   typ          uint8  = 8             //请求类型,ping请求为8
   code         uint8  = 0             //ping请求为0
   sendCount    int                    //请求次数
   successCount int                    //成功次数
   failCount    int                    //失败次数
   minTs        int64  = math.MaxInt32 //最小时间
   maxTs        int64                  //最大时间
   totalTs      int64                  //总计时间
   desIp        string                 //目标ip
)

/*
*报文首部结构体
 */

type ICMP struct {
   Type        uint8  //请求类型
   Code        uint8  //请求代码
   CheckSum    uint16 //校验和字段
   ID          uint16 //id
   SequenceNum uint16 //序列号
}

func main() {
   getCommandArgs()
   conn, err := net.DialTimeout("ip:icmp", desIp, time.Duration(timeout)*time.Millisecond)
   if err != nil {
      fmt.Println("连接失败")
      return
   }
   defer conn.Close()
   fmt.Printf("正在 Ping %s [%s]  具有 %d 字节的数据:\n", desIp, conn.RemoteAddr(), size)
   for i := 0; i < count; i++ {

      sendCount++
      icmp := &ICMP{
         Type:        typ,
         Code:        code,
         CheckSum:    0,
         ID:          1,
         SequenceNum: 1,
      }
      data := make([]byte, size)
      var buffer bytes.Buffer                       //定义一个缓冲区
      binary.Write(&buffer, binary.BigEndian, icmp) //将icmp首部大端写入
      buffer.Write(data)                            //将数据报内容写入
      data = buffer.Bytes()                         //将缓冲区的内容转到ICMP报文中
      checkSum := checkSum(data)                    //对data进行ICMP校验和
      data[2] = byte(checkSum >> 8)                 //校验和是在ICMP首部的第3,4字节,所以对应data的2,3
      data[3] = byte(checkSum)
      conn, err = net.DialTimeout("ip:icmp", desIp, time.Duration(timeout)*time.Millisecond)
      conn.SetDeadline(time.Now().Add(time.Duration(timeout) * time.Millisecond)) //设置读写超时时间
      t1 := time.Now().UnixNano() / int64(time.Millisecond)                       //获取当前时间
      n, err := conn.Write(data)                                                  //写入参数
      if err != nil {
         failCount++
         //log.Println(err)
         fmt.Println("写入出错")
         conn.Close()
         continue
      }
      buf := make([]byte, 65535)

      n, err = conn.Read(buf) //读取响应

      ts := time.Now().UnixNano()/int64(time.Millisecond) - t1
      if err != nil {
         failCount++
         fmt.Println("请求超时")
         conn.Close()
         time.Sleep(time.Second)
         continue
      }
      successCount++
      if minTs > ts {
         minTs = ts
      }
      if maxTs < ts {
         maxTs = ts
      }

      totalTs += ts
      fmt.Printf("来自 %d.%d.%d.%d 的回复: 字节=%d 时间=%d ms TTL=%d\n", buf[12], buf[13], buf[14], buf[15], n-28, ts, buf[8])
      conn.Close()
      time.Sleep(time.Second)
   }
   fmt.Printf("%s 的 Ping 统计信息:\n    数据包: 已发送 = %d,已接收 = %d,丢失 = %d (%.2f%% 丢失),\n往返行程的估计时间(以毫秒为单位):\n    最短 = %dms,最长 = %dms,平均 = %dms",
      conn.RemoteAddr(), sendCount, successCount, failCount, float64(failCount)/float64(sendCount)*100, minTs, maxTs, totalTs/int64(sendCount))
}

func getCommandArgs() {
   flag.Int64Var(&timeout, "w", 2000, "请求超时时长,单位毫秒")
   flag.IntVar(&size, "l", 32, "请求发送缓冲区大小,单位字节")
   flag.IntVar(&count, "n", 4, "请求发送次数")
   flag.StringVar(&desIp, "ip", "www.baidu.com", "请求发送次数")
   flag.Parse()
}

func checkSum(data []byte) uint16 {
   length := len(data)
   index := 0
   var sum uint32 = 0
   for length > 1 { //排除奇数
      sum += uint32(data[index])<<8 + uint32(data[index+1]) //拼接相邻的两个字节,所以前一个做高位<<8
      length -= 2
      index += 2
   }
   if length != 0 {
      sum += uint32(data[index]) //如果为奇数就加上最后一个数
   }
   hi16 := sum >> 16 //得出总和之后,将和值的高16位与低16位不断求和,直到高16位为0
   for hi16 != 0 {
      sum = hi16 + uint32(uint16(sum))
      hi16 = sum >> 16
   }
   return uint16(^sum)

}

启动方式可由goland等编译器直接启动也可以命令行启动

go run .\main.go -w 1000 -l 326 -n 10 -ip www.baidu.com


HR