如何在并发Go端口扫描器中安全实现进度条以避免输出混乱?
如何在并发Go端口扫描器中安全实现进度条以避免输出混乱?
这个问题太常见啦——并发场景下多个goroutine直接往标准输出写内容,必然会因为竞争导致输出乱成一锅粥。要解决这个问题,核心思路是把所有输出操作集中到单个goroutine处理,同时用安全的方式跟踪扫描进度,彻底规避并发写入的冲突。
咱们来一步步改造你的代码,实现既安全又美观的进度条:
核心改进点
- 统一输出通道:创建一个专用通道,所有goroutine的输出请求(包括开放端口提示、进度更新)都通过这个通道发送,由单独的goroutine负责实际打印,从根源避免并发写入竞争。
- 安全跟踪扫描进度:用原子变量记录已扫描端口数和开放端口数,保证并发更新的安全性。
- 动态进度条:利用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) }
关键代码解释
- OutputMsg结构体:统一了两种输出类型,让输出goroutine能清晰区分处理开放端口提示和进度更新。
- 输出通道:所有扫描goroutine不再直接打印,而是把消息发送到通道,由单独goroutine处理,彻底解决并发写入冲突。
- 原子变量跟踪进度:
scannedPorts和openPorts都用原子操作更新,保证并发下的计数准确性。 - ANSI转义码:
\r:让光标回到当前行的开头,实现进度条的覆盖刷新\x1b[K:清除当前行光标之后的所有内容,避免旧的进度条残留- 开放端口打印后重新输出进度条,保证进度条始终在最后一行
这样改造后,你会发现输出完全不会混乱,开放端口的信息清晰显示,进度条也会实时动态更新,体验提升很多!
备注:内容来源于stack exchange,提问作者Vuq17
相关产品推荐
相关产品推荐

