Skip to main content
今日目标:理论学得再多,不如写个工具操练一下。今天我们将利用 Go 的并发特性,编写一个多线程端口扫描器。你会亲眼看到它比 Python 单线程版本快 100 倍以上,并在此过程中掌握 “Worker Pool” (工作池) 这一工业级并发模式。

学习内容 (30 mins)

在开始写代码前,先搞懂我们要解决的核心问题和设计思路。
如何判断一个端口是否开放?
  • 探测逻辑: 我们使用 net.DialTimeout("tcp", "host:port", timeout) 尝试建立 TCP 连接。
  • 结果判断:
    • 如果不报错 (err == nil):说明三次握手成功,端口是通的。
    • 如果报错(超时或拒绝):说明端口关闭或被防火墙拦截。
  • 为什么一定要 Timeout: 默认的 TCP 连接超时时间可能长达几分钟。如果不设置超时,扫描一个不可达的 IP 会把你卡死很久。
为什么不直接启动 1024 个协程?虽然 Goroutine 很轻,但如果直接瞬间并发 1024 个网络连接:
  1. 可能会触发操作系统的 “Too many open files” 限制(文件句柄耗尽)。
  2. 可能会被防火墙判定为 DDoS 攻击 瞬间封锁你的 IP。
工业级解决方案:Worker Pool
  1. Job Queue: 创建一个 jobs 通道,里面放着要扫描的端口号(如 1-1024)。
  2. Workers: 启动固定数量(如 100 个)的 Worker 协程。
  3. 流程: 每个 Worker 抢到一个端口 -> 扫完 -> 再抢下一个。这样并发数永远稳定在 100。

代码任务 (90 mins)

我们将通过两个步骤,从基础函数到完整并发工具。
1

核心:基础连通性检查函数

这是所有网络工具的基石。先写一个独立的函数,专注于检测单个端口。
代码解释
  • net.DialTimeout("tcp", address, timeout):尝试建立 TCP 连接,带超时
  • err == nil:连接成功,端口开放
  • conn.Close():必须关闭连接,防止资源泄漏
  • 超时设置:1 秒超时,防止长时间等待
网络编程的设计哲学
  • 总是设置超时:网络操作可能永远不返回,必须设置超时
  • 及时释放资源:连接、文件等资源必须及时关闭
  • 错误处理:网络操作可能失败,必须处理所有错误情况
验证方法: 你可以写一个简单的 main 调用一下 CheckPort("scanme.nmap.org", 80),应该返回 true验证步骤
  1. 测试开放端口:CheckPort("scanme.nmap.org", 80) 应该返回 true
  2. 测试关闭端口:CheckPort("scanme.nmap.org", 9999) 应该返回 false
  3. 测试超时:尝试连接一个不存在的 IP,观察超时行为
2

实战:完整的并发扫描器

使用 Worker Pool 机制实现多线程扫描。这个代码可以直接作为面试作品。
代码解释
  • Worker Pool 模式:固定数量的 Worker,通过 Channel 分发任务
  • make(chan int, 100):带缓冲的通道,可以暂存 100 个任务
  • for port := range jobs:Worker 从通道中取任务,直到通道关闭
  • close(jobs):关闭通道,通知 Worker 没有更多任务
Worker Pool 的设计哲学
  • 控制并发数:避免创建过多 Goroutine,防止资源耗尽
  • 任务队列:通过 Channel 实现任务队列,解耦生产者和消费者
  • 优雅关闭:通过关闭 Channel 通知 Worker 停止工作
验证步骤
  1. 运行程序:go run 25_scanner.go
  2. 你应该能看到程序在几秒钟内完成扫描,并列出 80, 22 等开放端口
  3. 观察 Worker 的并发执行,理解 Worker Pool 的工作方式
  4. 尝试修改 Worker 数量,观察性能变化
常见错误
  • 句柄泄漏:很多新手在 net.Dial 成功后忘了写 conn.Close()。当并发量大时,你的程序会因为 Socket 没释放而崩溃。
  • 死锁:如果你在主线程里塞任务 jobs <- i,但是没有启动 Worker 或者 Worker 处理不过来导致 jobs 满了,主线程就会卡死。建议把任务生成也放到 go func() 里。

拓展任务 (30 mins)

命令行参数化

任务:使用 flag 包,让用户可以指定 IP 和并发数。提示
  • host := flag.String("host", "127.0.0.1", "Target IP")
  • threads := flag.Int("t", 50, "Number of threads")
  • 运行:./scanner -host 8.8.8.8 -t 100

更详细的探测

任务:只知道端口开放还不够。挑战:对于开放的端口,尝试读取它返回的前 1KB 数据(Banner Grabbing),看看能不能识别出它是 SSH 还是 HTTP 服务器。

今日产出物

  • ~/projects/learn-go/25_scanner.go - 高性能端口扫描器

参考代码

查看参考代码

在 GitHub 查看完整的示例代码

在线运行

使用 Replit 运行 (网络可能受限)

实际应用场景

性能大比拼

  • Python (单线程): 扫描 1000 个端口约需 5 分钟 (300秒)。
  • Go (50 并发 Worker): 扫描 1000 个端口约需 3 秒
  • 这就是 100 倍 的性能差距,也是为什么 Go 是开发网络工具的首选。

安全合规提示

  • 不要扫公网 IP: 除非是你自己的服务器。扫描他人 IP 在很多国家是违法的。
  • 内网巡检: 这个工具非常适合用于企业内网巡检,快速发现有哪些机器违规开放了高危端口(如 3389, 23)。
与 Day 26 的关联:今天我们解决了 网络 IO 并发。明天我们将挑战 本地磁盘 IO 并发,编写一个能瞬间分析 GB 级日志的统计工具。

回到目录

查看完整进度

下一天: 工具开发 II

Day 26 | Go 实战工具开发 (二)