前言Hey,大家好呀,我是码农,星期八。 这次呢, 咱们来实现一个简单的TCP端口扫描器! 也来体验一下黑客的风采! TCP扫描本质我们在使用TCP进行连接时,需要知道对方机器的ip:port 正常握手连接成功的话,流程如下。 连接失败
有正常,就有失败,如果被连接方关闭的话,流程如下。 如果有防火墙
还有一种可能是,端口开放,但是防火墙拦截,流程如下。 代码
本质理解之后,就可以开始撸代码了。 在Go中,我们通常使用net.Dial 进行TCP连接。 它就两种情况 普通版相对来说,刚开始时,我们可能都不是太胆大,都是先写原型,也不考虑性能。 代码 package main
import ( "fmt" "net" )
func main() { var ip = "192.168.43.34" for i := 21; i <= 120; i++ { var address = fmt.Sprintf("%s:%d", ip, i) conn, err := net.Dial("tcp", address) if err != nil { fmt.Println(address, "是关闭的") continue } conn.Close() fmt.Println(address, "打开") } }
执行结果
但是这个过程是非常缓慢的。
因为net.Dial 如果连接的是未开放的端口,一个端口可能就是20s+,所以,我们为什么学习多线程懂了把!!! 多线程版上述是通过循环去一个个连接ip:port 的,那我们就知道了,在一个个连接的位置,让多个人去干就好了。 所以,多线程如下。 代码 package main
import ( "fmt" "net" "sync" "time" )
func main() {
var begin =time.Now() //wg var wg sync.WaitGroup //ip var ip = "192.168.99.112" //var ip = "192.168.43.34" //循环 for j := 21; j <= 65535; j++ { //添加wg wg.Add(1) go func(i int) { //释放wg defer wg.Done() var address = fmt.Sprintf("%s:%d", ip, i) //conn, err := net.DialTimeout("tcp", address, time.Second*10) conn, err := net.Dial("tcp", address) if err != nil { //fmt.Println(address, "是关闭的", err) return } conn.Close() fmt.Println(address, "打开") }(j) } //等待wg wg.Wait() var elapseTime = time.Now().Sub(begin) fmt.Println("耗时:", elapseTime) }
执行结果
其实是同时开启了6W多个线程,去扫描每个ip:port 。
所以耗时最长的线程结束的时间,就是程序结束的时间。 感觉还行,20s+扫描完6w多个端口!!! 线程池版上面我们简单粗暴的方式为每个ip:port 都创建了一个协程。 虽然在Go中,理论上协程开个几十万个都没问题,但是还是有一些压力的。 所以我们应该采用一种相对节约的方式进行精简代码,一般采用线程池方式。 本次使用的线程池包:gohive 地址:https://github.com/loveleshsharma/gohive 简单介绍 代码
package main
//线程池方式 import ( "fmt" "github.com/loveleshsharma/gohive" "net" "sync" "time" )
//wg var wg sync.WaitGroup
//地址管道,100容量 var addressChan = make(chan string, 100)
//工人 func worker() { //函数结束释放连接 defer wg.Done() for { address, ok := <-addressChan if !ok { break } //fmt.Println("address:", address) conn, err := net.Dial("tcp", address) //conn, err := net.DialTimeout("tcp", address, 10) if err != nil { //fmt.Println("close:", address, err) continue } conn.Close() fmt.Println("open:", address) } } func main() { var begin = time.Now() //ip var ip = "192.168.99.112" //线程池大小 var pool_size = 70000 var pool = gohive.NewFixedSizePool(pool_size)
//拼接ip:端口 //启动一个线程,用于生成ip:port,并且存放到地址管道种 go func() { for port := 1; port <= 65535; port++ { var address = fmt.Sprintf("%s:%d", ip, port) //将address添加到地址管道 //fmt.Println("<-:",address) addressChan <- address } //发送完关闭 addressChan 管道 close(addressChan) }() //启动pool_size工人,处理addressChan种的每个地址 for work := 0; work < pool_size; work++ { wg.Add(1) pool.Submit(worker) } //等待结束 wg.Wait() //计算时间 var elapseTime = time.Now().Sub(begin) fmt.Println("耗时:", elapseTime) }
执行结果
我设置的线程池大小是7w个,所以也是一下子开启6w多个协程的,但是我们已经可以进行线程大小约束了。
假设现在有这样的去求,有100个ip ,需要扫描每个ip 开放的端口,如果采用简单粗暴开线程的方式. 那就是100+65535=6552300 ,600多w个线程,还是比较消耗内存的,可能系统就会崩溃,如果采用线程池方式。 将线程池控制在50w个,或许情况就会好很多。 但是有一点的是,在Go中,线程池通常需要配合chan 使用,可能需要不错的基础。 总结本篇更偏向于乐趣篇,了解一下好玩的玩意。 其实还可以通过net.DialTimeout 连接ip:port ,这个可以设置超时时间,比如超时5s就判定端口未开放。 此处就不做举例了。 咱们主要使用三种方式来实现功能。 通常情况下,如果基础可以,更推荐使用协程池方式。 如果在操作过程中有任何问题,记得下面留言,我们看到会第一时间解决问题。 用微笑告诉别人,今天的我比昨天强,今后也一样。 我是码农星期八,如果觉得还不错,记得动手点赞一下哈。 感谢你的观看。 如果你觉得文章还可以,记得点赞留言支持我们哈。感谢你的阅读,有问题请记得在下方留言噢~ |