Golang socket编程

go socket

socket编程,即网络编程,自从tcp-socket诞生后,网络编程的架构也进行了几次变化。从一开始的 “一个进程”→“一个连接”,变成“一个线程”→“一个连接”,到现在主流的“Non-Block + I/O多路复用”。经过这几次的变化,应用程序拥有更好的处理性能,可以同时支持更多的连接,但是I/O多路复用给使用者带来了不小的复杂度,在golang里面,设计者将这些复杂性隐藏在 runtime 中,开发者只需在每个连接对应的 goroutine 中以“block I/O”的方式对待 socket 处理即可。说到底 socket 也只是操作系统抽象出来的,用来方便我们使用通信协议进行多主机间通讯的一种方式,下面分别介绍 tcp-socket,udp-socket,http-request、response的基本编程方法,首先是net包的相关方法:

方法作用
ResolveTCPAddr(network string,address string)解析IP地址和port,返回TCPAddr类型的指针,network=网络类型,例:tcp(默认)/tcp4/tcp6/udp/ip address=服务器IP/域名:端口
ResolveUDPAddr(network string,address string)作用、参数同上,返回UDPAddr类型的指针
ResolveIPAddr(network string,address string)作用、参数同上,返回IPAddr类型的指针
Listen(network string,address string)开启监听,手动设置监听的协议network=网络类型,例:tcp(默认)/tcp4/tcp6/udp/ip address=本机需要监听的 ip 和 port,如果为 nil 或者未指定 ip,则监听广播
ListenTCP(network string,address string)开启监听TCP连接,返回一个*TCPlistener类型数据
ListenUDP(network string,address string)开启监听UDP连接,返回一个*UDPConn类型数据
ListenIP(network string,address string)开启监听IP连接,返回一个*IPConn类型数据
Dial(network string,address string)客户端主动连接服务器,是对Dial系列方法的封装,network=网络类型 address=服务器地址(ip/域名:port)
DialTimeout(network string,address string,timeout time.Duration)在主动连接的基础上设置了超时时间,network、address同上,timeout=自定义超时时间(例:3*time.second)
DialTCP(network string, laddr *TCPAddr, raddr *TCPAddr)建立 TCP 连接,network=网络类型 laddr=本地地址,通常为nil raddr=目的地址(类型为TCPAddr的指针)
DialUDP(network string, laddr *UDPAddr, raddr *UDPAddr)建立 UDP连接,参数同上
DialIP(network string, laddr *IPAddr, raddr *IPAddr)建立ICMP连接,参数同上

net.Listen()系列【数据接收】:

方法作用
Accept()接收发送端的数据(不限定类型)
AcceptTCP()只接收TCP类型的发送端数据
Close()停止监听

net.Listen().Accept()系列【TCP数据处理】:

方法作用
Read(b []byte)实现 Read 接口,把接收到的信息放在切片里读取
ReadFrom(r Reader)和Read()相似,但是从对象中读取(例:文件对象)
Write(b []byte)实现 Write 接口,把要发送的信息通过切片写入
Close()关闭连接

net.Listen().Read()系列【UDP数据处理】:

方法作用
Read(b []byte)同TCP数据处理的Read(),用于从字节流读取数据
ReadFrom(r Reader)同TCP数据处理的ReadFrom(),用于从对象中读取数据
ReadFromUDP(b []byte)获取一个完整的UDP数据包,从中读取信息;若缓冲区设置过小,不足以容纳一个UDP包,会引起报错
ReadMsgUDP(b []byte,oob []byte)暂时不知道用处
Write(b []byte)同TCP数据处理的Write(),用于发送数据字节流

客户端连接建立后,返回一个 net.Conn 接口类型变量值,后续通过连接返回的变量值调用的 Write、Read 和 Close 方法的作用同服务端。

TCP

众所周知,tcp 建立连接要进行三次握手,因此 go-socket 也有对应的信息发送端(客户端)及接收端(服务端);
go-socketTCP 通信过程:

  1. 服务器开启监听
  2. 客户端与服务器建立连接
  3. 客户端发送信息
  4. 服务端接收到信息并处理

