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

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

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

代碼
本質(zhì)理解之后,就可以開(kāi)始擼代碼了。
在Go中,我們通常使用net.Dial進(jìn)行TCP連接。
它就兩種情況
- 成功:返回conn。
- 失敗:err != nil。
普通版
相對(duì)來(lái)說(shuō),剛開(kāi)始時(shí),我們可能都不是太膽大,都是先寫(xiě)原型,也不考慮性能。
代碼
- 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, "打開(kāi)")
- }
- }
執(zhí)行結(jié)果

但是這個(gè)過(guò)程是非常緩慢的。
因?yàn)閚et.Dial如果連接的是未開(kāi)放的端口,一個(gè)端口可能就是20s+,所以,我們?yōu)槭裁磳W(xué)習(xí)多線(xiàn)程懂了把!!!
多線(xiàn)程版
上述是通過(guò)循環(huán)去一個(gè)個(gè)連接ip:port的,那我們就知道了,在一個(gè)個(gè)連接的位置,讓多個(gè)人去干就好了。
所以,多線(xiàn)程如下。
代碼
- 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, "打開(kāi)")
- }(j)
- }
- //等待wg
- wg.Wait()
- var elapseTime = time.Now().Sub(begin)
- fmt.Println("耗時(shí):", elapseTime)
- }
執(zhí)行結(jié)果
其實(shí)是同時(shí)開(kāi)啟了6W多個(gè)線(xiàn)程,去掃描每個(gè)ip:port。
所以耗時(shí)最長(zhǎng)的線(xiàn)程結(jié)束的時(shí)間,就是程序結(jié)束的時(shí)間。
感覺(jué)還行,20s+掃描完6w多個(gè)端口!!!
線(xiàn)程池版
上面我們簡(jiǎn)單粗暴的方式為每個(gè)ip:port都創(chuàng)建了一個(gè)協(xié)程。
雖然在Go中,理論上協(xié)程開(kāi)個(gè)幾十萬(wàn)個(gè)都沒(méi)問(wèn)題,但是還是有一些壓力的。
所以我們應(yīng)該采用一種相對(duì)節(jié)約的方式進(jìn)行精簡(jiǎn)代碼,一般采用線(xiàn)程池方式。
本次使用的線(xiàn)程池包:gohive
地址:https://github.com/loveleshsharma/gohive
簡(jiǎn)單介紹

代碼
- package main
- //線(xiàn)程池方式
- 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"
- //線(xiàn)程池大小
- var pool_size = 70000
- var pool = gohive.NewFixedSizePool(pool_size)
- //拼接ip:端口
- //啟動(dòng)一個(gè)線(xiàn)程,用于生成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)
- }()
- //啟動(dòng)pool_size工人,處理addressChan種的每個(gè)地址
- for work := 0; work < pool_size; work++ {
- wg.Add(1)
- pool.Submit(worker)
- }
- //等待結(jié)束
- wg.Wait()
- //計(jì)算時(shí)間
- var elapseTime = time.Now().Sub(begin)
- fmt.Println("耗時(shí):", elapseTime)
- }
執(zhí)行結(jié)果

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



























