在网络安全和系统管理中,端口扫描器是一种非常有用的工具。它可以帮助我们确定目标主机上哪些端口是开放的,从而了解系统的服务和潜在的安全风险。在本文中,我们将使用 Go 语言(Golang)编写三种不同版本的 TCP 端口扫描器:非并发版、并发版和 goroutine 池并发版。
P1:非并发版 TCP 端口扫描器
非并发版的端口扫描器是最基本的实现方式。它逐个扫描目标主机的端口,速度相对较慢,但对于理解端口扫描的基本原理非常有帮助。
以下是用 Go 语言实现非并发版 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)
        conn, err := net.Dial("tcp", address)
        if err == nil {
            fmt.Printf("Port %d is open\n", port)
            conn.Close()
        }
    }
}
        在这个版本中,我们使用了一个简单的循环来遍历从 1 到 65535 的所有端口。对于每个端口,我们尝试建立一个 TCP 连接到目标主机。如果连接成功,说明该端口是开放的,我们打印出端口号。
P2:并发版 TCP 端口扫描器
并发版的端口扫描器利用了 Go 语言的并发特性,可以同时扫描多个端口,大大提高了扫描速度。
以下是并发版 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
    for port := 1; port <= 65535; port++ {
        wg.Add(1)
        go scanPort(targetHost, port, &wg)
    }
    wg.Wait()
}
        在这个版本中,我们定义了一个scanPort函数,它接受目标主机和端口号作为参数,并尝试建立一个 TCP 连接。在主函数中,我们使用一个sync.WaitGroup来等待所有的 goroutine 完成。对于每个端口,我们启动一个新的 goroutine 来调用scanPort函数。
P3:goroutine 池并发版 TCP 端口扫描器
虽然并发版的端口扫描器速度很快,但如果同时启动的 goroutine 数量过多,可能会导致系统资源耗尽。为了解决这个问题,我们可以使用 goroutine 池来限制同时运行的 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
    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)
        }
    }
}
        在这个版本中,我们创建了一个固定数量的 goroutine(即 goroutine 池)来扫描端口。我们使用两个通道:一个用于传递端口号给 worker goroutine,另一个用于接收扫描结果。在主函数中,我们启动多个 worker goroutine,然后将端口号发送到端口通道。worker goroutine 从端口通道中接收端口号,进行扫描,并将结果发送到结果通道。最后,我们从结果通道中读取并打印开放的端口号。
以下是对三种 Go 语言编写的 TCP 端口扫描器的原理介绍:
P1:非并发版 TCP 端口扫描器
原理:
- 循环遍历从 1 到 65535 的所有端口号。对于每个端口号,通过
net.Dial("tcp", address)尝试与目标主机建立 TCP 连接。 - 如果连接成功,说明该端口处于开放状态,打印出"Port [端口号] is open"的信息,然后关闭连接。
 - 逐个端口进行尝试,不使用并发机制,速度相对较慢,因为在测试完一个端口后才会测试下一个端口。
 
P2:并发版 TCP 端口扫描器
原理:
- 定义了一个
scanPort函数,该函数接受目标主机和端口号作为参数。在函数内部,尝试建立 TCP 连接,如果连接成功,说明端口开放,打印端口号并关闭连接。 - 在主函数中,使用
sync.WaitGroup来协调多个 goroutine 的执行。对于每个端口号,启动一个新的 goroutine 来执行scanPort函数。 - 通过同时启动多个 goroutine 来并发地扫描不同的端口,大大提高了扫描速度。每个 goroutine 独立地尝试连接目标主机的一个端口,互不干扰。
 
P3:goroutine 池并发版 TCP 端口扫描器
原理:
- 首先创建了两个通道,一个用于传递端口号给 worker goroutine(
ports通道),另一个用于接收扫描结果(results通道)。 - 定义了一个
worker函数,作为 goroutine 池中的工作函数。每个 worker goroutine 从ports通道中接收端口号,尝试建立 TCP 连接,如果连接成功,将结果("Port [端口号] is open")发送到results通道,否则发送空字符串。 - 在主函数中,启动固定数量的 worker goroutine(即创建 goroutine 池),然后将端口号逐个发送到
ports通道。 - 当所有端口号都发送完毕后,关闭
ports通道。同时,启动一个新的 goroutine 来等待所有 worker goroutine 完成任务(通过sync.WaitGroup),并在所有任务完成后关闭results通道。 - 最后,从
results通道中读取结果并打印开放的端口号。 - 这种方式通过限制同时运行的 goroutine 数量,避免了无限制并发可能导致的系统资源耗尽问题,同时仍然能够利用并发来提高扫描速度。
 
通过以上三种版本的 TCP 端口扫描器,我们可以看到 Go 语言的并发特性在提高程序性能方面的强大之处。在实际应用中,我们可以根据具体需求选择合适的版本。同时,需要注意的是,在进行端口扫描时,应该遵守法律法规,确保获得合法的授权。