在这里插入图片描述
如图,每一个 socket 通信的步骤都有对应的方法实现,在这些底层方法上又有一个新的方法将它们封装,例如 Listen(),这个函数里面同时封装了监听 TCP 端口的方法 ListenTCP() 和监听 UDP 端口的方法 ListenUDP(),再往上则有一个 DialTCP() 函数将服务端和客户端除了读写和关闭连接外的所有功能都封装起来,也就是说 DialTCP() 是不分客户端和服务端的,两者通用。
下面是简单的例子:

客户端

客户端用 net.Dail 或 DialTimeout 建立连接,对应信息的发送方。

package sockApp
import (
	"log"
	"net"
)
func Connecter() () {
	//客户端主动连接,使用方法net.Dial()
	conn,err:=net.Dial("tcp4","127.0.0.1:8888")
	if err != nil {
		log.Printf("连接报错:%v",err)
		return
	}
	defer conn.Close()  //当函数调用结束时关闭连接
	conn.Write([]byte("Hello"))  //发送信息
}

服务端

服务端是标准的 listen+accept 结构,对应信息接收方(设置监听)

package sockApp
import (
	"log"
	"net"
)
func Listener() {
	//解析本地地址并开启监听
	addr,_:=net.ResolveTCPAddr("tcp4","127.0.0.1:8888")
	lis,_:=net.ListenTCP("tcp4",addr)
	log.Println("开始监听...")
	for {
		accept,_:=lis.Accept()  //接收数据
		//处理数据
		go func(){
			getByte:=make([]byte,1024)  //创建1024缓冲区的切片用于数据处理
			n,_:=accept.Read(getByte)
			log.Println(string(getByte[:n]))  //打印数据
			accept.Close()  //关闭连接
		}()
	}
}

UDP

UDP 通信就没有那么多讲究了,客户端和服务端之间并不会建立三次握手,而是客户端直接向服务端发送数据,但是在 go-socket 层面编程的步骤和tcp一样;
go-socketUDP 通信过程:

  1. 服务器开启监听端口
  2. 客户端发送信息
  3. 服务端接收信息并处理

在这里插入图片描述
和 TCP 端一样,这里也是每一个步骤对应一个底层方法,上面也有函数对这些方法进行封装。由于 UDP 通信面向无连接,因此多出一个 WriteToUDP() 方法,可以直接将数据发送到目标主机,省去了创建套接字的过程。DialUDP() 的作用和 DialTCP() 一样,可以用作客户端也可以用作服务端的套接字创建和监听端口。
下面是简单的例子:

客户端

客户端代码和TCP类似,用 Dial() 方法创建好套接字,然后直接调用 Write() 发送。

package sockApp
import (
	"log"
	"net"
)
func Connecter() {
	//客户端主动通信,使用方法net.Dial()
	conn,err:=net.Dial("udp4","127.0.0.1:8888")
	if err != nil {
		log.Printf("连接报错:%v",err)
		return
	}
	defer conn.Close()  //当函数调用结束时关闭连接
	conn.Write([]byte("Hello"))  //发送信息
	log.Println("信息发送完成...")
}

服务端

因为连接的不可靠性,UDP socket 并没有像 TCP 那样的 Accept() 方法,但其他部分和 TCP 几乎一模一样,因为这些函数可能同时封装了针对这两种协议的底层方法。

package sockApp
import (
	"log"
	"net"
)
func Listener() {
	//服务端监听端口
	log.Println("Socket Begin...")
	addr,_:=net.ResolveUDPAddr("udp4","127.0.0.1:8888")
	lis,_:=net.ListenUDP("udp4",addr)
	log.Println("开始监听...")
	for {
		//处理数据
		go func(){
			b:=make([]byte,1024)  //创建1024缓冲区的切片用于接收数据
			n,_,err:=lis.ReadFromUDP(b)  //接收数据
			if err != nil {
				log.Printf("数据接收报错:%v",err)
				return
			}
			log.Println(string(b[:n]))  //打印数据
		}()
	}
}

多线程应用&常见问题

