手把手教你用Go語言打造一款簡易TCP端口掃描器
前言
Hey,大家好呀,我是碼農(nóng),星期八。
這次呢, 咱們來實現(xiàn)一個簡單的TCP端口掃描器!
也來體驗一下黑客的風(fēng)采!
TCP掃描本質(zhì)
我們在使用TCP進行連接時,需要知道對方機器的ip:port
正常握手
連接成功的話,流程如下。

連接失敗
有正常,就有失敗,如果被連接方關(guān)閉的話,流程如下。

如果有防火墻
還有一種可能是,端口開放,但是防火墻攔截,流程如下。

代碼
本質(zhì)理解之后,就可以開始擼代碼了。
在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, "是關(guān)閉的")
 - continue
 - }
 - conn.Close()
 - fmt.Println(address, "打開")
 - }
 - }
 
執(zhí)行結(jié)果

但是這個過程是非常緩慢的。
因為net.Dial如果連接的是未開放的端口,一個端口可能就是20s+,所以,我們?yōu)槭裁磳W(xué)習(xí)多線程懂了把!!!
多線程版
上述是通過循環(huán)去一個個連接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"
 - //循環(huán)
 - 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, "是關(guān)閉的", err)
 - return
 - }
 - conn.Close()
 - fmt.Println(address, "打開")
 - }(j)
 - }
 - //等待wg
 - wg.Wait()
 - var elapseTime = time.Now().Sub(begin)
 - fmt.Println("耗時:", elapseTime)
 - }
 
執(zhí)行結(jié)果
其實是同時開啟了6W多個線程,去掃描每個ip:port。
所以耗時最長的線程結(jié)束的時間,就是程序結(jié)束的時間。
感覺還行,20s+掃描完6w多個端口!!!
線程池版
上面我們簡單粗暴的方式為每個ip:port都創(chuàng)建了一個協(xié)程。
雖然在Go中,理論上協(xié)程開個幾十萬個都沒問題,但是還是有一些壓力的。
所以我們應(yīng)該采用一種相對節(jié)約的方式進行精簡代碼,一般采用線程池方式。
本次使用的線程池包: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() {
 - //函數(shù)結(jié)束釋放連接
 - 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
 - }
 - //發(fā)送完關(guān)閉 addressChan 管道
 - close(addressChan)
 - }()
 - //啟動pool_size工人,處理addressChan種的每個地址
 - for work := 0; work < pool_size; work++ {
 - wg.Add(1)
 - pool.Submit(worker)
 - }
 - //等待結(jié)束
 - wg.Wait()
 - //計算時間
 - var elapseTime = time.Now().Sub(begin)
 - fmt.Println("耗時:", elapseTime)
 - }
 
執(zhí)行結(jié)果

我設(shè)置的線程池大小是7w個,所以也是一下子開啟6w多個協(xié)程的,但是我們已經(jīng)可以進行線程大小約束了。
假設(shè)現(xiàn)在有這樣的去求,有100個ip,需要掃描每個ip開放的端口,如果采用簡單粗暴開線程的方式.
那就是100+65535=6552300,600多w個線程,還是比較消耗內(nèi)存的,可能系統(tǒng)就會崩潰,如果采用線程池方式。
將線程池控制在50w個,或許情況就會好很多。
但是有一點的是,在Go中,線程池通常需要配合chan使用,可能需要不錯的基礎(chǔ)。
總結(jié)
本篇更偏向于樂趣篇,了解一下好玩的玩意。
其實還可以通過net.DialTimeout連接ip:port,這個可以設(shè)置超時時間,比如超時5s就判定端口未開放。
此處就不做舉例了。
咱們主要使用三種方式來實現(xiàn)功能。
- 正常版,沒有并發(fā),速度很慢。
 - 多協(xié)程版,并發(fā),性能很高,但是協(xié)程太多可能會崩潰。
 - 協(xié)程池版,并發(fā),性能高,協(xié)程數(shù)量可控。
 
通常情況下,如果基礎(chǔ)可以,更推薦使用協(xié)程池方式。
用微笑告訴別人,今天的我比昨天強,今后也一樣。

















 
 
 






 
 
 
 