Go 语言 TCP 端口扫描器实现与 Goroutine 池原理

一、引言

在网络管理与安全领域,TCP 端口扫描器是一款不可或缺的工具。它能够帮助我们快速探测目标主机上开放的端口,从而了解其提供的服务以及潜在的安全漏洞。Go 语言凭借其出色的并发性能,为编写高效的端口扫描器提供了有力支持。本文将详细介绍使用 Go 语言编写的三种 TCP 端口扫描器:非并发版、并发版以及基于 Goroutine 池的并发版,并深入剖析 Goroutine 池的原理及其在端口扫描器中的应用。

二、非并发版 TCP 端口扫描器

(一)代码实现

go 复制代码
package main

import (
    "fmt"
    "net"
)

func main() {
    targetHost := "127.0.0.1"
    // 遍历所有可能的端口号
    for port := 1; port <= 65535; port++ {
        address := fmt.Sprintf("%s:%d", targetHost, port)
        // 尝试建立 TCP 连接
        conn, err := net.Dial("tcp", address)
        if err == nil {
            // 连接成功,端口开放
            fmt.Printf("Port %d is open\n", port)
            conn.Close()
        }
    }
}

(二)原理剖析

此非并发版扫描器采用了最基础的扫描策略。它按照顺序,从端口 1 开始,逐个对目标主机的端口尝试建立 TCP 连接。对于每一个端口,使用net.Dial("tcp", address)函数进行连接操作。如果连接过程没有错误发生,即err == nil,则表明该端口处于开放状态,此时打印出端口号,并关闭连接以释放资源。由于是逐个端口进行扫描,没有利用并发机制,所以扫描速度相对较慢,尤其在面对大量端口时,效率较低。

三、并发版 TCP 端口扫描器

(一)代码实现

go 复制代码
package main

import (
    "fmt"
    "net"
    "sync"
)

func scanPort(host string, port int, wg *sync.WaitGroup) {
    defer wg.Done()
    address := fmt.Sprintf("%s:%d", host, port)
    conn, err := net.Dial("tcp", address)
    if err == nil {
        fmt.Printf("Port %d is open\n", port)
        conn.Close()
    }
}

func main() {
    targetHost := "127.0.0.1"
    var wg sync.WaitGroup
    // 遍历端口号并启动 goroutine 进行扫描
    for port := 1; port <= 65535; port++ {
        wg.Add(1)
        go scanPort(targetHost, port, &wg)
    }
    wg.Wait()
}

(二)原理剖析

并发版扫描器充分发挥了 Go 语言的并发特性。首先定义了scanPort函数,该函数负责对单个端口进行扫描操作。在主函数中,通过循环遍历所有端口号,针对每个端口启动一个独立的 goroutine 来执行scanPort函数。这里使用了sync.WaitGroup来协调所有 goroutine 的执行。在启动每个 goroutine 之前,通过wg.Add(1)增加等待组的计数,表示有一个任务正在进行。当每个 goroutine 完成任务后,通过defer wg.Done()通知等待组任务已完成。最后,wg.Wait()会阻塞主函数,直到所有的 goroutine 都完成任务。这种并发扫描方式使得多个端口的扫描可以同时进行,大大提高了扫描速度,相比于非并发版,能够在更短的时间内完成对大量端口的扫描。

四、Goroutine 池并发版 TCP 端口扫描器

(一)代码实现

go 复制代码
package main

import (
    "fmt"
    "net"
    "sync"
)

func worker(host string, ports <-chan int, results chan<- string, wg *sync.WaitGroup) {
    defer wg.Done()
    for port := range ports {
        address := fmt.Sprintf("%s:%d", host, port)
        conn, err := net.Dial("tcp", address)
        if err == nil {
            results <- fmt.Sprintf("Port %d is open", port)
            conn.Close()
        } else {
            results <- ""
        }
    }
}

func main() {
    targetHost := "127.0.0.1"
    numWorkers := 100
    // 创建端口号通道和结果通道
    ports := make(chan int, 65535)
    results := make(chan string, 65535)
    var wg sync.WaitGroup

    // 启动 worker goroutine
    for i := 0; i < numWorkers; i++ {
        wg.Add(1)
        go worker(targetHost, ports, results, &wg)
    }

    // 向端口号通道发送端口号
    for port := 1; port <= 65535; port++ {
        ports <- port
    }
    close(ports)

    go func() {
        wg.Wait()
        close(results)
    }()

    // 处理扫描结果
    for result := range results {
        if result!= "" {
            fmt.Println(result)
        }
    }
}

(二)原理剖析

1. 整体架构

此版本的扫描器基于 Goroutine 池的概念构建。主要由任务队列(端口号通道ports)、工作者(worker函数对应的 goroutine)以及协调机制(sync.WaitGroup和通道关闭操作)组成。

2. 任务队列

ports通道作为任务队列,用于存储待扫描的端口号。其缓冲区大小设置为 65535,以容纳所有可能的端口号。通过循环将 1 到 65535 的端口号逐个发送到ports通道中,完成任务的入队操作。当所有端口号都发送完毕后,关闭ports通道,表示任务队列不再接受新的任务。

3. 工作者

worker函数定义了工作者的行为。每个工作者从ports通道中接收端口号,尝试建立与目标主机对应端口的 TCP 连接。如果连接成功,将包含开放端口号信息的字符串发送到results通道;如果连接失败,则发送空字符串。工作者通过for port := range ports循环不断从任务队列获取任务,直到任务队列关闭且所有任务都被处理完毕。