Golang 是一种以高效的并发处理能力著称的语言,在程序运行的时候可以通过启动一个新的 goroutine 来处理相应任务,其返回结果放进通道进行传输。
在这里插入图片描述
如图,这是一个 socket 通信的实际应用场景,客户端 A 和 B 所在的两个内网是相互隔绝的,边界防火墙的 NAT 网络保护着他们同时也在节省 IP 地址。这种网络有个特点,就是外部请求内网主机的数据包在经过 NAT 网关的时候会因为没有保存过该地址而直接丢掉,并留下记录。相反,内网主机可以通过 NAT 网关主动与外网服务器建立 session 实现通信,这个 session 是临时的,只会存在几分钟到几小时不等,而且是定向的,只能是从 NAT 网关的某个端口到目标服务器的某个端口。那么现在想要穿过 NAT 网关的封锁访问内网主机 B,就需要两个条件:

  1. 主机 B 知道主机 A 的存在并且认为数据包可以到达;
  2. 主机 B 主动与外界建立连接;

于是就有了上图所示的内网穿透技术,首先客户端 A、B 主动向中转服务器发起连接,建立临时 session,然后由服务器向被访问方转发客户端 A 的探测包(源地址=A 的外网地址,目的地址=B 的外网地址),这个包会被客户端 B 所在内网的 NAT 网关丢掉,但是没关系,客户端 B 已经知道 A 是可到达的了,下一步客户端 B 就可以用这条记录上的地址作为目的地址发起访问,从而实现 客户端 A —— 客户端 B 的通信。这一步有个要注意的地方,如果在客户端 B 连接 客户端 A 的时候,A 的端口已经重新分配了,那么也是无法通信的。
下面实例将实现一个 P2P 连接,两端利用 UDP 协议通过一个中转服务器进行通信,实现内网穿透,并且在中转服务器和客户端上用多线程的技术处理信息传递和接收,达到提升效率的效果:
客户端代码:

package main

import (
	"fmt"
	"log"
	"net"
	"strconv"
	"strings"
	"time"
)
//发送心跳包,主动与外网中转服务器通信;
func heartbeat(local_sock *net.UDPAddr,server string) (connection *net.UDPConn){
	server_sock := &net.UDPAddr{IP: net.ParseIP(server),Port: 8888}
	conn,err_connect := net.DialUDP("udp4",local_sock,server_sock)
	if err_connect != nil{fmt.Printf("连接创建出错:%s\n",err_connect)}
	_,err_write := conn.Write([]byte("heartbeat"))
	if err_write != nil{
		fmt.Printf("信息发送出错:%s\n",err_write)
		return
	}
	return conn
}
//接收服务器发送的探测信息,然后关闭与中转服务器的连接;
//提取正文内容里的地址和端口(即另一个客户端的地址);
//返回一个创建好的套接字,目标地址为另一个内网客户端的地址;
func recv_info(conn *net.UDPConn) (address *net.UDPAddr){
	data := make([]byte,50)
	info,_,err_recv := conn.ReadFromUDP(data)
	if err_recv != nil{fmt.Printf("数据接收出错:%s\n",err_recv)}
	conn.Close()
	s := string(data[:info])
	addr := strings.Split(s,":")
	port,_ := strconv.Atoi(addr[1])
	dst_addr := &net.UDPAddr{IP: net.ParseIP(addr[0]),Port: port}
	return dst_addr
}
//向另一个内网客户端主动发送探测包,打通链路;
//后面发送信息时可换成tcp连接;
//使用多线程的异步执行特性实现同时收发信息;
func penetration(src *net.UDPAddr,dst *net.UDPAddr){
	udp_conn,err_create := net.DialUDP("udp4",src,dst)
	if err_create != nil{fmt.Printf("套接字创建出错:%s\n",err_create)}
	defer udp_conn.Close()
	_,err_survey := udp_conn.Write([]byte("hello"))
	if err_survey != nil{fmt.Printf("探测包发送失败:%s\n",err_survey)}
	log.Println("探测包发送成功...")
	time.Sleep(2*time.Second)
	fmt.Printf("接收到来自[%s:%d]的探测包\n" +
		"----------开始通信----------\n",dst.IP,dst.Port)

	msg := ""
	for{
		data := make([]byte,512)
		result,dst_addr,err := udp_conn.ReadFromUDP(data)
		if err != nil{
			fmt.Printf("信息接收出错:%s\n",err)
		} else if result == 0{break}
		fmt.Printf("[%s]>>>%s\n",dst_addr,data[:result])
		go func() {
			for{
				_,err_input:=fmt.Scanln(&msg)
				if err_input != nil{
					fmt.Println("-----警告:不允许出现空格!!!-----")
					continue
				} else if msg == "q"{break}
				_,err_sendMsg := udp_conn.Write([]byte(msg))
				if err_sendMsg != nil{fmt.Printf("信息发送失败:%s\n",err_sendMsg)}
			}
		}()
	}
}
//启动函数,传入中转服务器的地址;
func Run_client(server_ip string){
	localAddr := &net.UDPAddr{IP: net.IPv4zero,Port: 53}
	con := heartbeat(localAddr,server_ip)
	dst_addr := recv_info(con)
	penetration(localAddr,dst_addr)
}

