標籤:style blog http 使用 os strong 資料 for
引言
關於各種語言實現Ping已經是大家喜聞樂見的事情了,網路上利用Golang實現Ping已經有比較詳細的程式碼範例,但大多是僅僅是實現了Request過程,而對Response的回顯內容並沒有做接收。而Ping程式不僅僅是發送一個ICMP,更重要的是如何接收並進行統計。
下面是網路上幾篇關於Ping的實現代碼:
https://github.com/paulstuart/ping/blob/master/ping.go
http://blog.csdn.net/gophers/article/details/21481447
http://blog.csdn.net/laputa73/article/details/17226337
本文借鑒了第二個連結裡面的部分代碼。
準備
-
安裝最新的Go
由於Google被牆的原因,如果沒有VPN的話,就到這裡下載:
http://www.golangtc.com/download
-
使用任意文字編輯器,或者LiteIDE會比較方便編譯和調試,下面是LiteIDE的
https://github.com/visualfc/liteide
編碼
要用到的package:
import ("bytes""container/list""encoding/binary""fmt""net""os""time")
-
使用Golang提供的net包中的相關函數可以快速構造一個IP包並自訂其中一些關鍵參數,而不需要再自己手動填充IP報文。
-
使用encoding/binary包可以輕鬆擷取結構體struct的記憶體資料並且可以規定位元組序(這裡要用網路位元組序BigEndian),而不需要自己去轉換位元組序。之前的一片文中使用boost,還要自己去實現轉換過程,詳見:關於蹭網檢查的原理及實現
-
使用container/list包,方便進行結果統計
-
使用time包實現耗時和逾時處理
ICMP報文struct:
type ICMP struct {Type uint8Code uint8Checksum uint16Identifier uint16SequenceNum uint16}
Usage提示:
arg_num := len(os.Args)if arg_num < 2 {fmt.Print("Please runAs [super user] in [terminal].\n","Usage:\n","\tgoping url\n","\texample: goping www.baidu.com",)time.Sleep(5e9)return}
注意這個ping程式,包括之前的ARP程式都必須使用系統最高許可權執行,所以這裡先給出提示,使用time.Sleep(5e9),暫停5秒,是為了使雙擊執行者看到提示,避免控制台一閃而過。
關鍵net對象的建立和初始化:
var (icmp ICMPladdr = net.IPAddr{IP: net.ParseIP("0.0.0.0")}raddr, _ = net.ResolveIPAddr("ip", os.Args[1]))conn, err := net.DialIP("ip4:icmp", &laddr, raddr)if err != nil {fmt.Println(err.Error())return}defer conn.Close()
net.DialIP表示產生一個IP報文,版本號碼是v4,協議是ICMP(這裡字串ip4:icmp會把IP報文的協議欄位設為1表示ICMP協議),
源地址laddr可以是0.0.0.0也可以是自己的ip,這個並不影響ICMP的工作。
目的地址raddr是一個URL,這裡使用Resolve進行DNS解析,注意傳回值是一個指標,所以下面的DialIP方法中參數表示沒有取地址符。
這樣一個完整的IP報文就裝配好了,我們並沒有去操心IP中的其他一些欄位,Go已經為我們處理好了。
通過返回的conn *net.IPConn對象可以進行後續操作。
defer conn.Close() 表示該函數將在Return時被執行,確保不會忘記關閉。
下面需要構造ICMP報文了:
icmp.Type = 8icmp.Code = 0icmp.Checksum = 0icmp.Identifier = 0icmp.SequenceNum = 0var buffer bytes.Bufferbinary.Write(&buffer, binary.BigEndian, icmp)icmp.Checksum = CheckSum(buffer.Bytes())buffer.Reset()binary.Write(&buffer, binary.BigEndian, icmp)
仍然非常簡單,利用binary可以把一個結構體資料按照指定的位元組序讀到緩衝區裡面,計算校正和後,再讀進去。
檢驗和演算法參考上面給出的URL中的實現:
func CheckSum(data []byte) uint16 {var (sum uint32length int = len(data)index int)for length > 1 {sum += uint32(data[index])<<8 + uint32(data[index+1])index += 2length -= 2}if length > 0 {sum += uint32(data[index])}sum += (sum >> 16)return uint16(^sum)}
下面是Ping的Request過程,這裡仿照Windows的ping,預設只進行4次:
fmt.Printf("\n正在 Ping %s 具有 0 位元組的資料:\n", raddr.String())recv := make([]byte, 1024)statistic := list.New()sended_packets := 0for i := 4; i > 0; i-- {if _, err := conn.Write(buffer.Bytes()); err != nil {fmt.Println(err.Error())return}sended_packets++t_start := time.Now()conn.SetReadDeadline((time.Now().Add(time.Second * 5)))_, err := conn.Read(recv)if err != nil {fmt.Println("請求逾時")continue}t_end := time.Now()dur := t_end.Sub(t_start).Nanoseconds() / 1e6fmt.Printf("來自 %s 的回複: 時間 = %dms\n", raddr.String(), dur)statistic.PushBack(dur)//for i := 0; i < recvsize; i++ {//if i%16 == 0 {//fmt.Println("")//}//fmt.Printf("%.2x ", recv[i])//}//fmt.Println("")}
"具有0位元組的資料"表示ICMP報文中沒有資料欄位,這和Windows裡面32位元組的資料的略有不同。
conn.Write方法執行之後也就發送了一條ICMP請求,同時進行計時和計次。
conn.SetReadDeadline可以在未收到資料的指定時間內停止Read等待,並返回錯誤err,然後判定請求逾時。否則,收到回應後,計算來回所用時間,並放入一個list方便後續統計。
注釋部分內容是我在探索返回資料時的代碼,讀者可以試試看Read到的資料是哪個資料包的?
統計工作將在迴圈結束時進行,這裡使用了defer其實是希望按了Ctrl+C之後能return執行,但是控制台確實不給力,直接給殺掉了。。
defer func() {fmt.Println("")//資訊統計var min, max, sum int64if statistic.Len() == 0 {min, max, sum = 0, 0, 0} else {min, max, sum = statistic.Front().Value.(int64), statistic.Front().Value.(int64), int64(0)}for v := statistic.Front(); v != nil; v = v.Next() {val := v.Value.(int64)switch {case val < min:min = valcase val > max:max = val}sum = sum + val}recved, losted := statistic.Len(), sended_packets-statistic.Len()fmt.Printf("%s 的 Ping 統計資訊:\n 資料包:已發送 = %d,已接收 = %d,丟失 = %d (%.1f%% 丟失),\n往返行程的估計時間(以毫秒為單位):\n 最短 = %dms,最長 = %dms,平均 = %.0fms\n",raddr.String(),sended_packets, recved, losted, float32(losted)/float32(sended_packets)*100,min, max, float32(sum)/float32(recved),)}()
統計過程注意類型的轉換和格式化就行了。
全部代碼就這些,執行結果大概是這個樣子的:
注意每次Ping後都沒有"休息",不像Windows或者Linux的會停頓幾秒再Ping下一輪。
收尾
Golang實現整個Ping比我想象中的還要簡單很多,靜態編譯速度是十分快速,相比C而言,你需要更多得瞭解底層,甚至要從鏈路層開始,你需要寫更多更複雜的代碼來完成相同的工作,但究其根本,C語言仍然是鼻祖,功不可沒,很多原理和思想都要繼承和發展,這一點Golang做的很好。