使用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("\n正在 Ping %s 具有 %d(%d) 字節的數據:\n", 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\n", raddr.String(), len ,dur)
time.Sleep(time.Second)
}
fmt.Printf("丟包率: %.2f%%\n", 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\n", min_lan, avg_lan, max_lan)
}
func main() {
//if len(os.Args) < 3 {
// fmt.Printf("Param domain |data package Sizeof|trace times\n Ex: ./Ping www.so.com 100 4\n")
// 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)
}