技术干货Golang开发端口扫描器一

前言Golang是当前热门的语言之一,其拥有原生支持并发、对线程操作便捷、自身协程的轻量化等优点,使用其开发端口扫描器不仅开发过程高效,而且程序性能优秀。本文主要分享使用golang进行端口扫描器开发时可能涉及到的技术。

Golang简介

Go语言(或Golang)起源于年,并在年正式对外发布。Go是非常年轻的一门语言,它的主要目标是“兼具Python等动态语言的开发速度和C/C++等编译型语言的性能与安全性”。

Go语言是编程语言设计的又一次尝试,是对类C语言的重大改进,它不但能让你访问底层操作系统,还提供了强大的网络编程和并发编程支持。

Go语言的推出,旨在不损失应用程序性能的情况下降低代码的复杂性,具有“部署简单、并发性好、语言设计良好、执行性能好”等优势,目前国内诸多IT公司均已采用Go语言开发项目,其中包括Docker、Go-Ethereum、Thrraform和Kubernetes。

作为程序员,要开发出能充分利用硬件资源的应用程序是一件很难的事情。现代计算机都拥有多个核心,但是大部分编程语言都没有有效的工具让程序可以轻易利用这些资源。编程时需要写大量的线程同步代码来利用多个核,很容易导致错误。

Go语言正是在多核和网络化的时代背景下诞生的原生支持并发的编程语言。Go语言从底层原生支持并发,无需第三方库,开发人员可以很轻松地在编写程序时决定怎么使用CPU资源。

Go语言的并发是基于goroutine的,goroutine类似于线程,但并非线程。可以将goroutine理解为一种虚拟线程。Go语言运行时会参与调度goroutine,并将goroutine合理地分配到每个CPU中,最大限度地使用CPU性能。

多个goroutine中,Go语言使用通道(channel)进行通信,通道是一种内置的数据结构,可以让用户在不同的goroutine之间同步发送具有类型的消息。这让编程模型更倾向于在goroutine之间发送消息,而不是让多个goroutine争夺同一个数据的使用权。

并发技术

无论是扫描器还是各种高并发web服务,都少不了使用到并发技术,而Golang原生支持了这一特性,这也是Golang被广泛使用在各种高并发场景的原因。

Go语言的并发机制运用起来非常简便,在启动并发的方式上直接添加了语言级的关键字就可以实现,和其他编程语言相比更加轻量。

下面来介绍几个概念:

进程/线程

进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。

线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。

一个进程可以创建和撤销多个线程,同一个进程中的多个线程之间可以并发执行。

并发/并行

并发:把任务在不同的时间点交给处理器进行处理。在同一时间点,任务并不会同时运行。

并行:把每一个任务分配给每一个处理器独立完成。在同一时间点,任务一定是同时运行。

并发与并行并不相同,并发主要由切换时间片来实现“同时”运行,并行则是直接利用多核实现多线程的运行,Go程序可以设置使用核心数,以发挥多核计算机的能力。

协程/线程

协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。

线程:一个线程上可以跑多个协程,协程是轻量级的线程。

关于协程和线程的具体区别,可以参考线程和协程的区别的通俗说明一文。

Goroutine

Goroutine是Go语言中的轻量级线程实现,由Go运行时(runtime)管理。Go程序会智能地将goroutine中的任务合理地分配给每个CPU。

Go程序从main包的main()函数开始,在程序启动时,Go程序就会为main()函数创建一个默认的goroutine。

Goroutine的用法如下:

//go关键字放在方法调用前新建一个goroutine并执行方法体goGetThingDone(param1,param);//新建一个匿名方法并执行gofunc(param1,param){}(val1,val)//直接新建一个goroutine并在goroutine中执行代码块go{//dosometing...}

注:

a.使用go关键字创建goroutine时,被调用函数的返回值会被忽略

b.如果需要在goroutine中返回数据,可以使用channel把数据从goroutine中作为返回值传出

所有goroutine在main()函数结束时会一同结束。

runtime.GOMAXPROCS

在Go语言程序运行时(runtime)实现了一个小型的任务调度器。这套调度器的工作原理类似于操作系统调度线程,Go程序调度器可以高效地将CPU资源分配给每一个任务。传统逻辑中,开发者需要维护线程池中线程与CPU核心数量的对应关系。同样的,Go地中也可以通过runtime.GOMAXPROCS()函数做到,格式为:

runtime.GOMAXPROCS(逻辑CPU数量)

一般情况下,可以使用runtime.NumCPU()查询CPU数量,并使用runtime.GOMAXPROCS()函数进行设置,例如:

runtime.GOMAXPROCS(runtime.NumCPU())

Go1.5版本之前,默认使用的是单核心执行。从Go1.5版本开始,默认执行上面语句以便让代码并发执行。

Channel

