Ping 是什么?
? ping
是一個計算機網絡工具,通常用于測試網絡連接的可達性和測量往返時間。在大多數操作系統中,ping
命令是一個內置的命令行工具,可以通過命令行終端使用。例如,在 Windows 操作系統中,你可以在命令提示符中運行 ping
命令,而在類 Unix 操作系統(如 Linux 和 macOS)中,你可以在終端中使用 ping
命令。通常,命令的語法是 ping 目標主機或 IP
,然后命令將輸出與目標主機的通信狀態(tài)和 RTT 相關的信息。
Ping 有什么用處?
? Ping
工具主要有以下幾個主要用途:
- 測試主機的可達性:
ping
命令用于檢查另一個主機是否可以在網絡上訪問。它向目標主機發(fā)送一個小的數據包(通常是 ICMP Echo Request),如果目標主機正常工作,它將響應一個回復數據包(通常是 ICMP Echo Reply)。如果沒有響應,那么目標主機可能無法訪問或處于離線狀態(tài)。 - 測量往返時間(RTT) :
ping
命令通常會顯示每次請求和響應之間的時間差,這被稱為往返時間(RTT)。這個值表示了數據從發(fā)送端到接收端的往返延遲,通常以毫秒為單位。測量 RTT 對于評估網絡性能和延遲非常有用。 - 網絡故障排除:
ping
是網絡故障排除的有用工具之一。通過檢查ping
的輸出,網絡管理員可以確定網絡連接是否正常,以及延遲是否在可接受范圍內。如果ping
失敗,管理員可以進一步調查網絡故障的原因。 - 監(jiān)測網絡穩(wěn)定性:
ping
命令還可以用于監(jiān)測網絡的穩(wěn)定性。通過連續(xù)地向目標主機發(fā)送ping
請求,可以了解網絡連接的質量和穩(wěn)定性。如果出現不穩(wěn)定性,管理員可以及時采取措施。
動手實現一個 Ping 工具
? 首先,我們要了解一下 Ping
操作的工作原理:向網絡上的另一個主機系統發(fā)送 ICMP
報文,如果指定系統得到了報文,它將把回復報文傳回給發(fā)送者。
? ICMP 報文由 ICMP 報文頭 和 數據包組成,其報文頭包含 Type、Code、Checksum、ID、SequenceNum 字段。因此,我們需要先在本地主機上定義 ICMP 請求報文結構體:
type ICMP struct {
Type uint8 // 類型
Code uint8 // 代碼
CheckSum uint16 // 校驗和
ID uint16 // ID
SequenceNum uint16 // 序號
}
? 上面只是 ICMP 的報文頭,我們在后面還需要為這個報文構建請求數據。需要注意的是,定義的順序不能亂,因為我們發(fā)送數據包是按字節(jié)發(fā)送的,所以獲取對應的字段的時候,也是按照對應字段的位置去獲取的,如果順序亂了,獲取到的數據就會出錯。
? 在構建數據之前,我們先設置好命令行參數,以獲取對應參數和目標 IP,同時需要定義全局變量,將命令行參數綁定到對應的變量中,方便使用:
var (
helpFlag bool
timeout int64 // 耗時
size int // 大小
count int // 請求次數
)
func GetCommandArgs() {
flag.Int64Var(&timeout, "w", 1000, "請求超時時間")
flag.IntVar(&size, "l", 32, "發(fā)送字節(jié)數")
flag.IntVar(&count, "n", 4, "請求次數")
flag.BoolVar(&helpFlag, "h", false, "顯示幫助信息")
flag.Parse()
}
? 在 main
函數中,啟用命令行參數設置:
func main() {
GetCommandArgs()
}
? 在發(fā)送報文前,我們需要先建立連接,此時需要先獲取目標 IP,這個由命令行參數中獲?。?/p>
// 獲取目標 IP
desIP := os.Args[len(os.Args)-1]
// 構建連接
conn, err := net.DialTimeout("ip:icmp", desIP, time.Duration(timeout)*time.Millisecond)
if err != nil {
log.Println(err.Error())
return
}
defer conn.Close()
// 遠程地址
remoteaddr := conn.RemoteAddr()
? 連接建立后,我們需要根據參數中的發(fā)送次數 count
去發(fā)送對應次數的報文,因此需要用 for
去做:
for i := 0; i < count; i ++ {
...
}
? 同樣,我們在全局變量中添加對應的值:
var (
typ uint8 = 8
code uint8 = 0
)
? 做好前面的準備工作,我們就可以開始構建我們的 ICMP 請求報文了。我們這里以發(fā)送的第幾次作為 ID 和序列號:
icmp := &ICMP{
Type: typ,
Code: code,
CheckSum: uint16(0),
ID: uint16(i),
SequenceNum: uint16(i),
}
? 由于 ICMP 是使用二進制進行傳輸的,所以我們需要將信息用二進制表示:
var buffer bytes.Buffer
binary.Write(&buffer, binary.BigEndian, icmp)
? 然后根據發(fā)送數據的大小 size
構建數據并寫在 ICMP 報文后面:
data := make([]byte, size)
buffer.Write(data)
data = buffer.Bytes()
? 現在,就只差一個校驗和字段了,計算 ICMP(Internet Control Message Protocol)報文的校驗和字段遵循以下步驟:
- 將報文分為 16 位的字(兩個字節(jié))。
- 對所有字進行按位求和(二進制求和),包括數據部分和報文頭。如果有剩余字節(jié)(奇數個字節(jié)),將其附加到最后一個字節(jié)。
- 將溢出的進位位(如果有)加回到結果中。
- 取結果的二進制反碼(按位取反)
? 代碼實現如下:
func checkSum(data []byte) uint16 {
// 第一步:兩兩拼接并求和
length := len(data)
index := 0
var sum uint32
for length > 1 {
// 拼接且求和
sum += uint32(data[index])<<8 + uint32(data[index+1])
length -= 2
index += 2
}
// 奇數情況,還剩下一個,直接求和過去
if length == 1 {
sum += uint32(data[index])
}
// 第二部:高 16 位,低 16 位 相加,直至高 16 位為 0
hi := sum >> 16
for hi != 0 {
sum = hi + uint32(uint16(sum))
hi = sum >> 16
}
// 返回 sum 值 取反
return uint16(^sum)
}
? 接著再將算出來的校驗和放到報文頭對應的位置中去,這里需要計算一下位置。假設我們有以下 ICMP 報文:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Code |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum (2 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Identifier (2 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number (2 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data (variable length) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
? 校驗和屬于報文的第3、4個字節(jié),即 data[2] 和 data[3]。
data[2] = byte(checkSum >> 8)
data[3] = byte(checkSum)
? 最后再設置一下超時時間,就可以將數據 data
寫入連接中了:
// 設置超時時間
conn.SetDeadline(time.Now().Add(time.Duration(timeout) * time.Millisecond))
// 將 data 寫入連接中,
n, err := conn.Write(data)
if err != nil {
log.Println(err)
continue
}
? 發(fā)送完成后,再構建緩沖接收響應包,
buf := make([]byte, 1024)
n, err = conn.Read(buf)
//fmt.Println(data)
if err != nil {
log.Println(err)
continue
}
? 然后我們就可以從響應包中獲取我們需要的數據,比如 IP 地址、TTL等:
? 根據抓到的 ICMP 響應包,可以知道 IP 頭共 20 個字節(jié),源 IP 和 目標 IP 在我們接收的數據包的倒數 8 個字節(jié)里,所以我們可以推算出我們訪問的 IP 地址,就可以構建我們的打印信息了:
fmt.Printf("來自 %d.%d.%d.%d 的回復:字節(jié)=%d 時間=%d TTL=%d\n", buf[12], buf[13], buf[14], buf[15], n-28, t, buf[8])
? 至此,我們 Ping 工具的核心功能就實現了,還有一些統計信息,就不做具體的講解了,感興趣的可以從代碼中看具體的實現。
完整代碼如下:
package main
import (
"bytes"
"encoding/binary"
"flag"
"fmt"
"log"
"math"
"net"
"os"
"time"
)
// tcp 報文前20個是報文頭,后面的才是 ICMP 的內容。
// ICMP:組建 ICMP 首部(8 字節(jié)) + 我們要傳輸的內容
// ICMP 首部:type、code、校驗和、ID、序號,1 1 2 2 2
// 回顯應答:type = 0,code = 0
// 回顯請求:type = 8, code = 0
var (
helpFlag bool
timeout int64 // 耗時
size int // 大小
count int // 請求次數
typ uint8 = 8
code uint8 = 0
SendCnt int // 發(fā)送次數
RecCnt int // 接收次數
MaxTime int64 = math.MinInt64 // 最大耗時
MinTime int64 = math.MaxInt64 // 最短耗時
SumTime int64 // 總計耗時
)
// ICMP 序號不能亂
type ICMP struct {
Type uint8 // 類型
Code uint8 // 代碼
CheckSum uint16 // 校驗和
ID uint16 // ID
SequenceNum uint16 // 序號
}
func main() {
fmt.Println()
log.SetFlags(log.Llongfile)
GetCommandArgs()
// 打印幫助信息
if helpFlag {
displayHelp()
os.Exit(0)
}
// 獲取目標 IP
desIP := os.Args[len(os.Args)-1]
//fmt.Println(desIP)
// 構建連接
conn, err := net.DialTimeout("ip:icmp", desIP, time.Duration(timeout)*time.Millisecond)
if err != nil {
log.Println(err.Error())
return
}
defer conn.Close()
// 遠程地址
remoteaddr := conn.RemoteAddr()
fmt.Printf("正在 Ping %s [%s] 具有 %d 字節(jié)的數據:\n", desIP, remoteaddr, size)
for i := 0; i < count; i++ {
// 構建請求
icmp := &ICMP{
Type: typ,
Code: code,
CheckSum: uint16(0),
ID: uint16(i),
SequenceNum: uint16(i),
}
// 將請求轉為二進制流
var buffer bytes.Buffer
binary.Write(&buffer, binary.BigEndian, icmp)
// 請求的數據
data := make([]byte, size)
// 將請求數據寫到 icmp 報文頭后
buffer.Write(data)
data = buffer.Bytes()
// ICMP 請求簽名(校驗和):相鄰兩位拼接到一起,拼接成兩個字節(jié)的數
checkSum := checkSum(data)
// 簽名賦值到 data 里
data[2] = byte(checkSum >> 8)
data[3] = byte(checkSum)
startTime := time.Now()
// 設置超時時間
conn.SetDeadline(time.Now().Add(time.Duration(timeout) * time.Millisecond))
// 將 data 寫入連接中,
n, err := conn.Write(data)
if err != nil {
log.Println(err)
continue
}
// 發(fā)送數 ++
SendCnt++
// 接收響應
buf := make([]byte, 1024)
n, err = conn.Read(buf)
//fmt.Println(data)
if err != nil {
log.Println(err)
continue
}
// 接受數 ++
RecCnt++
//fmt.Println(n, err) // data:64,ip首部:20,icmp:8個 = 92 個
// 打印信息
t := time.Since(startTime).Milliseconds()
fmt.Printf("來自 %d.%d.%d.%d 的回復:字節(jié)=%d 時間=%d TTL=%d\n", buf[12], buf[13], buf[14], buf[15], n-28, t, buf[8])
MaxTime = Max(MaxTime, t)
MinTime = Min(MinTime, t)
SumTime += t
time.Sleep(time.Second)
}
fmt.Printf("\n%s 的 Ping 統計信息:\n", remoteaddr)
fmt.Printf(" 數據包: 已發(fā)送 = %d,已接收 = %d,丟失 = %d (%.f%% 丟失),\n", SendCnt, RecCnt, count*2-SendCnt-RecCnt, float64(count*2-SendCnt-RecCnt)/float64(count*2)*100)
fmt.Println("往返行程的估計時間(以毫秒為單位):")
fmt.Printf(" 最短 = %d,最長 = %d,平均 = %d\n", MinTime, MaxTime, SumTime/int64(count))
}
// 求校驗和
func checkSum(data []byte) uint16 {
// 第一步:兩兩拼接并求和
length := len(data)
index := 0
var sum uint32
for length > 1 {
// 拼接且求和
sum += uint32(data[index])<<8 + uint32(data[index+1])
length -= 2
index += 2
}
// 奇數情況,還剩下一個,直接求和過去
if length == 1 {
sum += uint32(data[index])
}
// 第二部:高 16 位,低 16 位 相加,直至高 16 位為 0
hi := sum >> 16
for hi != 0 {
sum = hi + uint32(uint16(sum))
hi = sum >> 16
}
// 返回 sum 值 取反
return uint16(^sum)
}
// GetCommandArgs 命令行參數
func GetCommandArgs() {
flag.Int64Var(&timeout, "w", 1000, "請求超時時間")
flag.IntVar(&size, "l", 32, "發(fā)送字節(jié)數")
flag.IntVar(&count, "n", 4, "請求次數")
flag.BoolVar(&helpFlag, "h", false, "顯示幫助信息")
flag.Parse()
}
func Max(a, b int64) int64 {
if a > b {
return a
}
return b
}
func Min(a, b int64) int64 {
if a < b {
return a
}
return b
}
func displayHelp() {
fmt.Println(`選項:
-n count 要發(fā)送的回顯請求數。
-l size 發(fā)送緩沖區(qū)大小。
-w timeout 等待每次回復的超時時間(毫秒)。
-h 幫助選項`)
}
作者:panco68120
鏈接:https://juejin.cn/post/7357142305423933494
來源:稀土掘金
著作權歸作者所有。商業(yè)轉載請聯系作者獲得授權,非商業(yè)轉載請注明出處。