应使用 net.DialTimeout 的返回错误判断端口是否开放,err 为 nil 才表示端口响应;超时建议设为 500ms–2s,需控制并发数防端口耗尽,并优先用 127.0.0.1 或 [::1] 避免 DNS 解析歧义。

用 net.DialTimeout 判断端口是否开放,别用 net.Conn.Close 做判断
连接成功即代表端口开放,失败才需要进一步分析;net.Conn 一旦建立就不能靠调用 Close() 来“验证”是否通——它只是释放资源,不反映连通性。
常见错误是:先 dial 成功,再立刻 Close(),然后以为“能关就说明通”,其实只要 TCP 握手完成,Close() 总会成功,哪怕对方服务已崩溃或防火墙拦截了后续数据。
- 必须检查
net.DialTimeout的返回值err:为nil才算端口响应 - 超时时间建议设为
500 * time.Millisecond到2 * time.Second,太短漏报,太长拖慢扫描 - 不要 defer
conn.Close()后还去读写——如果dial失败,conn是nil,defer 会 panic
并发控制不当会导致大量 connect: cannot assign requested address
Linux 默认限制本地可用的临时端口范围(通常是 32768–65535,共约 3.2 万个),每个未关闭的连接都会占用一个源端口。并发 1000 个 dial 请求,若未及时回收连接,很快就会耗尽本地端口,触发该错误。
- 务必用带缓冲的 channel 或
semaphore控制并发数,生产环境建议 ≤ 100 - 即使
dial失败,也要确保 goroutine 尽快退出,避免堆积 - 可临时扩宽端口范围:
sysctl -w net.ipv4.ip_local_port_range="1024 65535",但治标不治本
扫描 localhost 时要注意 DNS 解析和回环地址绑定差异
用 "localhost:22" 和 "127.0.0.1:22" 可能得到不同结果——前者走 DNS 解析(可能被 /etc/hosts 干扰),后者直连 IPv4 回环;某些服务(如 Docker 容器内 SSH)只监听 127.0.0.1,不监听 ::1,更不响应 localhost(若解析成 IPv6)。
立即学习“go语言免费学习笔记(深入)”;
- 测试时优先用
"127.0.0.1:PORT"或"[::1]:PORT"明确指定协议栈 - 避免直接传
"localhost",除非你确认目标服务明确监听该 hostname 对应的所有地址 - 用
net.ResolveIPAddr("ip", "localhost")查看实际解析结果,比凭经验靠谱
简单可运行的扫描器骨架,不含第三方依赖
以下代码片段只依赖标准库,支持单主机多端口、可控并发、基础超时与错误分类:
func scanPort(host string, port int, timeout time.Duration) bool {
addr := fmt.Sprintf("%s:%d", host, port)
conn, err := net.DialTimeout("tcp", addr, timeout)
if err != nil {
// 常见非致命错误,如连接拒绝、超时、无路由,都算“关闭”
// 注意:不要忽略 err == nil 时的 conn == nil(理论上不会,但防御性检查无害)
return false
}
conn.Close()
return true
}
<p>func main() {
host := "127.0.0.1"
ports := []int{22, 80, 443, 8080}
sem := make(chan struct{}, 50) // 并发上限 50</p><pre class="brush:php;toolbar:false;">var wg sync.WaitGroup
for _, p := range ports {
wg.Add(1)
go func(port int) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
if scanPort(host, port, 1*time.Second) {
fmt.Printf("open: %s:%d\n", host, port)
}
}(p)
}
wg.Wait()}
真正难的不是写出来,而是理解每次 dial 背后发生的三次握手、TIME_WAIT 状态、NAT 表项、防火墙规则链顺序——这些不会报错,但会让结果看起来“随机”。