中转服务器代码:

package main

import (
	"log"
	"net"
	"time"
)
//直接开启监听,套接字在参数中创建;
//返回一个*net.UDPConn对象;
func listener() (listen *net.UDPConn) {
	lis,err_connect := net.ListenUDP("udp4",&net.UDPAddr{
		IP: net.IPv4zero,
		Port: 8888,
	})
	if err_connect != nil{
		log.Printf("监听出错:%s",err_connect)
		return
	}
	return lis
}
//并发模式下接收心跳包,提取双方地址,经过通道按序添加进切片;
//当地址数量=2时就认为他们需要互相构建连接,开始转发地址;
//完成工作后退出,不影响两个客户端之间的通信;
func handler(listen *net.UDPConn){
	b := make([]byte,50)
	addr := make([]net.UDPAddr,0,2)
	ch := make(chan net.UDPAddr,30)
	for{
		go func(){
			_,address,err_recv := listen.ReadFromUDP(b)
			if err_recv != nil{log.Printf("数据接收出错:%s",err_recv)}
			ch <- *address
		}()
		addr = append(addr, <- ch)
		if len(addr) == 2 {
			log.Printf("开始建立连接:[%s]->[%s]\n", addr[0].String(), addr[1].String())
			listen.WriteToUDP([]byte(addr[1].String()), &addr[0])
			listen.WriteToUDP([]byte(addr[0].String()), &addr[1])
			log.Println("地址转发成功,中转服务器退出...")
			time.Sleep(3*time.Second)
			return
		}
	}
}
//启动函数;
func Run(){
	lis := listener()
	handler(lis)
}

go http

在 Golang 中,是通过标准库的 net/http 包实现 web 服务的,服务器每收到一个请求都会生成一个并生成一个goroutines来处理对应生成的conn连接,因此每个请求都是独立的,基本流程:
在这里插入图片描述
下面是 http 包相关的一些方法介绍,由于方法有点多,这里只介绍一部分常用的方法:

方法作用
NewRequest(method string,url string,body io.Reader)用于初始化请求对象,method=请求类型(例:GET) url=请求地址 body=请求内容(io.Reader类型)
NewRequestWithContext(ctx context.Context,method string,url string,body io.Reader)同样是初始化请求对象,参数同上,ctx=请求相关信息(要用到context包,用于控制请求的相关参数如goroutine的signal)
Get(url string)向目标网址发起get请求
Post(url string,contentType string,body io.Reader)向目标网址发起带参数的post请求,contentType=数据格式(例:application/json)
PostForm(url string,data url.Values)向目标网址提交数据,data=提交的表单数据内容(格式:{“key”: {“Value”}})
Head(url string)获取目标网址响应头信息
SetCookie(w ResponseWriter,cookie *Cookie)设置cookie,w=服务端的响应对象 cookie=http.Cookie{用户信息段}
ProxyURL(fixedURL *url.URL)设置代理,fixed为代理服务器地址
Client{Transport,CheckRedirect,Jar,Timeout}自定义客户端设置,Transport=指定执行http请求的运行机制(要实现http.RoundTripper接口,类似于NewRequest方法) CheckRedirect=指定处理重定向的策略(这是个自定义函数,策略一般是当请求为30*时,跳转到xxx页面) Jar=用于在Client中设定Cookie Timeout=超时时间
Server{…}自定义服务端设置,类似于Client,但是可设置参数更多,更复杂,在下面的服务端示例会详细说明
Header{map[string] [ ]string}是对http.Header.*系列方法的封装,用于直接设置请求头参数,简化了开发步骤
(*http.Client).Do(req *Request)发送自定义的请求,req=初始化的请求对象
HandleFunc(pattern string,handler func(ResponseWriter,*Resquest))自定义处理器,pattern=访问路径 handler=一个具有func(w http.ResponseWriter, r *http.Requests)签名的方法,用于定义响应的内容,返回 *Resquest对象
NewServeMux()用于创建一个ServeMux实例(自定义路由),这个实例也实现了ServeHTTP方法,因此也是一个Handler对象
ListenAndServe(addr string,handler Handler)使用指定的监听地址和处理器启动一个HTTP服务端,addr=访问路径 handler=分配的处理器函数名
ListenAndServeTLS(addr string,certFile string,keyFile string,handler Handler)作用同上,开启https服务,certFile=公钥证书 keyFile=对应私钥

