开发一款扫描器,Goroutine越多越好?
字数 1116 2025-08-22 12:22:30
Go语言扫描器开发与Goroutine并发控制详解
前言
扫描器在网络安全领域扮演着至关重要的角色,它既是攻击者发现系统弱点的工具,也是防御者检测系统漏洞的武器。Go语言凭借其出色的并发特性,成为开发高效扫描器的理想选择。本文将深入探讨如何利用Go语言开发高性能扫描器,并重点分析Goroutine并发控制的正确使用方法。
TCP全连接端口扫描基础
基本原理
TCP全连接端口扫描是最基础的扫描技术,其原理是通过尝试与目标IP的特定端口建立完整TCP连接:
func Connect(ip string, port int) (net.Conn, error) {
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", ip, port), 2*time.Second)
defer func() {
if conn != nil {
_ = conn.Close()
}
}()
return conn, err
}
单线程扫描实现
最基本的扫描实现是顺序执行每个端口的探测:
func main() {
for i := 0; i < task_cnt; i++ {
// 对任务列表进行依次调用探测
}
}
这种实现简单但效率低下,无法充分利用现代计算机的多核性能。
Goroutine并发扫描的问题与风险
无限制Goroutine的危险
Go语言的Goroutine虽然轻量,但无限制地创建仍会导致严重问题:
func main() {
task_cnt := math.MaxInt64
for i := 0; i < task_cnt; i++ {
go func(i int) {
fmt.Println("go func", i, "goroutine count =", runtime.NumGoroutine())
}(i)
}
}
上述代码会导致:
- CPU使用率急剧上升
- 内存占用不断增长
- 最终进程被操作系统强制终止(signal: killed)
Goroutine并发控制方法
方法一:带缓冲的Channel限速
func busi(ch chan bool, i int) {
fmt.Println("go func", i, "goroutine count =", runtime.NumGoroutine())
<-ch
}
func main() {
task_cnt := math.MaxInt64
ch := make(chan bool, 3) // 限制并发数为3
for i := 0; i < task_cnt; i++ {
ch <- true
go busi(ch, i)
}
}
这种方法控制了Goroutine的创建速度,但需要注意主进程可能提前结束的问题。
方法二:带阻塞等待的Channel限速
var wg = sync.WaitGroup{}
func busi(ch chan bool, i int) {
fmt.Println("go func", i, "goroutine count =", runtime.NumGoroutine())
<-ch
wg.Done()
}
func main() {
task_cnt := math.MaxInt64
ch := make(chan bool, 3)
for i := 0; i < task_cnt; i++ {
wg.Add(1)
ch <- true
go busi(ch, i)
}
wg.Wait()
}
通过sync.WaitGroup确保所有任务完成后再退出主进程。
方法三:无缓冲Channel与任务生产/消费分离(推荐)
var wg = sync.WaitGroup{}
func busi(ch chan int) {
for t := range ch {
fmt.Println("go task =", t, ", goroutine count =", runtime.NumGoroutine())
wg.Done()
}
}
func sendTask(task int, ch chan int) {
wg.Add(1)
ch <- task
}
func main() {
ch := make(chan int) // 无缓冲Channel
goCnt := 3 // Goroutine数量
// 启动工作池
for i := 0; i < goCnt; i++ {
go busi(ch)
}
// 发送任务
taskCnt := math.MaxInt64
for t := 0; t < taskCnt; t++ {
sendTask(t, ch)
}
wg.Wait()
}
这种模式实现了:
- 控制并发Goroutine数量
- 任务生产与执行分离
- 确保所有任务完成
- 资源利用率最大化
完整扫描器实现思路
1. 生成扫描任务列表
使用github.com/malfunkt/iprange包解析Nmap风格的IP范围:
list, err := iprange.ParseList("10.0.0.1, 10.0.0.5-10, 192.168.1.*, 192.168.10.0/24")
if err != nil {
log.Printf("error: %s", err)
}
rng := list.Expand()
2. 任务分组策略
将扫描任务按[ ]map[IP]Port结构分组,每个Goroutine处理一组任务:
type ScanTask struct {
IP string
Port int
}
// 任务分组逻辑
func groupTasks(ips []string, ports []int, groupSize int) [][]ScanTask {
// 实现任务分组算法
}
3. 并发扫描执行
结合推荐的工作池模式实现并发扫描:
func worker(id int, tasks <-chan ScanTask, results chan<- ScanResult) {
for task := range tasks {
conn, err := net.DialTimeout("tcp",
fmt.Sprintf("%s:%d", task.IP, task.Port), 2*time.Second)
result := ScanResult{
IP: task.IP,
Port: task.Port,
Open: err == nil,
}
if conn != nil {
conn.Close()
}
results <- result
}
}
func main() {
// 初始化任务和结果Channel
tasks := make(chan ScanTask, 100)
results := make(chan ScanResult, 100)
// 启动工作池
for w := 1; w <= 10; w++ {
go worker(w, tasks, results)
}
// 发送任务
go func() {
for _, taskGroup := range taskGroups {
for _, task := range taskGroup {
tasks <- task
}
}
close(tasks)
}()
// 处理结果
for range taskGroups {
result := <-results
// 处理扫描结果
}
}
4. 结果存储与展示
根据实际需求选择结果存储方式:
- 简单场景:直接输出到控制台或文件
- 复杂场景:存入数据库(MySQL、Elasticsearch等)
type ScanResult struct {
IP string
Port int
Open bool
Service string
Banner string
}
// 存储到文件
func saveToFile(results []ScanResult, filename string) error {
// 实现文件存储逻辑
}
// 存储到数据库
func saveToDB(results []ScanResult) error {
// 实现数据库存储逻辑
}
性能优化建议
- 连接超时优化:根据网络状况调整
DialTimeout值 - 端口扫描顺序:优先扫描常见服务端口
- 结果缓存:避免重复扫描相同目标
- 速率控制:实现动态调整扫描速率
- 错误处理:完善错误处理和重试机制
总结
开发高效的Go语言扫描器需要注意:
- 避免无限制创建Goroutine
- 使用Channel和WaitGroup实现并发控制
- 采用生产者-消费者模式分离任务
- 合理分组扫描任务
- 完善结果处理和存储机制
通过本文介绍的方法,您可以构建出既高效又稳定的端口扫描器,为网络安全工作提供有力工具。