From 8a038d4df302ad707795a15ddc5f1581037099c5 Mon Sep 17 00:00:00 2001 From: 4566704 <4566704@qq.com> Date: Sun, 20 Sep 2026 12:25:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(nettest):=20=E8=87=AA=20go-hua=20=E8=BF=81?= =?UTF-8?q?=E5=85=A5=E7=BD=91=E7=BB=9C=E8=BF=9E=E9=80=9A=E6=80=A7=E6=8E=A2?= =?UTF-8?q?=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TCP 端口探测 + 原始 ICMP Ping, 持续统计成功率与延迟 - 附测试、examples/nettest 例程与包 README --- examples/nettest/main.go | 42 +++++++++++ nettest/README.md | 31 ++++++++ nettest/nettest.go | 148 +++++++++++++++++++++++++++++++++++++ nettest/nettest_test.go | 102 ++++++++++++++++++++++++++ nettest/ping.go | 153 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 476 insertions(+) create mode 100644 examples/nettest/main.go create mode 100644 nettest/README.md create mode 100644 nettest/nettest.go create mode 100644 nettest/nettest_test.go create mode 100644 nettest/ping.go diff --git a/examples/nettest/main.go b/examples/nettest/main.go new file mode 100644 index 0000000..8183e0b --- /dev/null +++ b/examples/nettest/main.go @@ -0,0 +1,42 @@ +// nettest 包示例:网络连通性测试(port>0 走 TCP 端口探测,port=0 走 ICMP,需管理员权限) +package main + +import ( + "fmt" + "net" + "time" + + "git.zeroonesoft.cn/golib/zogo/nettest" +) + +func main() { + // 起一个本地 TCP 服务作为测试目标 + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + panic(err) + } + go func() { + for { + c, err := l.Accept() + if err != nil { + return + } + c.Close() + } + }() + port := l.Addr().(*net.TCPAddr).Port + + // 每 1 秒探测一次,IsEcho=true 时控制台输出每次结果 + t := nettest.NewNetTest(1, "本机端口", "127.0.0.1", port, 1000, true) + t.Start() + + time.Sleep(3 * time.Second) + t.Stop() + + r := t.GetResult(false) + fmt.Printf("探测目标: %s:%d\n", t.Addr, port) + fmt.Printf("总计: %d 成功: %d 失败: %d 平均: %dms 最小: %dms 最大: %dms\n", + r.TotalCount, r.SuccessCount, r.FailCount, r.Avg, r.Min, r.Max) + + // 运行中可动态更换目标: t.Set("名称", "目标", 端口, 超时毫秒) +} diff --git a/nettest/README.md b/nettest/README.md new file mode 100644 index 0000000..db9ca3e --- /dev/null +++ b/nettest/README.md @@ -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`。 diff --git a/nettest/nettest.go b/nettest/nettest.go new file mode 100644 index 0000000..ce4d632 --- /dev/null +++ b/nettest/nettest.go @@ -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 +} diff --git a/nettest/nettest_test.go b/nettest/nettest_test.go new file mode 100644 index 0000000..589af9a --- /dev/null +++ b/nettest/nettest_test.go @@ -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) + } +} diff --git a/nettest/ping.go b/nettest/ping.go new file mode 100644 index 0000000..281db6f --- /dev/null +++ b/nettest/ping.go @@ -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 +}