http.Header系列【自定义请求头】:

方法作用
Add(key string,value string)添加请求头参数,如果存在则覆盖(例:Add(“contentType”,“application/json”))
Set(key string,value string)设置请求头参数,key必须是已存在的,参数同上
Get(key string)获取请求头参数,key=参数名
Del(key string)删除请求头参数,key=参数名

http.Body:

方法作用
Close()回收连接,放回连接池,以便复用

客户端

客户端可以构建请求,发送给 web 服务器,然后获取服务器的响应并处理,上面的方法可能看起来逻辑有些乱,其请求构造流程大致可以分为两条线:直接请求和构造请求。两者区别在于前者用默认封装好的参数发起请求,后者则是自定义请求,从定义请求头到定义客户端的其他属性,最后发送构造的请求。通过 Client{} 封装对象定义客户端属性,大大的简化了开发者构造请求的方式,同时又具备极大的灵活性。
在这里插入图片描述

下面是分别用直接请求和构造请求的例子:

package app
import (
	"bytes"
	"io"
	"net/http"
	"time"
)
/*
*这里只列出Get()请求
*Post()请求需要外加contentType、data等参数
*/
func HttpClient() string {
	client:=&http.Client{Timeout: 3*time.Second}  //自定义客户端属性
	resp,_:=client.Get("http://127.0.0.1:8888")  //发送Get()请求
	defer resp.Body.Close()  //函数运行结束后回收连接,等待复用
	//响应数据处理
	var buffer [512]byte
	result := bytes.NewBuffer(nil)
	for {
		n, err := resp.Body.Read(buffer[0:])
		result.Write(buffer[0:n])
		if err != nil && err == io.EOF {
			break
		} else if err != nil {
			panic(err)
		}
	}
	return result.String()
}
package app
import (
	"compress/gzip"
	"io"
	"io/ioutil"
	"net/http"
	"time"
)
/*
*用NewRequest构造请求并发送,NewRequest支持用Header.Add/Set方法自定义请求头,
*并且支持&http.Client设置客户端属性
*/
func HttpClient() (result string) {
	//初始化请求对象 req           https://www.baidu.com
	req,_:=http.NewRequest("GET","http://127.0.0.1:8888",nil)
	//自定义头信息
	req.Header.Add("User-Agent","Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:87.0) Gecko/20100101 Firefox/87.0")
	//自定义客户端其他属性
	client:=&http.Client{
		Transport:     nil,
		CheckRedirect: nil,
		Jar:           nil,
		Timeout:       3*time.Second,
	}
	resp,_:=client.Do(req)   //发送请求
	defer resp.Body.Close()  //等函数运行结束,回收连接,等待复用
	//响应数据处理
	var body io.ReadCloser
	if resp.Header.Get("Content-Encoding") == "gzip"{
		body,_ = gzip.NewReader(resp.Body)
	} else {
		body = resp.Body
	}
	reader,_ := ioutil.ReadAll(body)
	return string(reader)
}

服务端

服务端相当于一个 web 服务器,用于接受客户端发送过来的请求并做出响应。对于服务端,需要做的工作是接受来自客户端的请求并作出响应,这里引用一张流程图:
在这里插入图片描述
这是一个典型的http服务端处理流程,go http 服务端的处理流程也是如此,客户端发送的请求首先进入 router(路由),即 Multiplexer →路由为这个请求分配对应的 handler(处理器)→处理器对请求进行处理并作出 response(响应)。编程流程大致如下:
1.注册路由,提供url和handler方法的映射(不创建则使用默认路由)
2.实例化http.Server对象并开启监听
其中,http.Server{} 对象包含了所有的服务器属性,常用对象属性如下:

参数解释
Addr监听的服务器tcp地址(string)
Handler自定义处理器方法(Handler)
TLSConfig可以选择提供TLS配置,用于使用ServeTLS和ListenAndServeTLS(*tls.Config)
ReadTimeout读取请求的最长时间,包括正文(time.Duration)
ReadHeaderTimeout读取请求头的超时时间(time.Duration)
WriteTimeout写入响应之前的最大持续时间(time.Duration)
IdleTimeout等待下个请求的最长时间,读取超时的判断顺序:IdleTimeout→ReadTimeout→ReadHeaderTimeout(time.Duration)
MaxHeaderBytes请求头的最大字节数(int)
TLSNextProto可选地指定在发生NPN / ALPN协议升级时接管所提供的TLS连接的所有权的函数。 映射键是协商的协议名称(map[string]func(*Server, *tls.Conn, Handler))
ConnState指定在客户端连接更改状态时调用的可选回调函数(func(net.Conn, ConnState))
ErrorLog服务器日志信息(*log.Logger)
listeners记录所有监听net.Listener的信息(map[net.Listener]struct{})
activeConn记录所有处于active状态的连接(map[*conn]struct{})

下面是简单的服务端例子:

package app
import (
	"fmt"
	"net/http"
)
/*
*方式一:
*使用NewServeMunx、HandleFunc方法注册路由
*/
func HttpServer() {
	//注册路由 ServeMux
	mux:=http.NewServeMux()
	mux.HandleFunc("/",indexHandler)
	//开启监听本地8888端口
	http.ListenAndServe(":8888",mux)
}
//自定义处理函数 handler
func indexHandler(w http.ResponseWriter,req *http.Request) {
	//返回一个 html 页面
	w.Header().Set("content-Type","text/html")
	html:=`
	<!doctype html>
	<META http-equiv="Content-Type" content="text/html" charset="utf-8">
    <html lang="zh-CN">
		<head>
			<title>test</title>
		</head>
		<body>
			<h1>Hello World</h1>
		</body>
	</html>`
	fmt.Fprintf(w,html)
}
package app
import (
	"fmt"
	"net/http"
	"time"
)
/*
*方式二:
*通过直接设置 Server 对象的属性,开启服务,启用监听
*属性 Handler 的值必须实现 Handle 接口,因此可以用 HandleFunc/Handle 方法
*/
func HttpServer() {
	//设置服务器属性
	server:=http.Server{
		Addr: "127.0.0.1:8888",
		Handler: nil,  //注册路由,这里先使用默认路由,下面就可以注册多个自定义路由
		ReadTimeout: 3*time.Second,
		WriteTimeout: 3*time.Second,
	}
	http.HandleFunc("/",indexHandler)
	/*
	  http.HandleFunc("path2",&handlerTwo{})
	  http.HandleFunc("path3",&handlerThree{})
	  ...
	*/
	server.ListenAndServe()  //启动服务器,开始监听
}
//自定义处理函数
func indexHandler(w http.ResponseWriter,req *http.Request) {
	//返回一个 html 页面
	w.Header().Set("content-Type","text/html")
	html:=`<!doctype html>
	<META http-equiv="Content-Type" content="text/html" charset="utf-8">
    <html lang="zh-CN">
		<head>
			<title>test</title>
		</head>
		<body>
			<h1>Hello World</h1>
		</body>
	</html>`
	fmt.Fprintf(w,html)
}

多线程应用&常见问题

和上一个经过并发改造的实例一样,这个实例也用到了 goroutine,不同的是上个实例是在执行任务前直接创建一个新的 goroutine 直接使用,用完就关掉;这个实例则是在程序运行开始时初始化一个 goroutine 池,在并发处理上需要用到多少 goroutine 就从池子里取出对应数量的 goroutine 进行任务处理,执行完成后对其进行回收等待复用,这种管理方案的优势是只需要创建和关闭一次 goroutine,不像上面那样要一直重复着创建和关闭的操作,程序执行效率会大大的提升。
在这里插入图片描述
如图,为了改造成并发模式,一个 http 请求被切分成了发送请求、接收响应两部分,在构造请求之前通过读取文件获取多个目标以实现批量请求。从业务逻辑来看,为了获取目标进行逐行读取要花不少时间,所以这个功能可以通过并发完成提升效率;下一步则是从已读取到内存的目标进行请求的构造,然后发送,整个过程不需要阻塞等待,因此也可以通过并发直接处理,结果放进通道;在最后一步则不同,需要等待请求发送到服务器,并且要考虑服务器发出的响应包传输回来所用的时间,如果是直接以非阻塞形式并发的话会接收不到响应就结束任务了,这样会导致结果误判。所以我在响应处理上选择串行模式,通道里有一个响应包就处理一个,保证不会漏掉结果。到了这里之后还有个问题,并发处理是多任务同时运行下的统一管理,是无序的,那么在文件读取上可能会乱序,会影响到请求发送,从而影响到响应接收(比如说:序号1、2、3的请求发送出去,接收到响应的顺序是2、3、1)。解决办法很简单,在每次文件读取之后让 goroutine Sleep() 一小会就行了。
实例中用的 goroutine 池是一个第三方包,名字叫 ants,相关介绍可以直接去作者的 GitHub 上看→传送门,下面是基于上图编写的一个简单的并发请求实例:

