feat(nettest): 自 go-hua 迁入网络连通性探测

- TCP 端口探测 + 原始 ICMP Ping, 持续统计成功率与延迟
- 附测试、examples/nettest 例程与包 README
This commit is contained in:
2026-09-20 12:25:37 +08:00
parent a8577b7b3b
commit 8a038d4df3
5 changed files with 476 additions and 0 deletions
+42
View File
@@ -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("名称", "目标", 端口, 超时毫秒)
}
+31
View File
@@ -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`。
+148
View File
@@ -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
}
+102
View File
@@ -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
View File
@@ -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
}