在 goroutine 中扫描端口

Scanning ports in goroutines

我目前正在学习围棋。为此,我正在制作一个相对简单的端口扫描器。

我面临的问题是扫描这些端口需要花费大量时间。我的行为是,如果我扫描端口(定义为 int32 数组(protobuf 不支持 int16),则不使用 goroutines 是有效的,但是扫描超过 5 个端口时速度很慢,因为它不是运行并联

为了实现并行性,我想出了以下代码(解释+问题在代码之后):

//entry point for port scanning
var results []*portscan.ScanResult
//len(splitPorts) is the given string (see benchmark below) chopped up in an int32 slice
ch := make(chan *portscan.ScanResult, len(splitPorts))

var wg sync.WaitGroup
for _, port := range splitPorts {
    connect(ip, port, req.Timeout, ch, &wg)
}
wg.Wait()

for elem := range ch {
    results = append(results, elem)
}

// go routine
func connect(ip string, port, timeout int32, ch chan *portscan.ScanResult, wg *sync.WaitGroup) {
    wg.Add(1)
    go func() {
        res := &portscan.ScanResult{
            Port:   port,
            IsOpen: false,
        }
        conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", ip, port), time.Duration(timeout)*time.Millisecond)

        if err == nil {
            conn.Close()
            res.IsOpen = true
        }
        ch <- res
        wg.Done()
    }()
}

所以 protobuf 为我准备了一个如下所示的结构:

type ScanResult struct {
    Port   int32 `protobuf:"varint,1,opt,name=port" json:"port,omitempty"`
    IsOpen bool  `protobuf:"varint,2,opt,name=isOpen" json:"isOpen,omitempty"`
}

如代码片段的第一行所示,我定义了一个切片来保存所有结果,我的想法是我的应用程序并行扫描端口,完成后将结果发送给任何感兴趣的人.

但是,使用这段代码,程序卡住了。

我运行这个基准来测试它的性能:

func BenchmarkPortScan(b *testing.B) {
  request := &portscan.ScanPortsRequest{
      Ip:        "62.129.139.214",
      PortRange: "20,21,22,23",
      Timeout:   500,
  }

  svc := newService()

  for i := 0; i < b.N; i++ {
      svc.ScanPorts(nil, request)
  }
}

卡住是什么原因。看看这段代码会泄露什么吗?

所以简而言之,我希望我的最终结果是在不同的 go 例程中扫描每个端口,当它们全部完成时,所有内容都汇集在一个 ScanResult 的结果片段中。

我希望我已经说清楚并提供了足够的信息让你们帮助我。

哦,我特别在寻找指针和学习点,而不是查看工作代码示例。

您需要在 wg.Wait() 后关闭频道。否则你的循环范围会卡住。

除此之外,您的代码看起来还不错。

正如@creker 所写,您必须关闭通道,否则从中读取的循环将是无限循环。但是,我不同意在 wg.Wait() 之后添加 close(ch) 是正确的方法 - 这意味着从通道读取值的循环在扫描所有端口之前不会启动(所有 connect() 调用 return)。我会说您想在结果可用后立即开始处理。为此,你必须重组你的代码,使生产者和消费者成为不同的 goroutines,就像下面的

var results []*portscan.ScanResult
ch := make(chan *portscan.ScanResult)

// launch the producer goroutine    
go func() {
   var wg sync.WaitGroup
   wg.Add(len(splitPorts))
   for _, port := range splitPorts {
       go func(port int32) {
          defer wg.Done()
          ch <- connect(ip, port, req.Timeout)
       }(port)
   }
   wg.Wait()
   close(ch)
}()

// consume results
for elem := range ch {
    results = append(results, elem)
}

func connect(ip string, port, timeout int32) *portscan.ScanResult {
    res := &portscan.ScanResult{
            Port:   port,
            IsOpen: false,
    }
    conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", ip, port), time.Duration(timeout)*time.Millisecond)

    if err == nil {
        conn.Close()
        res.IsOpen = true
    }
    return res
}

请注意,现在通道是无缓冲的,connect 函数不知道等待组或通道,因此它更易于重用。如果事实证明生产者生成数据的速度比消费者读取数据的速度快,您也可以使用缓冲通道,但您可能不需要缓冲区 len(splitPorts),而是更小的东西。

另一个优化可能是预分配 results 数组,因为您似乎事先知道结果的数量 (len(splitPorts)),因此您不需要使用 append.