模拟Ping程序

 提示:转载请注明原文链接

 本文链接:https://360us.net/article/28.html

ping程序发的是icmp包,这个协议和tcp、udp协议是在同一层的。

包结构如下:

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |     Type      |     Code      |          Checksum             |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |           Identifier          |        Sequence Number        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |     Data ...
   +-+-+-+-+-

最后的Data数据可有可无,前面这些都一定要。

读取回响的时候,读到的数据前20个字节是ip头,后面的其他数据就是回响的icmp包。

ping结果里一般都有现实ttl,这个东西在ip头里面,前20个字节里面第九个字节就是ttl(生存时间)。

最终效果图:

效果图

完整代码如下:

/*
Copyright 360us.net
link: https://360us.net/article/28.html

模拟ping程序
*/

package main

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

//帮助提示
const HELP = "错误,用法如下:\nping [-c number] ip/domain\n\n-c number 指定执行次数,不指定默认4次\nip/domain 指定要ping的ip或者域名"

//icmp包结构
type icmp struct {
	Type       uint8
	Code       uint8
	Checksum   uint16
	Identifier uint16
	Sequence   uint16
}

func main() {
	checkInput()

	var dest string
	var numbers int = 0
	if len(os.Args) == 2 {
		dest = os.Args[1]
	} else if len(os.Args) == 4 {
		dest = os.Args[3]
		n, err := strconv.ParseInt(os.Args[2], 10, 0)
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}
		numbers = int(n)
	}

	if numbers == 0 || numbers < 1 {
		numbers = 4
	}

	var (
		icmpPack icmp
		laddr    net.IPAddr = net.IPAddr{IP: net.ParseIP("0.0.0.0")} //源地址
		raddr, _            = net.ResolveIPAddr("ip", dest)          //目的地址
		response []byte     = make([]byte, 128)                      //保存响应数据
	)

	conn, err := net.DialIP("ip:icmp", &laddr, raddr) //建立ip连接
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	defer conn.Close()

	icmpPack.Type = 8
	icmpPack.Code = 0
	icmpPack.Checksum = 0 //计算Checksum之前置为0
	icmpPack.Identifier = 0
	icmpPack.Sequence = 0
	Data := []byte("123456abcdefghijklmnopqrstuvwxyz") //自定义数据

	var buffer bytes.Buffer //数据缓冲

	//计算校验和
	binary.Write(&buffer, binary.BigEndian, icmpPack) //写入ICMP头
	binary.Write(&buffer, binary.BigEndian, Data)     //写入自定义数据

	icmpPack.Checksum = checkSum(buffer.Bytes())

	buffer.Reset() //清空buffer

	//生成最终发送数据
	binary.Write(&buffer, binary.BigEndian, icmpPack) //写入ICMP头
	binary.Write(&buffer, binary.BigEndian, Data)     //写入自定义数据

	fmt.Printf("\n正 在Ping %s [%s] 具有%d字节的数据:\n", dest, raddr.String(), len(Data))

	var sent, receive int //已发送/已接收的数量统计
	var times []int       //记录每次完整请求花费的时间

	for c := 0; c < numbers; c++ {
		//发包
		_, err = conn.Write(buffer.Bytes())
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}
		sent++

		t_start := time.Now() //请求开始时间

		conn.SetReadDeadline(time.Now().Add(time.Second * 5)) //设置读取超时时间点

		//读取响应,响应的slice里面,前20个字节是ip头,剩下的都是icmp包
		_, err = conn.Read(response)
		if err != nil {
			fmt.Println("请求超时")
			continue
		}

		/*
			计算响应的数据包的校验和,校验和不对丢弃
			1、把首部看成以16位为单位的数字组成,依次进行二进制反码求和,包括校验和字段;
			2、检查计算出的校验和的结果是否为0;
			3、如果等于0,说明被整除,校验和正确。否则,校验和就是错误的,协议栈要抛弃这个数据包。
		*/
		if checkSum(response[20:]) != 0 {
			continue
		}

		t_end := time.Now()                           //请求结束时间
		dur := t_end.Sub(t_start).Nanoseconds() / 1e6 //消耗时间

		receive++
		times = append(times, int(dur))

		fmt.Printf("来自 %s 的回复: 字节=%d 时间 = %dms TTL=%d\n", raddr.String(), len(Data), dur, response[8])

		//每次请求完成暂停一秒
		time.Sleep(time.Second)
	}

	result(raddr.String(), sent, receive, times) //展示最终结果统计信息
	os.Exit(0)
}

/*
求校验和步骤:
(1)把校验和字段置为0;
(2)把需校验的数据看成以16位为单位的数字组成,依次进行二进制反码求和;
(3)把得到的结果存入校验和字段中。
*/
func checkSum(data []byte) uint16 {
	var (
		sum    uint32
		length int = len(data)
		index  int
	)
	for length > 1 {
		sum += uint32(data[index])<<8 + uint32(data[index+1])
		index += 2
		length -= 2
	}
	if length > 0 {
		sum += uint32(data[index])
	}
	sum += (sum >> 16)

	return uint16(^sum)
}

//检查用户输入
func checkInput() {
	if len(os.Args) > 4 {
		fmt.Println(HELP)
		os.Exit(1)
	}
	switch len(os.Args) {
	case 1:
		fmt.Println(HELP)
		os.Exit(1)
	case 3:
		fmt.Println(HELP)
		os.Exit(1)
	case 4:
		_, err := strconv.ParseInt(os.Args[2], 10, 0)
		if (os.Args[1] != "-c") || (err != nil) {
			fmt.Println(HELP)
			os.Exit(1)
		}
	}
}

//结果统计
func result(addr string, sent, receive int, times []int) {
	var (
		format string  = "\n%s 的 Ping 统计信息:\n    数据包:已发送 = %d,已接收 = %d,丢失 = %d (%.1f%% 丢失),\n往返行程的估计时间(以毫秒为单位):\n    最短 = %dms,最长 = %dms,平均 = %.0fms\n"
		min    int     //最短时间
		max    int     //最长时间
		count  int     //花费的总时间
		losted int     //丢包数
		t      int     //成功发送的包总数
		avg    float32 //平均时间
	)
	if len(times) > 0 {
		min, max, count, losted, t = times[0], times[0], 0, sent-receive, len(times)
	} else {
		min, max, count, losted, t = 0, 0, 0, sent-receive, len(times)
	}
	for k, v := range times {
		count += v
		if k == 0 {
			continue
		}
		if min > v {
			min = v
		}
		if max < v {
			max = v
		}
	}
	if t == 0 {
		avg = float32(0)
	} else {
		avg = float32(count) / float32(t)
	}
	fmt.Printf(format, addr, sent, receive, losted, float32(losted)/float32(sent)*100, min, max, avg)
}

本来链接:https://360us.net/article/28.html