You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在并发Go端口扫描器中安全实现进度条以避免输出混乱?

如何在并发Go端口扫描器中安全实现进度条以避免输出混乱?

这个问题太常见啦——并发场景下多个goroutine直接往标准输出写内容,必然会因为竞争导致输出乱成一锅粥。要解决这个问题,核心思路是把所有输出操作集中到单个goroutine处理,同时用安全的方式跟踪扫描进度,彻底规避并发写入的冲突。

咱们来一步步改造你的代码,实现既安全又美观的进度条:


核心改进点

  1. 统一输出通道:创建一个专用通道,所有goroutine的输出请求(包括开放端口提示、进度更新)都通过这个通道发送,由单独的goroutine负责实际打印,从根源避免并发写入竞争。
  2. 安全跟踪扫描进度:用原子变量记录已扫描端口数和开放端口数,保证并发更新的安全性。
  3. 动态进度条:利用ANSI转义码实现进度条的实时刷新,避免刷屏式输出。

改造后的完整代码

package main

import (
    "flag"
    "fmt"
    "log"
    "net"
    "strconv"
    "sync"
    "time"
    "sync/atomic"
)

// 定义输出消息的结构体,区分不同类型的输出请求
type OutputMsg struct {
    MsgType string // "open" 表示开放端口,"progress" 表示进度更新
    Address string // 仅open类型使用
    Scanned int64  // 已扫描端口数
    Total   int64  // 总端口数
    Open    int32  // 已发现的开放端口数
}

func scanhost(host string, start int, step int, end int, wg *sync.WaitGroup, 
              openPorts *int32, scannedPorts *int64, outputChan chan<- OutputMsg) {
    defer wg.Done()

    for i := start; i <= end; i += step {
        address := host + ":" + strconv.Itoa(i)

        // 扫描完成后更新已扫描数,发送进度更新
        atomic.AddInt64(scannedPorts, 1)
        outputChan <- OutputMsg{
            MsgType: "progress",
            Scanned: atomic.LoadInt64(scannedPorts),
            Total:   int64(end - start + 1),
            Open:    atomic.LoadInt32(openPorts),
        }

        conn, err := net.DialTimeout("tcp", address, 500*time.Millisecond)
        if err != nil {
            continue
        }

        // 发现开放端口,发送提示消息
        outputChan <- OutputMsg{
            MsgType: "open",
            Address: address,
        }
        atomic.AddInt32(openPorts, 1)
        conn.Close()
    }
}

func GetOutboundIP() net.IP {
    conn, err := net.Dial("udp", "8.8.8.8:80")
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()
    localAddr := conn.LocalAddr().(*net.UDPAddr)
    return localAddr.IP
}

func main() {
    hostFlag := flag.String("host", "", "Target host to scan (default: auto-detect outbound IP)")
    workersFlag := flag.Int("workers", 20, "Number of concurrent workers")
    startPortFlag := flag.Int("start", 1, "Starting port")
    endPortFlag := flag.Int("end", 65535, "Ending port")
    flag.Parse()

    host := *hostFlag
    if host == "" {
        host = GetOutboundIP().String()
        fmt.Printf("No host specified. Using local IP address: %v\n", host)
    } else {
        fmt.Printf("Scanning host: %v\n", host)
    }

    var wg sync.WaitGroup
    workers := *workersFlag
    var openPorts int32 = 0
    var scannedPorts int64 = 0
    totalPorts := int64(*endPortFlag - *startPortFlag + 1)

    // 创建输出通道,设置缓冲避免扫描goroutine阻塞
    outputChan := make(chan OutputMsg, 100)

    // 启动专门的输出处理goroutine
    go func() {
        for msg := range outputChan {
            switch msg.MsgType {
            case "open":
                // 打印开放端口时先换行,避免覆盖进度条
                fmt.Printf("\x1b[0;42mOPEN\x1b[0m %v\n", msg.Address)
                // 重新打印进度条,保证进度条始终在最后一行
                progress := float64(atomic.LoadInt64(&scannedPorts)) / float64(totalPorts) * 100
                fmt.Printf("\rScanned: %d/%d (%.1f%%) | Open Ports: %d \x1b[K", 
                    scannedPorts, totalPorts, progress, atomic.LoadInt32(&openPorts))
            case "progress":
                // 用\r回到行首,\x1b[K清除当前行剩余内容,实现动态刷新
                progress := float64(msg.Scanned) / float64(totalPorts) * 100
                fmt.Printf("\rScanned: %d/%d (%.1f%%) | Open Ports: %d \x1b[K", 
                    msg.Scanned, totalPorts, progress, msg.Open)
            }
        }
    }()

    // 启动扫描goroutine
    for i := 0; i < workers; i++ {
        wg.Add(1)
        go scanhost(host, *startPortFlag+i, workers, *endPortFlag, &wg, &openPorts, &scannedPorts, outputChan)
    }

    // 等待所有扫描任务完成
    wg.Wait()
    // 关闭输出通道,让输出goroutine退出
    close(outputChan)

    // 打印最终结果,换行保证排版美观
    fmt.Printf("\n\nScan complete. Total open ports found: %d\n", openPorts)
}

关键代码解释

  1. OutputMsg结构体:统一了两种输出类型,让输出goroutine能清晰区分处理开放端口提示和进度更新。
  2. 输出通道:所有扫描goroutine不再直接打印,而是把消息发送到通道,由单独goroutine处理,彻底解决并发写入冲突。
  3. 原子变量跟踪进度:scannedPorts和openPorts都用原子操作更新,保证并发下的计数准确性。
  4. ANSI转义码:
    • \r:让光标回到当前行的开头,实现进度条的覆盖刷新
    • \x1b[K:清除当前行光标之后的所有内容,避免旧的进度条残留
    • 开放端口打印后重新输出进度条,保证进度条始终在最后一行

这样改造后,你会发现输出完全不会混乱,开放端口的信息清晰显示,进度条也会实时动态更新,体验提升很多!

备注:内容来源于stack exchange,提问作者Vuq17

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.13 18:09:27