4. 协调机制

使用sync.WaitGroup来协调所有工作者的完成状态。在启动每个工作者 goroutine 时,通过wg.Add(1)增加计数。当工作者完成任务(即for循环结束)后,通过defer wg.Done()通知等待组任务完成。同时,启动一个独立的 goroutine,在其中等待所有工作者完成任务(wg.Wait()),当所有工作者完成后,关闭results通道,表示结果收集阶段结束。

5. 结果处理

主函数通过for result := range results循环从results通道中读取扫描结果。如果结果字符串不为空,则表示对应的端口开放,将其打印输出。

五、Goroutine 池原理

(一)基本概念

Goroutine 池是一种用于管理和复用 Goroutine 的机制。在处理大量并发任务时,如果无节制地创建 Goroutine,可能会导致系统资源耗尽,例如内存占用过高、调度开销过大等问题。Goroutine 池通过预先创建一定数量的 Goroutine(工作者),并让它们从任务队列中获取任务执行,从而实现对 Goroutine 的有效管理和资源的合理利用。

(二)组成部分

1. 任务队列

任务队列是 Goroutine 池的核心组件之一。它是一个通道(如上述代码中的ports通道),用于存储待执行的任务。任务可以是任何需要并发处理的操作,在端口扫描器中就是要扫描的端口号。任务队列的缓冲区大小需要根据任务数量和系统资源进行合理设置,既要避免缓冲区过小导致任务无法及时入队,又要防止缓冲区过大占用过多内存。

2. 工作者

工作者是实际执行任务的 Goroutine。每个工作者从任务队列中获取任务,并执行相应的操作。在端口扫描器的示例中,worker函数对应的 Goroutine 就是工作者。工作者通常会在一个循环中不断地从任务队列获取任务,直到任务队列为空且被关闭,表示所有任务都已完成。工作者的数量也需要根据系统资源和任务特性进行调整,过多的工作者可能导致资源竞争和调度开销增加,过少则无法充分利用系统的并发能力。

3. 协调机制

协调机制用于确保 Goroutine 池的正确运行。在上述代码中,sync.WaitGroup起到了关键的协调作用。它用于跟踪所有工作者的完成状态,确保在所有任务都执行完毕后,进行后续的清理和结果处理工作。此外,通道的关闭操作也是协调机制的一部分。关闭任务通道表示不再有新的任务进入,而关闭结果通道则表示结果收集阶段结束,通知结果处理部分停止等待新的结果。

(三)工作流程

1. 初始化阶段

首先,确定工作者的数量(如numWorkers),创建任务队列(ports通道)和结果队列(results通道),并为任务队列设置合适的缓冲区大小。然后,启动预先设定数量的工作者 Goroutine,每个工作者开始等待从任务队列中获取任务。

2. 任务分配阶段

将需要处理的任务逐个放入任务队列中。在端口扫描器中,就是将 1 到 65535 的端口号依次发送到ports通道。工作者 Goroutine 会自动从任务队列中获取任务,一旦任务队列中有任务可用,工作者就会立即开始执行。

3. 任务执行阶段

工作者从任务队列中获取任务后,执行相应的任务操作。在端口扫描器中,工作者会尝试建立与目标端口的 TCP 连接,并根据连接结果将相应信息发送到结果队列。如果任务执行过程中出现错误或完成任务,工作者会继续从任务队列中获取下一个任务,直到任务队列为空且被关闭。

4. 完成阶段

当所有任务都被放入任务队列且任务队列被关闭后,工作者会继续处理剩余的任务,直到任务队列为空。此时,通过协调机制(如sync.WaitGroup)等待所有工作者完成任务。当所有工作者完成后,关闭结果队列,然后对结果队列中的结果进行处理,如在端口扫描器中打印开放端口的信息。

六、总结

通过对三种不同版本的 Go 语言 TCP 端口扫描器的介绍以及对 Goroutine 池原理的深入剖析,我们可以看到 Go 语言在并发编程方面的强大能力。

非并发版扫描器为我们展示了端口扫描的基本逻辑,而并发版扫描器则体现了 Go 语言并发特性带来的性能提升。

基于 Goroutine 池的并发版扫描器进一步优化了资源利用和性能表现,通过合理管理 Goroutine 的数量和任务队列,避免了无节制并发带来的问题。

相关推荐
酷爱码15 分钟前
如何通过python连接hive,并对里面的表进行增删改查操作
开发语言·hive·python
画个大饼16 分钟前
Go语言实战:快速搭建完整的用户认证系统
开发语言·后端·golang
简单.is.good1 小时前
【计算机网络】IP地址
网络·tcp/ip·计算机网络
喵先生!1 小时前
C++中的vector和list的区别与适用场景
开发语言·c++
Thomas_YXQ2 小时前
Unity3D Lua集成技术指南
java·开发语言·驱动开发·junit·全文检索·lua·unity3d
xMathematics2 小时前
计算机图形学实践:结合Qt和OpenGL实现绘制彩色三角形
开发语言·c++·qt·计算机图形学·cmake·opengl
yuanManGan4 小时前
C++入门小馆: 深入了解STLlist
开发语言·c++
北极的企鹅884 小时前
XML内容解析成实体类
xml·java·开发语言
BillKu4 小时前
Vue3后代组件多祖先通讯设计方案
开发语言·javascript·ecmascript
Python自动化办公社区4 小时前
Python 3.14:探索新版本的魅力与革新
开发语言·python