手把手教你用Go语言打造一款简易TCP端口扫描器

开发 前端
这次呢, 咱们来实现一个简单的TCP端口扫描器!

[[374779]]

 前言

Hey,大家好呀,我是码农,星期八。

这次呢, 咱们来实现一个简单的TCP端口扫描器!

也来体验一下黑客的风采!

TCP扫描本质

我们在使用TCP进行连接时,需要知道对方机器的ip:port

正常握手

连接成功的话,流程如下。


连接失败

有正常,就有失败,如果被连接方关闭的话,流程如下。


如果有防火墙

还有一种可能是,端口开放,但是防火墙拦截,流程如下。


代码

本质理解之后,就可以开始撸代码了。

在Go中,我们通常使用net.Dial进行TCP连接

它就两种情况

  • 成功:返回conn。
  • 失败:err != nil。

普通版

相对来说,刚开始时,我们可能都不是太胆大,都是先写原型,也不考虑性能。

代码

  1. package main 
  2.  
  3. import ( 
  4.     "fmt" 
  5.     "net" 
  6.  
  7. func main() { 
  8.     var ip = "192.168.43.34" 
  9.     for i := 21; i <= 120; i++ { 
  10.         var address = fmt.Sprintf("%s:%d", ip, i) 
  11.         conn, err := net.Dial("tcp", address) 
  12.         if err != nil { 
  13.             fmt.Println(address, "是关闭的"
  14.             continue 
  15.         } 
  16.         conn.Close() 
  17.         fmt.Println(address, "打开"
  18.   } 

执行结果


但是这个过程是非常缓慢的。

因为net.Dial如果连接的是未开放的端口,一个端口可能就是20s+,所以,我们为什么学习多线程懂了把!!!

多线程版

上述是通过循环去一个个连接ip:port的,那我们就知道了,在一个个连接的位置,让多个人去干就好了。

所以,多线程如下。

代码

  1. package main 
  2.  
  3. import ( 
  4.     "fmt" 
  5.     "net" 
  6.     "sync" 
  7.     "time" 
  8.  
  9. func main() { 
  10.  
  11.     var begin =time.Now() 
  12.     //wg 
  13.     var wg sync.WaitGroup 
  14.     //ip 
  15.     var ip = "192.168.99.112" 
  16.     //var ip = "192.168.43.34" 
  17.     //循环 
  18.     for j := 21; j <= 65535; j++ { 
  19.         //添加wg 
  20.         wg.Add(1) 
  21.         go func(i int) { 
  22.             //释放wg 
  23.             defer wg.Done() 
  24.             var address = fmt.Sprintf("%s:%d", ip, i) 
  25.             //conn, err := net.DialTimeout("tcp", address, time.Second*10) 
  26.             conn, err := net.Dial("tcp", address) 
  27.             if err != nil { 
  28.                 //fmt.Println(address, "是关闭的", err) 
  29.                 return 
  30.             } 
  31.             conn.Close() 
  32.             fmt.Println(address, "打开"
  33.         }(j) 
  34.     //等待wg 
  35.     wg.Wait() 
  36.     var elapseTime = time.Now().Sub(begin
  37.     fmt.Println("耗时:", elapseTime) 

执行结果

 

其实是同时开启了6W多个线程,去扫描每个ip:port。

所以耗时最长的线程结束的时间,就是程序结束的时间。

感觉还行,20s+扫描完6w多个端口!!!

线程池版

上面我们简单粗暴的方式为每个ip:port都创建了一个协程。

虽然在Go中,理论上协程开个几十万个都没问题,但是还是有一些压力的。

所以我们应该采用一种相对节约的方式进行精简代码,一般采用线程池方式。

本次使用的线程池包:gohive

地址:https://github.com/loveleshsharma/gohive

简单介绍


代码

  1. package main 
  2.  
  3. //线程池方式 
  4. import ( 
  5.     "fmt" 
  6.     "github.com/loveleshsharma/gohive" 
  7.     "net" 
  8.     "sync" 
  9.     "time" 
  10.  
  11. //wg 
  12. var wg sync.WaitGroup 
  13.  
  14. //地址管道,100容量 
  15. var addressChan = make(chan string, 100) 
  16.  
  17. //工人 
  18. func worker() { 
  19.     //函数结束释放连接 
  20.     defer wg.Done() 
  21.     for { 
  22.         address, ok := <-addressChan 
  23.         if !ok { 
  24.             break 
  25.         } 
  26.         //fmt.Println("address:", address) 
  27.         conn, err := net.Dial("tcp", address) 
  28.         //conn, err := net.DialTimeout("tcp", address, 10) 
  29.         if err != nil { 
  30.             //fmt.Println("close:", address, err) 
  31.             continue 
  32.         } 
  33.         conn.Close() 
  34.         fmt.Println("open:", address) 
  35. func main() { 
  36.     var begin = time.Now() 
  37.     //ip 
  38.     var ip = "192.168.99.112" 
  39.     //线程池大小 
  40.     var pool_size = 70000 
  41.     var pool = gohive.NewFixedSizePool(pool_size) 
  42.  
  43.     //拼接ip:端口 
  44.     //启动一个线程,用于生成ip:port,并且存放到地址管道种 
  45.     go func() { 
  46.         for port := 1; port <= 65535; port++ { 
  47.             var address = fmt.Sprintf("%s:%d", ip, port) 
  48.             //将address添加到地址管道 
  49.             //fmt.Println("<-:",address) 
  50.             addressChan <- address 
  51.         } 
  52.         //发送完关闭 addressChan 管道 
  53.         close(addressChan) 
  54. }() 
  55.     //启动pool_size工人,处理addressChan种的每个地址 
  56.     for work := 0; work < pool_size; work++ { 
  57.         wg.Add(1) 
  58.         pool.Submit(worker) 
  59.     //等待结束 
  60.     wg.Wait() 
  61.     //计算时间 
  62.     var elapseTime = time.Now().Sub(begin
  63.     fmt.Println("耗时:", elapseTime) 

执行结果


我设置的线程池大小是7w个,所以也是一下子开启6w多个协程的,但是我们已经可以进行线程大小约束了。

假设现在有这样的去求,有100个ip,需要扫描每个ip开放的端口,如果采用简单粗暴开线程的方式.

那就是100+65535=6552300,600多w个线程,还是比较消耗内存的,可能系统就会崩溃,如果采用线程池方式。

将线程池控制在50w个,或许情况就会好很多。

但是有一点的是,在Go中,线程池通常需要配合chan使用,可能需要不错的基础。

总结

本篇更偏向于乐趣篇,了解一下好玩的玩意。

其实还可以通过net.DialTimeout连接ip:port,这个可以设置超时时间,比如超时5s就判定端口未开放。

此处就不做举例了。

咱们主要使用三种方式来实现功能。

  • 正常版,没有并发,速度很慢。
  • 多协程版,并发,性能很高,但是协程太多可能会崩溃。
  • 协程池版,并发,性能高,协程数量可控。

通常情况下,如果基础可以,更推荐使用协程池方式。

用微笑告诉别人,今天的我比昨天强,今后也一样。

 

责任编辑:姜华 来源: Go语言进阶学习
相关推荐

2021-01-21 06:04:55

Go语言TCP目录生成器

2024-01-07 20:00:27

2023-05-22 10:04:24

2021-02-01 08:41:06

Java考试系统

2021-02-04 15:52:46

Java考试系统

2017-09-14 09:09:04

php应用LibreOfficeWord转HTML

2021-01-04 09:55:26

Java移动互联网

2021-01-05 09:04:20

Javatxt文件

2022-02-17 10:26:17

JavaScript扫雷游戏前端

2021-11-01 10:26:07

CanvasAPI画布技术HTML5

2021-12-30 08:56:57

Python摸鱼倒计界面Python基础

2021-08-13 09:01:31

Python小游戏Python基础

2021-08-09 13:31:25

PythonExcel代码

2022-01-24 11:02:27

PySimpleGUPython计算器

2022-10-19 14:30:59

2011-03-28 16:14:38

jQuery

2021-02-06 14:55:05

大数据pandas数据分析

2021-02-04 09:00:57

SQLDjango原生

2022-08-04 10:39:23

Jenkins集成CD

2022-06-30 16:10:26

Python计时器装饰器
点赞
收藏

51CTO技术栈公众号