feat(nettest): 自 go-hua 迁入网络连通性探测
- TCP 端口探测 + 原始 ICMP Ping, 持续统计成功率与延迟 - 附测试、examples/nettest 例程与包 README
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
# nettest
|
||||
|
||||
网络连通性测试:TCP 端口探测与原始 ICMP Ping,持续统计成功率与延迟,
|
||||
适合做线路质量监控探针。
|
||||
|
||||
> 迁移自 go-hua/nettest,代码保持原样。
|
||||
|
||||
## 用法
|
||||
|
||||
```go
|
||||
import "git.zeroonesoft.cn/golib/zogo/nettest"
|
||||
|
||||
// 单次 Ping(addr 支持 IP 或域名)
|
||||
p := nettest.NewPing("192.168.1.1", 0, 1000) // port=0 表示纯 ICMP
|
||||
ok := p.Test() // true/false
|
||||
|
||||
// 持续探测任务:TCP(echo=false 只测端口) / ICMP,后台 goroutine 运行
|
||||
nt := nettest.NewNetTest(1, "gateway", "192.168.1.1", 80, 1000, false)
|
||||
nt.Start()
|
||||
defer nt.Stop()
|
||||
|
||||
time.Sleep(time.Minute)
|
||||
res := nt.GetResult(true) // TestResult:成功/失败次数、平均延迟等,true 表示取后清零
|
||||
```
|
||||
|
||||
完整可运行例程:[examples/nettest/main.go](../examples/nettest/main.go)
|
||||
|
||||
## 注意
|
||||
|
||||
- 原始 ICMP 在 Windows 直接可用;Linux 下通常需要 root 或 `cap_net_raw`。
|
||||
- `NewPing` 的 timeout 单位毫秒;`Ping.Test` 为一次性探测,无统计,统计用 `NetTest`。
|
||||
@@ -0,0 +1,148 @@
|
||||
// Package nettest 提供网络连通性测试:TCP 端口探测与 ICMP Ping,持续统计成功率与延迟。
|
||||
package nettest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestResult 测试结果统计
|
||||
type TestResult struct {
|
||||
SuccessCount int // 成功计次
|
||||
FailCount int // 失败计次
|
||||
TotalCount int // 总数
|
||||
TotalTime int // 总耗时
|
||||
Avg int // 平均耗时 平均值
|
||||
Min int // 最小耗时 最小值
|
||||
Max int // 最大耗时 最大值
|
||||
}
|
||||
|
||||
// NetTest 网络测试任务:后台协程周期性探测目标并累计统计
|
||||
type NetTest struct {
|
||||
Id int
|
||||
Name string
|
||||
Addr string
|
||||
Port int
|
||||
Timeout int64
|
||||
TestResult
|
||||
Ping *Ping
|
||||
IsStop bool
|
||||
IsEcho bool
|
||||
IsUpdate bool
|
||||
mux sync.Mutex
|
||||
}
|
||||
|
||||
// NewNetTest 创建网络测试任务
|
||||
// port 大于 0 时做 TCP 端口探测,等于 0 时做 ICMP Ping(需管理员权限)
|
||||
// timeout 为超时毫秒数;echo 为 true 时控制台输出每次探测结果
|
||||
func NewNetTest(id int, name string, addr string, port int, timeout int64, echo bool) *NetTest {
|
||||
t := new(NetTest)
|
||||
t.Id = id
|
||||
t.Name = name
|
||||
t.Addr = addr
|
||||
t.Port = port
|
||||
t.Timeout = timeout
|
||||
t.IsEcho = echo
|
||||
t.IsUpdate = false
|
||||
t.Ping = NewPing(addr, port, timeout)
|
||||
return t
|
||||
}
|
||||
|
||||
// Set 运行中动态修改测试目标,下一轮探测生效
|
||||
func (t *NetTest) Set(name string, addr string, port int, timeout int64) {
|
||||
t.mux.Lock()
|
||||
t.Name = name
|
||||
t.Addr = addr
|
||||
t.Port = port
|
||||
t.Timeout = timeout
|
||||
t.IsUpdate = true
|
||||
t.mux.Unlock()
|
||||
t.Ping = NewPing(addr, port, timeout)
|
||||
}
|
||||
|
||||
// Start 启动后台测试协程
|
||||
func (t *NetTest) Start() {
|
||||
go t.process()
|
||||
}
|
||||
|
||||
// Stop 停止测试协程
|
||||
func (t *NetTest) Stop() {
|
||||
t.mux.Lock()
|
||||
t.IsStop = true
|
||||
t.mux.Unlock()
|
||||
}
|
||||
|
||||
// process 测试主循环:探测、累计统计、按剩余时间休眠
|
||||
func (t *NetTest) process() {
|
||||
isLoop := true
|
||||
for isLoop {
|
||||
|
||||
t.mux.Lock()
|
||||
timeout := int(t.Timeout)
|
||||
if t.IsUpdate {
|
||||
t.Ping.Set(t.Addr, t.Port, t.Timeout)
|
||||
}
|
||||
if t.IsStop {
|
||||
isLoop = false
|
||||
}
|
||||
t.mux.Unlock()
|
||||
if !isLoop {
|
||||
break
|
||||
}
|
||||
et := t.Ping.Test()
|
||||
if et > 0 {
|
||||
timeout = timeout - et
|
||||
}
|
||||
if timeout > int(timeout) {
|
||||
timeout = int(timeout)
|
||||
}
|
||||
t.mux.Lock()
|
||||
t.TotalCount++
|
||||
if et >= 0 {
|
||||
t.SuccessCount++
|
||||
} else {
|
||||
t.FailCount++
|
||||
}
|
||||
if et == 0 {
|
||||
t.TotalTime++
|
||||
} else if et > 0 {
|
||||
t.TotalTime += et
|
||||
}
|
||||
if t.SuccessCount > 0 {
|
||||
t.Avg = t.TotalTime / t.SuccessCount
|
||||
}
|
||||
if (et < t.Min || t.Min == 0) && et > 0 {
|
||||
t.Min = et
|
||||
}
|
||||
if et > t.Max {
|
||||
t.Max = et
|
||||
}
|
||||
t.mux.Unlock()
|
||||
|
||||
if timeout > 0 {
|
||||
time.Sleep(time.Duration(timeout) * time.Millisecond)
|
||||
}
|
||||
if t.IsEcho {
|
||||
fmt.Printf("来自 %s 的回复: 时间=%dms\n", t.Addr, et)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetResult 获取测试结果统计,isReset 为 true 时取完清零
|
||||
func (t *NetTest) GetResult(isReset bool) TestResult {
|
||||
t.mux.Lock()
|
||||
r := TestResult{}
|
||||
r = t.TestResult
|
||||
if isReset {
|
||||
t.SuccessCount = 0
|
||||
t.FailCount = 0
|
||||
t.TotalCount = 0
|
||||
t.TotalTime = 0
|
||||
t.Avg = 0
|
||||
t.Min = 0
|
||||
t.Max = 0
|
||||
}
|
||||
t.mux.Unlock()
|
||||
return r
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package nettest
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCheckSum(t *testing.T) {
|
||||
// 单字节直接累加:sum=0x01 → ^0x01 = 0xFFFE
|
||||
if got := CheckSum([]byte{0x01}); got != 0xFFFE {
|
||||
t.Errorf("CheckSum([0x01]) = %#04x, want 0xFFFE", got)
|
||||
}
|
||||
// 双字节:sum=0x0102 → ^0x0102 = 0xFEFD
|
||||
if got := CheckSum([]byte{0x01, 0x02}); got != 0xFEFD {
|
||||
t.Errorf("CheckSum([0x01,0x02]) = %#04x, want 0xFEFD", got)
|
||||
}
|
||||
// 往返性质:带正确校验和的报文再次计算应为 0
|
||||
data := []byte{0x08, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01}
|
||||
cs := CheckSum(data)
|
||||
full := append(data, byte(cs>>8), byte(cs&0xFF))
|
||||
if got := CheckSum(full); got != 0 {
|
||||
t.Errorf("正确校验和报文再计算 = %#04x, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPortCheck(t *testing.T) {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer l.Close()
|
||||
go func() {
|
||||
for {
|
||||
c, err := l.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
port := l.Addr().(*net.TCPAddr).Port
|
||||
p := NewPing("127.0.0.1", port, 1000)
|
||||
if got := p.Test(); got < 0 {
|
||||
t.Errorf("本地端口探测应成功, got %d", got)
|
||||
}
|
||||
|
||||
// 探测不存在的端口应失败
|
||||
if got := NewPing("127.0.0.1", 1, 500).Test(); got >= 0 {
|
||||
t.Errorf("不可达端口应返回 -1, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPingSet(t *testing.T) {
|
||||
p := NewPing("127.0.0.1", 80, 1000)
|
||||
p.Set("localhost", 443, 2000)
|
||||
if p.addr != "localhost" || p.port != 443 || p.timeout != 2000 {
|
||||
t.Errorf("Set 后字段错误: %+v", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetTestFlow(t *testing.T) {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer l.Close()
|
||||
go func() {
|
||||
for {
|
||||
c, err := l.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.Close()
|
||||
}
|
||||
}()
|
||||
port := l.Addr().(*net.TCPAddr).Port
|
||||
|
||||
// timeout 300ms 同时作为探测间隔
|
||||
tt := NewNetTest(1, "test", "127.0.0.1", port, 300, false)
|
||||
tt.Start()
|
||||
time.Sleep(1100 * time.Millisecond)
|
||||
tt.Stop()
|
||||
|
||||
r := tt.GetResult(false)
|
||||
if r.TotalCount == 0 {
|
||||
t.Fatal("应有探测记录")
|
||||
}
|
||||
if r.SuccessCount == 0 {
|
||||
t.Errorf("本地端口应全部成功: %+v", r)
|
||||
}
|
||||
if r.SuccessCount+r.FailCount != r.TotalCount {
|
||||
t.Errorf("成功+失败应等于总数: %+v", r)
|
||||
}
|
||||
|
||||
// GetResult(false) 不清零
|
||||
r2 := tt.GetResult(false)
|
||||
if r2.TotalCount != r.TotalCount {
|
||||
t.Errorf("isReset=false 时统计不应清零: %d != %d", r2.TotalCount, r.TotalCount)
|
||||
}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
package nettest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"log"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ICMP ICMP 报文头
|
||||
type ICMP struct {
|
||||
Type uint8
|
||||
Code uint8
|
||||
Checksum uint16
|
||||
Identifier uint16
|
||||
SequenceNum uint16
|
||||
}
|
||||
|
||||
// Ping 单次探测器:port 为 0 时走 ICMP,否则走 TCP 端口探测
|
||||
type Ping struct {
|
||||
addr string
|
||||
port int
|
||||
timeout int64
|
||||
icmp ICMP
|
||||
size int
|
||||
}
|
||||
|
||||
// NewPing 创建探测器,timeout 为超时毫秒数
|
||||
func NewPing(addr string, port int, timeout int64) *Ping {
|
||||
p := new(Ping)
|
||||
p.addr = addr
|
||||
p.port = port
|
||||
p.timeout = timeout
|
||||
p.size = 32
|
||||
|
||||
// icmp
|
||||
p.icmp.Type = 8
|
||||
p.icmp.Code = 0
|
||||
p.icmp.Checksum = 0
|
||||
p.icmp.Identifier = 1
|
||||
p.icmp.SequenceNum = 0
|
||||
return p
|
||||
}
|
||||
|
||||
// Set 修改探测目标与超时
|
||||
func (p *Ping) Set(addr string, port int, timeout int64) {
|
||||
p.addr = addr
|
||||
p.port = port
|
||||
p.timeout = timeout
|
||||
}
|
||||
|
||||
// Test 执行一次探测,返回耗时毫秒数;失败返回 -1
|
||||
func (p *Ping) Test() int {
|
||||
et := 0
|
||||
if p.port == 0 {
|
||||
et = p.icmpEcho()
|
||||
} else {
|
||||
et = p.portCheck()
|
||||
}
|
||||
return et
|
||||
}
|
||||
|
||||
// icmpEcho 发送 ICMP Echo 并等待回包,返回耗时毫秒数(需管理员权限)
|
||||
func (p *Ping) icmpEcho() int {
|
||||
|
||||
conn, err := net.DialTimeout("ip:icmp", p.addr, time.Duration(p.timeout)*time.Millisecond)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
|
||||
var buffer bytes.Buffer
|
||||
binary.Write(&buffer, binary.BigEndian, p.icmp) // 以大端模式写入
|
||||
data := make([]byte, p.size) //
|
||||
buffer.Write(data)
|
||||
data = buffer.Bytes()
|
||||
|
||||
p.icmp.SequenceNum++
|
||||
// 检验和设为0
|
||||
data[2] = byte(0)
|
||||
data[3] = byte(0)
|
||||
|
||||
data[6] = byte(p.icmp.SequenceNum >> 8)
|
||||
data[7] = byte(p.icmp.SequenceNum)
|
||||
p.icmp.Checksum = CheckSum(data)
|
||||
data[2] = byte(p.icmp.Checksum >> 8)
|
||||
data[3] = byte(p.icmp.Checksum)
|
||||
|
||||
// 开始时间
|
||||
t1 := time.Now()
|
||||
err = conn.SetDeadline(t1.Add(time.Duration(time.Duration(p.timeout) * time.Millisecond)))
|
||||
if err != nil {
|
||||
//log.Println(err)
|
||||
return -1
|
||||
}
|
||||
_, err = conn.Write(data)
|
||||
if err != nil {
|
||||
//log.Println(err)
|
||||
return -1
|
||||
}
|
||||
buf := make([]byte, 65535)
|
||||
_, err = conn.Read(buf)
|
||||
if err != nil {
|
||||
//fmt.Println("请求超时。")
|
||||
return -1
|
||||
}
|
||||
et := int(time.Since(t1) / 1000000)
|
||||
return et
|
||||
}
|
||||
|
||||
// CheckSum 计算 ICMP 校验和
|
||||
func CheckSum(data []byte) uint16 {
|
||||
var sum uint32
|
||||
var length = len(data)
|
||||
var index int
|
||||
for length > 1 { // 溢出部分直接去除
|
||||
sum += uint32(data[index])<<8 + uint32(data[index+1])
|
||||
index += 2
|
||||
length -= 2
|
||||
}
|
||||
if length == 1 {
|
||||
sum += uint32(data[index])
|
||||
}
|
||||
// CheckSum的值是16位,计算是将高16位加低16位,得到的结果进行重复以该方式进行计算,直到高16位为0
|
||||
/*
|
||||
sum的最大情况是:ffffffff
|
||||
第一次高16位+低16位:ffff + ffff = 1fffe
|
||||
第二次高16位+低16位:0001 + fffe = ffff
|
||||
即推出一个结论,只要第一次高16位+低16位的结果,再进行之前的计算结果用到高16位+低16位,即可处理溢出情况
|
||||
*/
|
||||
sum = uint32(sum>>16) + uint32(sum)
|
||||
sum = uint32(sum>>16) + uint32(sum)
|
||||
return uint16(^sum)
|
||||
}
|
||||
|
||||
// portCheck TCP 端口探测,返回连接耗时毫秒数;失败返回 -1
|
||||
func (p *Ping) portCheck() int {
|
||||
// 开始时间
|
||||
t1 := time.Now()
|
||||
conn, err := net.DialTimeout("tcp", p.addr+":"+strconv.Itoa(p.port), time.Duration(p.timeout)*time.Millisecond)
|
||||
et := int(time.Since(t1) / 1000000)
|
||||
if err != nil {
|
||||
//log.Println(err)
|
||||
//fmt.Println("请求超时。")
|
||||
return -1
|
||||
}
|
||||
defer conn.Close()
|
||||
return et
|
||||
}
|
||||
Reference in New Issue
Block a user