余晖落尽暮晚霞,黄昏迟暮远山寻
本站
当前位置:网站首页 > 编程知识 > 正文

基于Go的多级正向代理服务器

xiyangw 2023-10-04 18:04 16 浏览 0 评论

前言:在实际项目中,经常会对一些非重要非紧急的数据采取网络爬虫手段抓取到本地,以达到节约成本的目的,但是站点对数据访问地来源有频率限制。很多码农会使用网络上免费的,多渠道的代理来解决频率限制的问题,由于是免费的,所以代理不是很稳定,这样导致每个工程需要花大量的时间和逻辑处理代理选择,失败重试的问题,并最终导致了应用代码的复杂性。本文采用多级代理的方法,第一级代理解决所有问题,用户只需要简单的使用第一级的代理。

基本思路:开发一个代理的代理模块,对应用层屏蔽掉上述问题。 下面是经验和学习的总结

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
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
}

相关推荐

华为交换机配置命令总结

1、配置文件相关命令[Quidway]displaycurrent-configuration显示当前生效的配置[Quidway]displaysaved-configuration显示fla...

解决账户无法登录的故障
解决账户无法登录的故障

在优化系统时错误地根据网上的提示,将唯一的Administrator账户设置为禁用,导致重启后无法进入系统。类似的故障还有使用组策略限制本地账户登录,导致重启后...

2023-10-11 17:16 xiyangw

S5720交换机登录提示初始密码存在安全风险
S5720交换机登录提示初始密码存在安全风险

问题描述客户每次登录输密码时,提示初始密码不安全,现在客户嫌麻烦想要去掉:Username:huaweiPassword:Warning:Theinitia...

2023-10-11 17:15 xiyangw

Springboot,Mybatis修改登录用户的密码
Springboot,Mybatis修改登录用户的密码

一、Mybatis.xml<updateid="changePassword"parameterType="string...

2023-10-11 17:15 xiyangw

PHP理论知识之沐浴更衣重看PHP基础(二)
PHP理论知识之沐浴更衣重看PHP基础(二)

接上篇,咱们继续讲解PHP基础八、标准PHP组件和框架的数量很多,随之产生的问题就是:单独开发的框架没有考虑到与其他框架的通信。这样对开发者和框架本身都是不利的...

2023-10-11 17:15 xiyangw

新鲜出炉UCloud云主机“数据方舟”评测报告(5)— — 关其城
新鲜出炉UCloud云主机“数据方舟”评测报告(5)— — 关其城

2015年10月29日,UCloud云主机黑科技——“数据方舟”功能正式上线,首轮内测随即开放。截止至2015年12月6日,我们共收到了534位用户的评测申...

2023-10-11 17:14 xiyangw

业余无线电Q简语及英文缩语
业余无线电Q简语及英文缩语

Q简语:语音通信及CW通信通用(加粗为常用)QRA电台何台QRB电台间之距离QRG告之正确频率QRH频率是否变动QRI发送音调QRJ能否收到QRK信号之可...

2023-10-11 17:14 xiyangw

非常详细!如何理解表格存储的多版本、生命周期和有效版本偏差
非常详细!如何理解表格存储的多版本、生命周期和有效版本偏差

表格存储在8月份推出了容量型实例,直接支持了表级别最大版本号和生命周期,高性能实例也将会在9月中旬支持这两个特性。那么,最大版本号和生命周期以及特有的...

2023-10-11 17:14 xiyangw

H3C交换机恢复出厂和各种基本配置,这20个要点你知道吗?
H3C交换机恢复出厂和各种基本配置,这20个要点你知道吗?

私信“干货”二字,即可领取138G伺服与机器人专属及电控资料!H3C交换机不知道密码如何恢复出厂设置1、开机启动,Ctrl+B进入bootrom菜单,选择恢复出...

2023-10-11 17:13 xiyangw

在使用移动支付系统的时候如何保护信息安全?

移动支付的方式近年来不断被更新,使得Venmo(据嘉丰瑞德理财师了解,此为美国的“支付宝”)之类的支付方式已经可以某种意义上代替随身携带现金了。但是你必须防范那些第三方应用程序轻松地获取你的银行卡以及...

界面控件DevExpress WinForms MVVM入门指南——登录表单(下)

从本文档中,您将了解如何向应用程序添加登录表单。在本节教程中着重讨论了如何实现此任务,这基本上是附加应用程序功能的一部分。DevExpressUniversalSubscription官方最新版免...

linux基础命令(一)
linux基础命令(一)

为啥要学linux?您可能熟悉WindowsXP、Windows7、Windows10和MacOSX等操作系统。Linux就是这样一种强大的操...

2023-10-11 17:13 xiyangw

MySQL数据库密码忘记了,怎么办?

#头条创作挑战赛#MySQL数据库密码忘记了且没有其他可以修改账号密码的账户时怎么办呢?登录MySQL,密码输入错误/*密码错误,报如下错误*/[root@TESTDB~]#mysql-u...

MobaXterm忘记Session密码,如何查看已保存的密码
MobaXterm忘记Session密码,如何查看已保存的密码

MobaXterm工具登录过SSH终端后,如果存储了Session(存储后再连接ssh的时候只需要输入账号不需要输入密码就可以直接连接上ssh),则可以...

2023-10-11 17:12 xiyangw

华为交换机密码丢失修改方法
华为交换机密码丢失修改方法

华为S2300交换机找回密码设置一、目的交换机的console和telnet密码丢失,无法登录设备。交换机已进行过数据配置,要把密码恢复而数据配置不能丢失。二、...

2023-10-11 17:12 xiyangw

取消回复欢迎 发表评论: