GO语言实现简易的扫描器

发布于 2021-08-03  368 次阅读


(打卡学习go语言第二天OVO)

1. net包

想要实现一个端口扫描器,首先应该考虑的就是如何与目的ip的目的端口连接,golang中的net包就解决了这个问题。net 包 为网络 I/O 提供了一个便携式接口,包括 TCP/IP,UDP,域名解析和 Unix 域套接字。

虽然该软件包提供对低级网络原语的访问,但大多数客户端只需要 Dial,Listen 和 Accept 函数以及相关的 Conn 和 Listener 接口提供的基本接口。crypto/tls 包使用相同的接口和类似的 Dial 和 Listen 功能。我们这里使用的,就是net.Dial。

package main
import (
	"fmt"
	"net"
)

func main() {
	ip := "127.0.0.1"
	for port := 130; port < 140; port++ {
		conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", ip, port))
		if err != nil {
			fmt.Printf("%s的%d端口是关闭的\n", ip, port)
		} else {
			fmt.Printf("%s的%d端口是打开的\n", ip, port)
			conn.Close()
		}
	}
}

运行结果:

到这里我们就已经实现了最简单最基本的端口扫描功能,但是仅仅扫描了这10个端口就耗费了近10s的时间,所以这里我们还需要继续的优化。

2. Golang协程优化

提起Go语言就不得不提及其高并发的这一特性了。实现高并发,就不得不提一下goroutine了,即Go的协程。这里直接上一段代码可以更好的感受下

package main

import (
	"fmt"
)

func colors(col string) {
	for i := 0; i <= 5; i++ {
		fmt.Println(col)
	}
}

func main() {
	go colors("red")
	go colors("blue")
	go colors("orange")
}

go语言启动协程的关键词就是"go",后面通常可以跟上一个匿名函数或者是已定义的函数。

我们不难读出这段代码的意思,就是调用colors函数三次,每次调用都会输出五次对应传入的颜色。但是当我们保存、运行时却发现没有任何的输出,这是为什么呢???

所以这里就引出了go语言中协程的运行机制,协程是由程序主线程所创建,当主线程结束后协程无论执行到哪里都会被结束。了解了最基本的原理,我们在主函数最后让程序睡眠3s,修改的代码如下:

package main

import (
	"fmt"
	"time"
)

func colors(col string) {
	for i := 0; i <= 5; i++ {
		fmt.Println(col)
	}
}

func main() {
	go colors("red")
	go colors("blue")
	go colors("orange")
	time.Sleep(time.Second * 3)
}

看看运行结果:

可以发现这里输出时并不是按照我们写的从上到下的代码顺序执行的,这就说明多个协程是同时交替执行的。但是在实际场景中我们也不能确定最后这个主线程的“睡眠时间”,go的WaitGroup可以完美的解决这个问题,利用WaitGroup优化后的代码如下:

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func colors(col string) {
	for i := 0; i <= 5; i++ {
		fmt.Println(col)
	}
	wg.Done()
}

func main() {
	wg.Add(3)
	go colors("red")
	go colors("blue")
	go colors("orange")
	wg.Wait()
}

即先声明一个全局变量wg是sync.WaitGroup类型的,在主函数中wg.Add(3)向等待队列里添加3个协程标记,每当colors函数执行完成(协程执行完毕)就会wg.Done,也将等待队列里的协程标记减一。主函数最后的wg.Wait会等待所有协程结束后再结束主线程。当然这里主线程和协程间、协程和协程间还有比如管道技术等,用来进行数据的交互,这里就不展开细谈了。

回到最开始的问题,如何优化扫描器,这时答案就不辩自明了。我们可以利用go语言的特性同时创建成百上千甚至是上万个协程同时交替来执行扫描工作。

具体代码如下:

package main

import (
	"fmt"
	"net"
	"sort"
	"sync"
)

var ports []int

func main() {
	var wg sync.WaitGroup
	for port := 130; port <= 140; port++ {
		wg.Add(1)
		go func(port int) {
			ip := "127.0.0.1"
			conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", ip, port))
			if err != nil {
				fmt.Printf("%s的%d端口是关闭的\n", ip, port)
			} else {
				fmt.Printf("%s的%d端口是打开的\n", ip, port)
				ports = append(ports, port)
				conn.Close()
			}
			wg.Done()
		}(port)

	}
	wg.Wait()
	sort.Ints(ports)
	fmt.Printf("开放的端口有:%v", ports)
}

3. flag包

上面的扫描器关于ip和和port都是写定的,对将来的使用都有极大的不便,这里我们可以引入go语言的flag包,在命令行中可以改变一些参数。

1. flag.Type()

基本格式:flag.Type(flag名, 默认值, 帮助信息)*Type

例如:

name := flag.String("name", "张三", "姓名")
age := flag.Int("age", 18, "年龄")
married := flag.Bool("married", false, "婚否")
delay := flag.Duration("d", 0, "时间间隔")

此时的name、age、married、delay均为对应类型的指针。

2. flag.TypeVar()

基本格式:flag.TypeVar(Type指针, flag名, 默认值, 帮助信息)

例如:

var name string
var age int
var married bool
var delay time.Duration
flag.StringVar(&name, "name", "张三", "姓名")
flag.IntVar(&age, "age", 18, "年龄")
flag.BoolVar(&married, "married", false, "婚否")
flag.DurationVar(&delay, "d", 0, "时间间隔")

3. flag.Parse()

以上两个使用方式均需使用flag.Parse()来对命令行参数进行解析。

4. 使用flag包对扫描器进行优化

我们需要添加一些基本的命令行参数,例如:ip、端口范围等

package main

import (
	"flag"
	"fmt"
	"net"
	"sort"
	"sync"
)

var ports []int

func main() {
	hostname := flag.String("u", "", "taget address")
	startport := flag.Int("sp", 1, "start port")
	endport := flag.Int("ep", 1000, "end port")

	flag.Parse()
	var wg sync.WaitGroup
	for port := *startport; port <= *endport; port++ {
		wg.Add(1)
		go func(port int) {
			ip := *hostname
			conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", ip, port))
			if err != nil {
				//fmt.Printf("%s的%d端口是关闭的\n", ip, port)
			} else {
				//fmt.Printf("%s的%d端口是打开的\n", ip, port)
				ports = append(ports, port)
				conn.Close()
			}
			wg.Done()
		}(port)

	}
	wg.Wait()
	sort.Ints(ports)
	fmt.Printf("开放的端口有:%v", ports)
}

4. 新的功能。。。。


业精于勤,荒于嬉;行成于思,毁于随