Реализовать команду ping с помощью golang | Началось с Luochen

Go

Ping — это инструмент, который часто используется для проверки связи между хостами.Он реализован на основе протокола ICMP.Основной принцип очень прост: локальная машина отправляет ICMP-сообщение на удаленную машину, а удаленный хост отвечает после получение ICMP-сообщения Аналогичное ICMP-сообщение, когда локальная машина получает ответ, она считает, что удаленный хост доступен для подключения, в противном случае он считает хост недостижимым.

Чтобы понять сетевое программирование golang, я использовал команду go для реализации команды ping.Эта статья расскажет, как реализовать команду ping.

Demo

Полный пример кода здесь, можно выполнить напрямую для достижения следующих эффектов (Обратите внимание, что требуются привилегии sudo):

➜  ping git:(master) sudo go run goping.go baidu.com
Ping 111.13.101.208 (baidu.com):
28 bytes from 111.13.101.208: seq=1 time=9ms
28 bytes from 111.13.101.208: seq=2 time=9ms
28 bytes from 111.13.101.208: seq=3 time=10ms
28 bytes from 111.13.101.208: seq=4 time=10ms
28 bytes from 111.13.101.208: seq=5 time=9ms

Как добиться

ICMP-сообщение

Сначала нам нужно определить структуру заголовка ICMP:

type ICMP struct {
	Type        uint8
	Code        uint8
	CheckSum    uint16
	Identifier  uint16
	SequenceNum uint16

вTypeУказывает тип ICMP,CodeОн используется для дальнейшего разделения типа ICMP, ping использует ICMP типа эха, и эти два значения нужно установить на 8 и 0 соответственно.

CheckSumЭто контрольное значение заголовка сообщения для предотвращения ошибок данных при передаче по сети.Это поле будет установлено на 0 для расчета контрольного значения, а затем контрольное значение будет присвоено этому полю после завершения расчета.

ID используется для идентификации ICMP и может быть установлен равным 0.SequenceNumЭто порядковый номер, который можно последовательно накапливать при отправке ICMP-сообщений.

эта статьяСтруктура ICMP описана более подробно.

Основываясь на приведенном выше описании, мы можем реализовать следующую функцию, которая генерирует заголовок ICMP на основе порядкового номера:

func getICMP(seq uint16) ICMP {
	icmp := ICMP{
		Type:        8,
		Code:        0,
		CheckSum:    0,
		Identifier:  0,
		SequenceNum: seq,
	}
	var buffer bytes.Buffer
	binary.Write(&buffer, binary.BigEndian, icmp)
	icmp.CheckSum = CheckSum(buffer.Bytes())
	buffer.Reset()
	return icmp
}

вCheckSum()это функция, используемая для вычисления значения контрольной суммы.Данные, передаваемые по сети, должны иметь порядок байтов от старшего к старшему.

Отправка и получение ICMP-сообщений

Во-первых, мы используемnet.DialIP("ip4:icmp", nil, destAddr)для создания ICMP-сообщения.

Затем мы используем следующий код, чтобы заполнить ICMP-сообщение и отправить его:

binary.Write(&buffer, binary.BigEndian, icmp)
if _, err := conn.Write(buffer.Bytes()); err != nil {
    return err
}

После отправки мы используем следующую команду для получения запроса:

recv := make([]byte, 1024)
receiveCnt, err := conn.Read(recv)

При этом нам также необходимо считать время, прошедшее между отправкой и получением.

Полный код выглядит так:

func sendICMPRequest(icmp ICMP, destAddr *net.IPAddr) error {
	conn, err := net.DialIP("ip4:icmp", nil, destAddr)
	if err != nil {
		fmt.Printf("Fail to connect to remote host: %s\n", err)
		return err
	}
	defer conn.Close()
	var buffer bytes.Buffer
	binary.Write(&buffer, binary.BigEndian, icmp)
	if _, err := conn.Write(buffer.Bytes()); err != nil {
		return err
	}
	tStart := time.Now()
	conn.SetReadDeadline((time.Now().Add(time.Second * 2)))
	recv := make([]byte, 1024)
	receiveCnt, err := conn.Read(recv)
	if err != nil {
		return err
	}
	tEnd := time.Now()
	duration := tEnd.Sub(tStart).Nanoseconds() / 1e6
	fmt.Printf("%d bytes from %s: seq=%d time=%dms\n", receiveCnt, destAddr.String(), icmp.SequenceNum, duration)
	return err
}

Полный код команды ping

путь к файлу на Github

package main
import (
	"bytes"
	"encoding/binary"
	"fmt"
	"net"
	"os"
	"time"
)
type ICMP struct {
	Type        uint8
	Code        uint8
	CheckSum    uint16
	Identifier  uint16
	SequenceNum uint16
}
func usage() {
	msg := `
Need to run as root!
Usage:
	goping host
	Example: ./goping www.baidu.com`
	fmt.Println(msg)
	os.Exit(0)
}
func getICMP(seq uint16) ICMP {
	icmp := ICMP{
		Type:        8,
		Code:        0,
		CheckSum:    0,
		Identifier:  0,
		SequenceNum: seq,
	}
	var buffer bytes.Buffer
	binary.Write(&buffer, binary.BigEndian, icmp)
	icmp.CheckSum = CheckSum(buffer.Bytes())
	buffer.Reset()
	return icmp
}
func sendICMPRequest(icmp ICMP, destAddr *net.IPAddr) error {
	conn, err := net.DialIP("ip4:icmp", nil, destAddr)
	if err != nil {
		fmt.Printf("Fail to connect to remote host: %s\n", err)
		return err
	}
	defer conn.Close()
	var buffer bytes.Buffer
	binary.Write(&buffer, binary.BigEndian, icmp)
	if _, err := conn.Write(buffer.Bytes()); err != nil {
		return err
	}
	tStart := time.Now()
	conn.SetReadDeadline((time.Now().Add(time.Second * 2)))
	recv := make([]byte, 1024)
	receiveCnt, err := conn.Read(recv)
	if err != nil {
		return err
	}
	tEnd := time.Now()
	duration := tEnd.Sub(tStart).Nanoseconds() / 1e6
	fmt.Printf("%d bytes from %s: seq=%d time=%dms\n", receiveCnt, destAddr.String(), icmp.SequenceNum, duration)
	return err
}
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 main() {
	if len(os.Args) < 2 {
		usage()
	}
	host := os.Args[1]
	raddr, err := net.ResolveIPAddr("ip", host)
	if err != nil {
		fmt.Printf("Fail to resolve %s, %s\n", host, err)
		return
	}
	fmt.Printf("Ping %s (%s):\n\n", raddr.String(), host)
	for i := 1; i < 6; i++ {
		if err = sendICMPRequest(getICMP(uint16(i)), raddr); err != nil {
			fmt.Printf("Error: %s\n", err)
		}
		time.Sleep(2 * time.Second)
	}
}

References