> ## Documentation Index
> Fetch the complete documentation index at: https://docs.akria.net/llms.txt
> Use this file to discover all available pages before exploring further.

# Day 25 | Go 实战工具开发 (一)

> 实战：编写一个高性能并发端口扫描器，体验 Goroutine 的威力

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

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="book" size={26} color="#059669" /> 学习内容 (30 mins)</span>

在开始写代码前，先搞懂我们要解决的核心问题和设计思路。

<AccordionGroup>
  <Accordion title="核心原理：net.DialTimeout (15 mins)" icon="plug">
    **如何判断一个端口是否开放？**

    * **探测逻辑**: 我们使用 `net.DialTimeout("tcp", "host:port", timeout)` 尝试建立 TCP 连接。
    * **结果判断**:
      * 如果不报错 (`err == nil`)：说明三次握手成功，端口是通的。
      * 如果报错（超时或拒绝）：说明端口关闭或被防火墙拦截。
    * **为什么一定要 Timeout**: 默认的 TCP 连接超时时间可能长达几分钟。如果不设置超时，扫描一个不可达的 IP 会把你卡死很久。
  </Accordion>

  <Accordion title="并发架构：Worker Pool (工作池) (15 mins)" icon="gears">
    **为什么不直接启动 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。
  </Accordion>
</AccordionGroup>

***

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="code" size={26} color="#059669" /> 代码任务 (90 mins)</span>

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

