在 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
.
我目前正在学习围棋。为此,我正在制作一个相对简单的端口扫描器。
我面临的问题是扫描这些端口需要花费大量时间。我的行为是,如果我扫描端口(定义为 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
.