一个channel是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。每个channel都有一个特殊的类型,也就是channels可发送数据的类型。

Go语言提倡使用通信的方法代替共享内存,当一个资源需要在goroutine之间共享时,通道在goroutine之间架起了一个管道,并提供了确保同步交换数据的机制。声明通道时,需要指定将要被共享的数据的类型。可以通过通道共享内置类型、命名类型、结构类型和引用类型的值或者指针。

在任何时候,同时只能有一个goroutine访问通道进行发送和获取数据。goroutine间通过通道就可以通信,通道像一个传送带或者队列,总是遵循先入先出(FirstInFirstOut)的规则,保证收发数据的顺序。

?声明通道类型

var通道变量chan通道类型//通道类型:通道内的数据类型。//通道变量:保存通道的变量。

?创建通道

通道实例:=make(chan数据类型)//数据类型:通道内传输的元素类型。//通道实例:通过make创建的通道句柄。eg:ch1:=make(chanint)//创建一个整型类型的通道ch:=make(chaninterface{})//创建一个空接口类型的通道,可以存放任意格式

?使用通道发送数据

通道变量-值//通道变量:通过make创建好的通道实例。//值:可以是变量、常量、表达式或者函数返回值等。值的类型必须与ch通道的元素类型一致。eg://创建一个空接口通道ch:=make(chaninterface{})//将0放入通道中ch-0//将hello字符串放入通道中ch-"hello"

把数据往通道中发送时,如果接收方一直都没有接收,那么发送操作将持续阻塞直至数据被接收。

?使用通道接收数据

a.阻塞接收数据

阻塞模式接收数据时,将接收变量作为-操作符的左值,格式如下:

data:=-ch

执行该语句时将会阻塞,直到接收到数据并赋值给data变量。

b.非阻塞接收数据

使用非阻塞方式从通道接收数据时,语句不会发生阻塞,格式如下:

data,ok:=-ch//data:表示接收到的数据。未接收到数据时,data为通道类型的零值。//ok:表示是否接收到数据。

非阻塞的通道接收方法可能造成高的CPU占用,因此使用非常少。如果需要实现接收超时检测,可以配合select和计时器channel进行。

c.接收任意数据,忽略接收的数据

阻塞接收数据后,忽略从通道返回的数据,格式如下:

-ch

执行该语句时将会发生阻塞,直到接收到数据,但接收到的数据会被忽略。这个方式实际上只是通过通道在goroutine间阻塞收发实现并发同步。

sync.WaitGroup

经常会看到以下代码:

funcmain(){fori:=0;i;i++{gofmt.Println(i)}time.Sleep(time.Second)}

为了等待所有goroutine运行完毕,不得不在程序的末尾使用time.Sleep()来睡眠一段时间,等待其他线程充分运行,对于简单的代码这样做既方便也简单,但是遇到复杂的代码,无法估计运行时间时,就不能使用time.Sleep来等待所有goroutine运行完成了。

可以考虑使用管道来完成上述操作:

funcmain(){c:=make(chanbool,)fori:=0;i;i++{gofunc(iint){fmt.Println(i)c-true}(i)}fori:=0;i;i++{-c}}

但是管道被设计出来不仅仅只是在这里用作简单的同步处理,假设我们有十万甚至更多的for循环,需要申请同样数量大小的管道出来,对内存是不小的开销。

sync.WaitGroup可以方便的解决这种情况:

funcmain(){wg:=sync.WaitGroup{}wg.Add()fori:=0;i;i++{gofunc(iint){fmt.Println(i)wg.Done()}(i)}wg.Wait()}

官方文档对WaitGroup的描述是:一个WaitGroup对象可以等待一组协程结束。

使用方法是:

1.main协程通过调用wg.Add(deltaint)设置worker协程的个数,然后创建worker协程;

.worker协程执行结束以后,都要调用wg.Done();

3.main协程调用wg.Wait()且被阻塞,直到所有worker协程全部执行结束后返回。

以端口扫描器中的代码片段为例:

//限制goroutine数量ch=make(chanbool,goroutineNum)funcrun(){//...for_,host:=rangeipList{for_,port:=rangeportList{ch-truewg.Add(1)goscan(host,port)}}wg.Wait()}funcscan(hoststring,portint){//...-chwg.Done()}

通信技术

Golang提供了官方库net为网络I/O提供了一个便携式接口,包括TCP/IP,UDP,域名解析和Unix域套接字。

端口扫描与主机存活探测主要用到tcp、udp、arp与icmp协议。

常用函数

funcDial

funcDial(network,addressstring)(Conn,error)//Dial连接到指定网络上的地址。eg:Dial("tcp","golang.org:


转载请注明:http://www.guyukameng.com/html/html1/15589.html

  • 上一篇文章:
  •   
  • 下一篇文章: 没有了