package main
import (
	"bufio"
	"bytes"
	"compress/gzip"
	"encoding/json"
	"fmt"
	"github.com/panjf2000/ants/v2"
	"io"
	"io/ioutil"
	"math/rand"
	"net/http"
	url2 "net/url"
	"os"
	"regexp"
	"runtime"
	"strings"
	"sync"
	"time"
)

var UA=[]string{"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0",
	         "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0",
	         "Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0",
	         "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:85.0) Gecko/20100101 Firefox/85.0",
	         "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.7113.93 Safari/537.36",
	         "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.101 Safari/537.36",
	         "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.3538.77 Safari/537.36",
	         "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4346.0 Safari/537.36 Edg/89.0.731.0",
	         "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.50 Safari/537.36 Edg/88.0.705.29",
	         "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27"}
var wg sync.WaitGroup
//通过自定义结构体实现函数可选+默认参数
type reqOpt struct {
	Uri     string
	Method  string
	Proxies string
	Payload string
	Judge   string
}
func DefOpt() *reqOpt{
	return &reqOpt{
		Uri:     "",
		Method:  "GET",
		Proxies: "",
		Payload: "",
		Judge:   "",
	}
}
//本方法用于发起请求,通过对传入参数进行判断请求方式&是否带请求实体,
//可以是get/post请求,支持代理;
//post请求支持提交json/form表单数据,支持gzip压缩;
func SendReq(url string,option *reqOpt) (result *http.Response){
	data := make(chan io.Reader,512)
	proxy := make(chan http.RoundTripper,512)
	if url == ""{
		fmt.Println("URL为空,无法构造请求")
		return
	}
	if option.Payload != "" && strings.ToUpper(option.Method) == "POST"{
		//提交json数据    对应contentType = application/json
		p,_ := json.Marshal(option.Payload)
		payload := bytes.NewReader(p)
		data <- payload
		//提交表单数据     对应contentType = application/x-www-form-urlencoded
		//data <- strings.NewReader(option.Payload)
	} else {
		data <- nil
	}
	if option.Proxies != ""{
		proxy <- &http.Transport{Proxy: func(_ *http.Request) (*url2.URL,error){return url2.Parse(option.Proxies)}}
	} else {
		proxy <- nil
	}
	req,_ := http.NewRequest(strings.ToUpper(option.Method),url+"/"+option.Uri,<-data)
	req.Header.Add("Accept","text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
	req.Header.Add("Accept-Encoding","gzip")
	req.Header.Add("Accept-Language","zh-CN;zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2")
	req.Header.Add("Connection","keep-alive")
	if strings.ToUpper(option.Method) == "POST"{
		req.Header.Set("Connect-Type","application/json")//application/x-www-form-urlencoded
	}
	req.Header.Set("Host",regexp.MustCompile(`[\w\d]+\.[\w\d]+\.[\w]+`).FindString(url))
	req.Header.Set("User-Agent",UA[rand.Intn(len(UA))])
	client:=&http.Client{
		Transport:     <-proxy,
		CheckRedirect: nil,
		Jar:           nil,
		Timeout:       5*time.Second,
	}
	resp,err:=client.Do(req)
	if err!=nil{
		fmt.Printf("[-] %s/%v 请求发送失败!!!\n",url,option.Uri)
		return
	} else{
		fmt.Printf("[+] %s/%v 请求发送成功...\n",url,option.Uri)
		return resp
	}
}
//本方法用于接收响应并进行解析,通过返回状态码和option结构体传入的Judge进行结果判断;
func RecvResp(resp *http.Response,url string,option *reqOpt){
	if resp != nil && resp.Body != nil{
		defer resp.Body.Close()
	} else if resp == nil{
		fmt.Println("响应接收失败!!!")
		return
	}
	if resp.StatusCode == 200{
		var body io.ReadCloser
		if resp.Header.Get("Content-Encoding") == "gzip"{
			body,_ = gzip.NewReader(resp.Body)
		} else {
			body = resp.Body
		}
		reader,_ := ioutil.ReadAll(body)
		if strings.Contains(string(reader),option.Judge){
			fmt.Printf("[+] %s/%v 存在漏洞\n",url,option.Uri)
		}
	} else {
		fmt.Printf("[-] %s/%v 请求状态码:%v\n",url,option.Uri,resp.StatusCode)
	}
}
//读取的目标文件应存放在程序同目录下,
//通过并发读取文件到切片中,提升程序效率;
func ReadFile(fname string,pool *ants.Pool) []string{
	target := make([]string,1)
	var path string
	p,_ := os.Getwd()
	if runtime.GOOS == "windows"{
		path = p+"\\"+fname
	} else {path = p+"/"+fname}
	//打开文件
	f,err := os.Open(path)
	if err != nil{
		fmt.Println("文件打开出错:",err)
	}
	defer f.Close()
	buf := bufio.NewReader(f)
	reading := func(){
		for {
			line,err := buf.ReadString('\n')
			line = strings.TrimSpace(line)
			if err != nil && err != io.EOF{
				fmt.Println("文件读取出错:",err)
				break
			}
			i := 0
			target = append(target,line)//BUG:元素添加不当导致切片中有空元素
			i++
			if err == io.EOF{break}
		}
		wg.Done()
	}
	//i总数=读取的行数=使用的goroutine数量
	for i := 0; i<5;i++{
		wg.Add(1)
		_ = pool.Submit(reading)
		time.Sleep(1*time.Millisecond)
	}
	return target
}
//启动函数,设定了程序的运行流程,包括协程池的创建,
//为了方便,wg对象在全局进行实例化,
//程序工作流程:
//1.通过goroutine并发逐行读取文件内容并写入到切片中;
//2.通过goroutine并发构建+发送请求;
//3.主线程按序接收响应并处理;
func Run(filename string,o *reqOpt){
	respone := make(chan *http.Response,5120)
	defer ants.Release()
	pool,_ := ants.NewPool(100)
	tar := ReadFile(filename,pool)
	var url string
	fmt.Println(tar)
	for i:=1;i<len(tar);i++{
		url = tar[i]
		req := func(){
			respone <- SendReq(url,o)
			wg.Add(1)
		}
		_ = pool.Submit(req)
		RecvResp(<- respone,url,o)
	}
}
func main() {
	var name,method,payload,judge string
	for {
		fmt.Println("+----------------------------------+\n" +
						"|文件仅支持.txt格式                |\n" +
						"|仅支持GET/POST方法                |\n" +
						"|URL格式:http(s)://xxx.example.xxx|\n" +
						"|PROXY格式:http://host:port       |\n" +
						"+----------------------------------+")
		fmt.Printf("请输入文件名:")
		fmt.Scanln(&name)
		if name != regexp.MustCompile(`[\w\d]+\.[t][x][t]`).FindString(name){continue}
		fmt.Printf("请输入判断依据:")
		fmt.Scanln(&judge)
		fmt.Printf("请输入请求方式:")
		fmt.Scanln(&method)
		if strings.ToUpper(method) == "POST"{
			fmt.Printf("请输入payload:")
			fmt.Scanln(&payload)
			break
		} else if strings.ToUpper(method) == "GET"{
			break
		} else {continue}
	}
	opt := DefOpt()
	opt.Method = method
	opt.Payload = payload
	opt.Judge = judge
	fmt.Println("开始运行...")
	Run(name,opt)
}

如果有不对的地方,还请各位大佬指出 ^ _ ^


相关文章:
Socket 编程原理
Python socket编程
PHP socket编程
Java socket编程

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:数字20 设计师:CSDN官方博客 返回首页

打赏作者

B__

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值