<Steps>
  <Step title="核心：基础连通性检查函数">
    这是所有网络工具的基石。先写一个独立的函数，专注于检测单个端口。

    <CodeGroup>
      ```go scan_core.go theme={null}
      package main

      import (
          "fmt"
          "net"
          "time"
      )

      // CheckPort 探测单个端口
      // 参数: host (IP或域名), port (端口号)
      // 返回: bool (true 表示开放, false 表示关闭)
      func CheckPort(host string, port int) bool {
          address := fmt.Sprintf("%s:%d", host, port)
          
          // 1秒超时，这对局域网或内网足够了。公网可能需要设大一点。
          conn, err := net.DialTimeout("tcp", address, 1*time.Second)
          
          if err != nil {
              // 连接失败（超时或 Refused）
              return false
          }
          
          // 重要：一旦连接成功，必须手动关闭，否则会耗尽系统句柄
          conn.Close()
          return true
      }
      ```
    </CodeGroup>

    **代码解释**：

    * **`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，观察超时行为
  </Step>

  <Step title="实战：完整的并发扫描器">
    使用 Worker Pool 机制实现多线程扫描。这个代码可以直接作为面试作品。

    <CodeGroup>
      ```go 25_scanner.go theme={null}
      package main

      import (
          "fmt"
          "sort"
          "sync"
          "time"
      )

      // 把 CheckPort 函数复制到这里，或者放在同一个包下

      func main() {
          host := "scanme.nmap.org" // Nmap 官方提供的测试靶机
          fmt.Printf("Scanning %s ...\n", host)
          start := time.Now()

          // 1. 初始化通道
          // jobs: 存放要扫描的端口号
          // results: 存放扫描结果（开放的端口号）
          jobs := make(chan int, 100)
          results := make(chan int, 100)
          
          var wg sync.WaitGroup

          // 2. 启动 50 个并发 Worker
          // 这些 Worker 启动后会立刻阻塞在 range jobs 上，等待任务
          for i := 0; i < 50; i++ {
              wg.Add(1)
              go func() {
                  defer wg.Done()
                  // 不断取任务，直到 jobs 通道被关闭
                  for port := range jobs {
                      if CheckPort(host, port) {
                          results <- port // 发现开放端口，发送给结果通道
                      } else {
                          results <- 0    // 未开放，发送 0 表示完成
                      }
                  }
              }()
          }

          // 3. 生产者：分发任务 (1-1024 端口)
          // 另起一个协程去发任务，避免阻塞主线程接收结果
          go func() {
              for i := 1; i <= 1024; i++ {
                  jobs <- i
              }
              close(jobs) // 发完了，记得关掉，通知 Worker 准备收工
          }()

          // 4. 收集结果
          var openPorts []int
          
          // 既然我们知道发了 1024 个任务，就肯定会收到 1024 个结果
          // 这里可以直接循环 1024 次，或者用另外的 WaitGroup 策略
          for i := 1; i <= 1024; i++ {
              port := <-results
              if port != 0 {
                  openPorts = append(openPorts, port)
                  fmt.Printf("✨ Port %d is open\n", port)
              }
          }

          // 5. 打印最终报告
          close(results)
          sort.Ints(openPorts) // 排序让结果更好看
          
          fmt.Printf("\n--- Scan Finished in %v ---\n", time.Since(start))
          fmt.Printf("Total %d ports open: %v\n", len(openPorts), openPorts)
      }
      ```
    </CodeGroup>

    **代码解释**：

    * **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 数量，观察性能变化

    **常见错误**：

    <Warning>
      * ❌ **句柄泄漏**：很多新手在 `net.Dial` 成功后忘了写 `conn.Close()`。当并发量大时，你的程序会因为 `Socket` 没释放而崩溃。
      * ❌ **死锁**：如果你在主线程里塞任务 `jobs <- i`，但是没有启动 Worker 或者 Worker 处理不过来导致 jobs 满了，主线程就会卡死。建议把任务生成也放到 `go func()` 里。
    </Warning>
  </Step>
</Steps>

***

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="lightbulb" size={26} color="#059669" /> 拓展任务 (30 mins)</span>

<CardGroup cols={2}>
  <Card title="命令行参数化" icon="terminal">
    **任务**：使用 `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`
  </Card>

  <Card title="更详细的探测" icon="magnifying-glass">
    **任务**：只知道端口开放还不够。

    **挑战**：对于开放的端口，尝试读取它返回的前 1KB 数据（Banner Grabbing），看看能不能识别出它是 SSH 还是 HTTP 服务器。
  </Card>
</CardGroup>

***

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="file" size={26} color="#059669" /> 今日产出物</span>

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

***

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="github" size={26} color="#059669" /> 参考代码</span>

<CardGroup cols={2}>
  <Card title="查看参考代码" icon="code" href="https://github.com/akriamail/insightful-ops/tree/main/docs/mintlify/scripts/training/30daystudy/week-4/day-25">
    在 GitHub 查看完整的示例代码

    <br />

    <small>包含命令行参数解析的完整版</small>
  </Card>

  <Card title="在线运行" icon="play" href="https://replit.com">
    使用 Replit 运行 (网络可能受限)

    <br />

    <small>建议本地运行以获得最佳网络体验</small>
  </Card>
</CardGroup>

***

## 实际应用场景

<CardGroup cols={2}>
  <Card title="性能大比拼" icon="chart-bar">
    * **Python (单线程)**: 扫描 1000 个端口约需 **5 分钟** (300秒)。
    * **Go (50 并发 Worker)**: 扫描 1000 个端口约需 **3 秒**。
    * 这就是 **100 倍** 的性能差距，也是为什么 Go 是开发网络工具的首选。
  </Card>

  <Card title="安全合规提示" icon="shield-check">
    * **不要扫公网 IP**: 除非是你自己的服务器。扫描他人 IP 在很多国家是违法的。
    * **内网巡检**: 这个工具非常适合用于企业内网巡检，快速发现有哪些机器违规开放了高危端口（如 3389, 23）。
  </Card>
</CardGroup>

<Tip>
  **与 Day 26 的关联**：今天我们解决了 **网络 IO 并发**。明天我们将挑战 **本地磁盘 IO 并发**，编写一个能瞬间分析 GB 级日志的统计工具。
</Tip>

***

<CardGroup cols={2}>
  <Card title="回到目录" href="/training/30daystudy/week-4">
    查看完整进度
  </Card>

  <Card title="下一天: 工具开发 II" href="/training/30daystudy/day-26">
    Day 26 | Go 实战工具开发 (二)
  </Card>
</CardGroup>
