前言:在实际项目中,经常会对一些非重要非紧急的数据采取网络爬虫手段抓取到本地,以达到节约成本的目的,但是站点对数据访问地来源有频率限制。很多码农会使用网络上免费的,多渠道的代理来解决频率限制的问题,由于是免费的,所以代理不是很稳定,这样导致每个工程需要花大量的时间和逻辑处理代理选择,失败重试的问题,并最终导致了应用代码的复杂性。本文采用多级代理的方法,第一级代理解决所有问题,用户只需要简单的使用第一级的代理。
基本思路:开发一个代理的代理模块,对应用层屏蔽掉上述问题。 下面是经验和学习的总结
1、正向代理转发原理
理解正向代理与反向代理原理的区别是快速编码的关键
正向代理是知道真正的目标服务器,而反向代理是不知道的,以为代理服务器就是真正的目标服务器
2、Go语言TCP Socket编程
Go语言的传输层编程代码非常的简单,一行代码就搞定。下面就是创建了一个端口监听,从传输层层面接受来自网络上的各种请求。
net.Listen("tcp", ":7856")
3、TCP协议解析
proxyUrl指向上面创建的监听地址,格式:http://ip:port
proxy, _ := url.Parse(proxyUrl)
tr := &http.Transport{
Proxy: http.ProxyURL(proxy),
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
}
client := &http.Client{
Transport: tr,
Timeout: 10 * time.Second,
}
......
client.Do(req)
client.Do就会发起http的连接请求,这个过程中有两个步骤:
1、与代理服务器建立TCP的连接。
2、发送HTTP连接请求(见 HTTP协议解析)
三次握手,Wireshark抓包如下:
886 47.168484 192.168.0.105 X.X.X.X TCP 78 50707 → 7856[SYN] Seq=0 Win=65535 Len=0 MSS=1460 WS=64 TSval=104140878 TSecr=0 SACK_PERM=1
887 47.187710 X.X.X.X 192.168.0.105 TCP 74 7856 → 50707 [SYN, ACK] Seq=0 Ack=1 Win=28960 Len=0 MSS=1412 SACK_PERM=1 TSval=822200229 TSecr=104140878 WS=128
888 47.187803 192.168.0.105 X.X.X.X TCP 66 50707 → 7856 [ACK] Seq=1 Ack=1 Win=131584 Len=0 TSval=104140896 TSecr=822200229
至此,TCP通道已经打通(路基已经打好,就等着修什么样的道路)
3、HTTP协议解析
TCP/IP协议族里面的传输层通道已经打通,下面就是打通HTTP应用层的初始化包(路基在上一步打好,现在我们铺设的是跑卡车的道路)。
889 47.188292 192.168.0.105 120.24.69.155 HTTP 159 CONNECT w.mmm920.com:443 HTTP/1.1
893 53.023565 120.24.69.155 192.168.0.105 HTTP 105 HTTP/1.1 200 Connection established
4、HTTP代理服务端逻辑
客户端到HTTP的连接建立只有两步:
- 客户端发起
CONNECT w.XXXX.com:443 HTTP/1.1
请求 - 服务器端判断是CONNECT连接,返回
HTTP/1.1 200 Connection established\r\n\r\n
数据包
至此,客户端到HTTP代理的连接建立成功
if method == "CONNECT" {
fmt.Fprint(client, "HTTP/1.1 200 Connection established\r\n\r\n")
} else {
log.Println("server write", method) //其它协议
server.Write(b[:n])
}
代理服务器到其它网络上代理的连接逻辑:
c, err := net.DialTimeout("tcp", remote proxy addr, time.Second*5)
req, err := http.NewRequest(http.MethodConnect, reqURL.String(), nil)
req.Write(c)
resp, err := http.ReadResponse(bufio.NewReader(c), req)
if resp.StatusCode != 200 {
err = fmt.Errorf("Connect server using proxy error, StatusCode [%d]", resp.StatusCode)
return nil, err
}
通过HTTP的CONNECT协议判断代理服务器是否当前可用,获得net.Conn管道c
5、TCP连接池的研究
进阶篇
在第一级代理服务器和第二级代理服务器之间的传输层通道可以考虑使用TCP连接池。因为第二级代理的服务器都是网络上免费的代理,建立连接的成本比较高、也不稳定,因此一旦建立连接后,应该立刻复用。同时带来的一些风险也需要考虑,连接池的维护,对远程代理服务器的压力等
6、Wireshark工具使用
Wireshark是个好东西,特别是问题排查和TCP/IP协议分析学习的时候非常方便
7、Go代码
代码非常的简洁,短短200行代码,就实现了多级代理的功能,而且对于学习TCP/IP协议和HTTP协议连接过程非常的简单明了。
涉及到机密信息,所以省去了refreshProxyAddr的逻辑。refreshProxyAddr就是更新代理ip地址池的逻辑,如测试用的话,可以手动设置几个ip,格式如:proxyUrls["http://x.x.x.x:3128"]=''
package main
import (
"bufio"
"bytes"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"os"
"runtime/debug"
"strings"
"sync"
"time"
"github.com/robfig/cron"
)
var proxyUrls map[string]string = make(map[string]string)
var choiseURL string
var mu sync.Mutex
var connHold map[string]net.Conn = make(map[string]net.Conn) //map[代理服务器url]tcp连接
func init() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
refreshProxyAddr()
cronTask := cron.New()
cronTask.AddFunc("@every 1h", func() {
mu.Lock()
defer mu.Unlock()
refreshProxyAddr()
})
cronTask.Start()
}
func main() {
l, err := net.Listen("tcp", ":7856")
if err != nil {
log.Panic(err)
}
for {
client, err := l.Accept()
if err != nil {
log.Panic(err)
}
go handle(client)
}
}
func handle(client net.Conn) {
defer func() {
if err := recover(); err != nil {
log.Println(err)
debug.PrintStack()
}
}()
if client == nil {
return
}
log.Println("client tcp tunnel connection:", client.LocalAddr().String(), "->", client.RemoteAddr().String())
// client.SetDeadline(time.Now().Add(time.Duration(10) * time.Second))
defer client.Close()
var b [1024]byte
n, err := client.Read(b[:]) //读取应用层的所有数据
if err != nil || bytes.IndexByte(b[:], '\n') == -1 {
log.Println(err) //传输层的连接是没有应用层的内容 比如:net.Dial()
return
}
var method, host, address string
fmt.Sscanf(string(b[:bytes.IndexByte(b[:], '\n')]), "%s%s", &method, &host)
log.Println(method, host)
hostPortURL, err := url.Parse(host)
if err != nil {
log.Println(err)
return
}
if hostPortURL.Opaque == "443" { //https访问
address = hostPortURL.Scheme + ":443"
} else { //http访问
if strings.Index(hostPortURL.Host, ":") == -1 { //host不带端口, 默认80
address = hostPortURL.Host + ":80"
} else {
address = hostPortURL.Host
}
}
server, err := Dial("tcp", address)
if err != nil {
log.Println(err)
return
}
//在应用层完成数据转发后,关闭传输层的通道
defer server.Close()
log.Println("server tcp tunnel connection:", server.LocalAddr().String(), "->", server.RemoteAddr().String())
// server.SetDeadline(time.Now().Add(time.Duration(10) * time.Second))
if method == "CONNECT" {
fmt.Fprint(client, "HTTP/1.1 200 Connection established\r\n\r\n")
} else {
log.Println("server write", method) //其它协议
server.Write(b[:n])
}
//进行转发
go func() {
io.Copy(server, client)
}()
io.Copy(client, server) //阻塞转发
}
//refreshProxyAddr 刷新代理ip
func refreshProxyAddr() {
var proxyUrlsTmp map[string]string = make(map[string]string)
\\获取代理ip地址逻辑
proxyUrls = proxyUrlsTmp //可以手动设置测试代理ip
}
//DialSimple 直接通过发送数据报与二级代理服务器建立连接
func DialSimple(network, addr string) (net.Conn, error) {
var proxyAddr string
for proxyAddr = range proxyUrls { //随机获取一个代理地址
break
}
c, err := func() (net.Conn, error) {
u, _ := url.Parse(proxyAddr)
log.Println("代理host", u.Host)
// Dial and create client connection.
c, err := net.DialTimeout("tcp", u.Host, time.Second*5)
if err != nil {
log.Println(err)
return nil, err
}
_, err = c.Write([]byte("CONNECT w.xxxx.com:443 HTTP/1.1\r\n Host: w.xxxx.com:443\r\n User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.3\r\n\r\n"))// w.xxxx.com:443 替换成实际的地址
if err != nil {
panic(err)
}
c.Write([]byte(`GET www.baidu.com HTTP/1.1\r\n\r\n`))
io.Copy(os.Stdout, c)
return c, err
}()
return c, err
}
//Dial 建立一个传输通道
func Dial(network, addr string) (net.Conn, error) {
var proxyAddr string
for proxyAddr = range proxyUrls { //随机获取一个代理地址
break
}
//建立到代理服务器的传输层通道
c, err := func() (net.Conn, error) {
u, _ := url.Parse(proxyAddr)
log.Println("代理地址", u.Host)
// Dial and create client connection.
c, err := net.DialTimeout("tcp", u.Host, time.Second*5)
if err != nil {
return nil, err
}
reqURL, err := url.Parse("http://" + addr)
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodConnect, reqURL.String(), nil)
if err != nil {
return nil, err
}
req.Close = false
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.3")
err = req.Write(c)
if err != nil {
return nil, err
}
resp, err := http.ReadResponse(bufio.NewReader(c), req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
log.Println(resp.StatusCode, resp.Status, resp.Proto, resp.Header)
if resp.StatusCode != 200 {
err = fmt.Errorf("Connect server using proxy error, StatusCode [%d]", resp.StatusCode)
return nil, err
}
return c, err
}()
if c == nil || err != nil { //代理异常
log.Println("代理异常:", c, err)
log.Println("本地直接转发:", c, err)
return net.Dial(network, addr)
}
log.Println("代理正常,tunnel信息", c.LocalAddr().String(), "->", c.RemoteAddr().String())
return c, err
}