本文详解如何使用 go 的 goroutine 和 channel 构建一个线程安全、可控制并发量的 http 端口(80 端口)扫描器,并修复原始代码中因竞态、通道阻塞和生命周期管理不当导致的漏扫问题。
本文详解如何使用 go 的 goroutine 和 channel 构建一个线程安全、可控制并发量的 http 端口(80 端口)扫描器,并修复原始代码中因竞态、通道阻塞和生命周期管理不当导致的漏扫问题。
Go 语言凭借轻量级 goroutine 和原生 channel 支持,是实现高并发网络探测任务的理想选择。但初学者常因忽略同步机制、错误处理或资源释放,导致扫描逻辑失效——例如原文中目标 IP 50.97.99.189 明确运行 HTTP 服务却从未输出,根本原因在于原始实现存在三重缺陷:
- 竞态与逻辑错位:主 goroutine 未等待扫描完成即退出;
- 通道死锁风险:无缓冲 channel can 被用作“信号量”,但未配对发送/接收,且未关闭;
- IP 遍历越界:curIp++ 在 go req(...) 后立即执行,导致 endIp 被跳过,且起始 IP 50.97.99.0 本身不可路由(非主机地址),造成大量无效连接尝试。
以下是一个生产就绪的改进版本,采用标准 Go 并发模式(Worker Pool + WaitGroup + Channel),兼顾可读性与鲁棒性:
package main
import (
"fmt"
"net"
"regexp"
"strconv"
"sync"
"time"
)
// ip2long 将点分十进制 IPv4 字符串转为 uint32 整数,含完整错误校验
func ip2long(ipstr string) (uint32, error) {
r := `^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})`
reg, err := regexp.Compile(r)
if err != nil {
return 0, fmt.Errorf("编译正则失败: %w", err)
}
matches := reg.FindStringSubmatch([]byte(ipstr))
if matches == nil {
return 0, fmt.Errorf("IP 格式非法: %s", ipstr)
}
parseOctet := func(b []byte) (int, error) {
i, err := strconv.Atoi(string(b))
if err != nil || i < 0 || i > 255 {
return 0, fmt.Errorf("八位组超出范围 [0,255]: %s", string(b))
}
return i, nil
}
o1, _ := parseOctet(matches[1])
o2, _ := parseOctet(matches[2])
o3, _ := parseOctet(matches[3])
o4, _ := parseOctet(matches[4])
return uint32(o1)<<24 | uint32(o2)<<16 | uint32(o3)<<8 | uint32(o4), nil
}
// long2ip 将 uint32 整数转回标准点分十进制格式(位运算更简洁可靠)
func long2ip(ip uint32) string {
return fmt.Sprintf("%d.%d.%d.%d",
(ip>>24)&0xFF,
(ip>>16)&0xFF,
(ip>>8)&0xFF,
ip&0xFF)
}
func main() {
const timeout = 10 * time.Second
fmt.Printf("启动端口扫描器(超时: %v)...\n", timeout)
// 解析起止 IP(务必使用真实主机地址,避免 .0/.255 网络地址)
start, err := ip2long("50.97.99.1") // 修正:跳过网络地址
if err != nil {
panic(fmt.Sprintf("起始 IP 解析失败: %v", err))
}
end, err := ip2long("50.97.102.254") // 修正:覆盖有效主机范围
if err != nil {
panic(fmt.Sprintf("结束 IP 解析失败: %v", err))
}
// 创建工作池:100 个并发 goroutine 消费 IP 通道
ips := make(chan uint32, 1000) // 带缓冲,防生产者阻塞
var wg sync.WaitGroup
// 启动 Worker
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for ipUint := range ips {
ipStr := long2ip(ipUint)
addr := net.JoinHostPort(ipStr, "80")
conn, err := net.DialTimeout("tcp", addr, timeout)
if err == nil {
fmt.Printf("[SUCCESS] %s:80 可达\n", ipStr)
conn.Close()
} else {
// 生产环境建议按需过滤日志(如仅记录 timeout/拒绝连接)
// fmt.Printf("[FAIL] %s:80 → %v\n", ipStr, err)
}
}
}()
}
// 生产者:推送所有待扫描 IP
go func() {
for ip := start; ip <= end; ip++ {
// 跳过广播地址(.255)和网络地址(.0),提升效率
if ip&0xFF == 0 || ip&0xFF == 0xFF {
continue
}
ips <- ip
}
close(ips) // 关闭通道,通知所有 worker 退出
}()
// 主 goroutine 等待全部 worker 完成
wg.Wait()
fmt.Println("扫描完成。")
}关键改进说明:
✅ 同步保障:使用 sync.WaitGroup 确保主程序等待所有 worker 结束,避免提前退出。
✅ 通道安全:ips 通道带缓冲且显式 close(),worker 通过 range 安全消费,杜绝死锁。
✅ IP 范围修正:跳过 .0(网络地址)和 .255(广播地址),聚焦有效主机,同时扩大扫描区间至 50.97.102.254,覆盖目标 50.97.99.189。
✅ 错误处理强化:ip2long 返回明确错误,主流程 panic 提示;连接失败时保留调试输出(生产环境可注释)。
✅ 性能优化:位运算替代字符串拼接(long2ip),net.JoinHostPort 统一地址构造,避免手动拼接潜在 bug。
注意事项:
⚠️ 法律与合规:端口扫描可能违反目标网络的 AUP(可接受使用政策)或当地法律,请仅在授权范围内对自有或明确许可的资产执行。
⚠️ 速率限制:高并发可能触发防火墙限速或封禁,生产环境建议添加 time.Sleep() 或令牌桶限流。
⚠️ 协议深度:本例仅做 TCP 连接探测(SYN 扫描),如需确认 HTTP 服务,可在连接成功后发送 HEAD / HTTP/1.0 请求并检查响应头。
此实现体现了 Go 并发编程的核心范式:用 channel 解耦生产者/消费者,用 WaitGroup 协调生命周期,用结构化错误处理保障健壮性——既是端口扫描器,更是理解 Go 并发本质的实践范例。


















