使用golang 实现一个Ping程序
基本原理
ping 程序的基本原理
首先呢,ping用到的协议是网络层的ICMP协议,发送/接收的是ICMP报文,最终的形式还是以一个IP报文在网络中传送。
ping命令主要基于ICMP(Internet Control Message Protocol)实现,它包含了两部分:客户端、服务器。
客户端 : 向服务端发送ICMP回显请求报文
服务端 : 向客户端返回ICMP回显响应请求报文
ICMP的报文通用格式
- 类型:1个字节。8表示回显请求报文,0表示回显响应报文。
- 代码:1个字节。回显请求报文、回显响应报文 时均为0。
- 校验和:2个字节。非重点,略过。
- 标识符:2个字节。发送ICMP报文的客户端进程的id,服务端会回传给客户端。因为同一个客户端可能同时运行多个ping程序,这样客户端收到回西显报文,可以知道是响应给哪个客户端进程的。
- 序列号:2个字节。从0开始,客户端每次发送新的回显请求时+1。服务端原样会传。
- 数据:6个字节。客户端记录回显请求的发送时间,服务端记录回西显响应的发送时间
分析:
ping 命令在执行后显示出被测试系统主机名,和相应的IP地址、返回给当前主机的ICMP报文顺序号、ttl生存时间和往返时间rtt(单位是毫秒)
要想真正了解ping 命令的实现原理,首先要了解ping命令所以使用到的TCP/IP协议,ICMP(Internet Control Message,网际控制报文协议)是为网关和目标主机而提供的一种差错控制机制,使他们在遇到时尽可能的包错误报告发送给源发方。ICMP协议是IP层的一个协议,但是由于差错报告在发送报文给报文源发方时可能也要经过若干子网,因此涉及到路由选择等问题,所以ICMP报文需要通过IP协议来发送。ICMP数据包的数据发送前需要进性两级封装
1. 首先添加ICMP报头形成ICMP报文
2. 在添加IP报头形成IP数据报
由于IP层协议是一种点对点的协议,而非端对端的协议,提供无连接的数据报服务,没有端对端的概念,因此很少使用bind() 和 connect() 函数,若有使用也只是用于设置IP地址。
我们在go中定义 ICMP 的报文格式如下结构
// 定义 ICMP 报文
type ICMP struct {
Type uint8 类型
Code uint8 代码
Checksum uint16 校验和
Identifier uint16 标识符
SequenceNum uint16 序列号
}
实现代码如下
package main
import (
"bytes"
"encoding/binary"
"fmt"
"net"
"time"
)
const (
MAX_PG = 2000
)
// 封装 icmp 报头
type ICMP struct {
Type uint8
Code uint8
Checksum uint16
Identifier uint16
SequenceNum uint16
}
var (
originBytes []byte
)
func init() {
originBytes = make([]byte, MAX_PG)
}
func CheckSum(data []byte) (rt 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]) << 8
}
rt = uint16(sum) + uint16(sum>>16)
return ^rt
}
func Ping(domain string, PS, Count int) {
var (
icmp ICMP
laddr = net.IPAddr{IP: net.ParseIP("0.0.0.0")} // 得到本机的IP地址结构
raddr, _ = net.ResolveIPAddr("ip", domain) // 解析域名得到 IP 地址结构
max_lan, min_lan, avg_lan float64
)
// 返回一个 ip socket
conn, err := net.DialIP("ip4:icmp", &laddr, raddr)
if err != nil {
fmt.Println(err.Error())
return
}
defer conn.Close()
// 初始化 icmp 报文
icmp = ICMP{8, 0, 0, 0, 0}
var buffer bytes.Buffer
binary.Write(&buffer, binary.BigEndian, icmp)
//fmt.Println(buffer.Bytes())
binary.Write(&buffer, binary.BigEndian, originBytes[0:PS])
b := buffer.Bytes()
binary.BigEndian.PutUint16(b[2:], CheckSum(b))
//fmt.Println(b)
fmt.Printf("
正在 Ping %s 具有 %d(%d) 字节的数据:
", raddr.String(), PS, PS+28)
recv := make([]byte, 1024)
ret_list := []float64{}
dropPack := 0.0 /*统计丢包的次数,用于计算丢包率*/
max_lan = 3000.0
min_lan = 0.0
avg_lan = 0.0
for i := Count; i > 0; i-- {
/*
向目标地址发送二进制报文包
如果发送失败就丢包 ++
*/
if _, err := conn.Write(buffer.Bytes()); err != nil {
dropPack++
time.Sleep(time.Second)
continue
}
// 否则记录当前得时间
t_start := time.Now()
conn.SetReadDeadline((time.Now().Add(time.Second * 3)))
len, err := conn.Read(recv)
/*
查目标地址是否返回失败
如果返回失败则丢包 ++
*/
if err != nil {
dropPack++
time.Sleep(time.Second)
continue
}
t_end := time.Now()
dur := float64(t_end.Sub(t_start).Nanoseconds()) / 1e6
ret_list = append(ret_list, dur)
if dur < max_lan {
max_lan = dur
}
if dur > min_lan {
min_lan = dur
}
fmt.Printf("来自 %s 的回复: 大小 = %d byte 时间 = %.3fms
", raddr.String(), len ,dur)
time.Sleep(time.Second)
}
fmt.Printf("丢包率: %.2f%%
", dropPack/float64(Count)*100)
if len(ret_list) == 0 {
avg_lan = 3000.0
} else {
sum := 0.0
for _, n := range ret_list {
sum += n
}
avg_lan = sum / float64(len(ret_list))
}
fmt.Printf("rtt 最短 = %.3fms 平均 = %.3fms 最长 = %.3fms
", min_lan, avg_lan, max_lan)
}
func main() {
//if len(os.Args) < 3 {
// fmt.Printf("Param domain |data package Sizeof|trace times
Ex: ./Ping www.so.com 100 4
")
// os.Exit(1)
//}
//PS, err := strconv.Atoi(os.Args[2])
//if err != nil {
// fmt.Println("you need input correct PackageSizeof(complete int)")
// os.Exit(1)
//}
//Count, err := strconv.Atoi(os.Args[3])
//if err != nil {
// fmt.Println("you need input correct Counts")
// os.Exit(1)
//}
Ping("www.baidu.com", 48, 5)
}