手把手教你用Go语言打造一款简易TCP端口扫描器
回复“Go语言”即可获赠从入门到进阶共10本电子书
含光混世贵无名,何用孤高比云月?
前言
Hey,大家好呀,我是码农,星期八。
这次呢, 咱们来实现一个简单的TCP端口扫描器!
也来体验一下黑客的风采!
TCP扫描本质
我们在使用TCP进行连接时,需要知道对方机器的ip:port
正常握手
连接成功的话,流程如下。
连接失败
有正常,就有失败,如果被连接方关闭的话,流程如下。
如果有防火墙
还有一种可能是,端口开放,但是防火墙拦截,流程如下。
代码
本质理解之后,就可以开始撸代码了。
在Go中,我们通常使用net.Dial
进行TCP连接。
它就两种情况
成功:返回conn。
失败:
err != nil
。
普通版
相对来说,刚开始时,我们可能都不是太胆大,都是先写原型,也不考虑性能。
代码
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就判定端口未开放。
此处就不做举例了。
咱们主要使用三种方式来实现功能。
正常版,没有并发,速度很慢。
多协程版,并发,性能很高,但是协程太多可能会崩溃。
协程池版,并发,性能高,协程数量可控。
通常情况下,如果基础可以,更推荐使用协程池方式。
如果在操作过程中有任何问题,记得下面留言,我们看到会第一时间解决问题。
用微笑告诉别人,今天的我比昨天强,今后也一样。
我是码农星期八,如果觉得还不错,记得动手点赞一下哈。
感谢你的观看。
如果你觉得文章还可以,记得点赞留言支持我们哈。感谢你的阅读,有问题请记得在下方留言噢~
想学习更多关于Python的知识,可以参考学习网址:http://pdcfighting.com/,点击阅读原文,可以直达噢~
想了解更多关于Go的知识,请前往:http://pdcfighting.com/
------------------- End -------------------
往期精彩文章推荐:
欢迎大家点赞,留言,转发,转载,感谢大家的相伴与支持
想加入Go学习群请在后台回复【入群】
万水千山总是情,点个【在看】行不行