3分钟,手摸手教你用OpenResty搭建高性能隧道代理(附完整配置!)

经常写爬虫的小伙伴们对代理 IP 应该不会很陌生了吧?

通常,我们为了让爬虫更加稳定,一般我们都会去购买一些代理 IP 用在我们的爬虫服务上。常规的做法,我们一般会去某个代理网站上面购买服务,然后我们会得到一个获取代理 IP 的请求地址,之后我们再写一个请求去获取这些代理 IP。

一般来说,这些代理 IP 的有效期都不会太长,当然和你购买的套餐有一定的关系,常规来说,一般每个代理 IP 的有效期就只有 1-5分钟。我们还需要在爬虫应用程序中去维护这些代理 IP,可能我们的代码就会这样去写

go 复制代码
package main

import (
	"crypto/tls"
	"io"
	"net/http"
	"net/url"
	"time"
)

func main() {
   // 通过请求代理IP服务获得一些可用的代理 IP
   // ips := []string{"192.168.0.1:8080", "192.168.0.1:8081", "192.168.0.1:8082"}
   ips := fetchProxyIPs()
   proxyIP := ips[1]

	proxyUrl, err := url.Parse("http://"+proxyIP)
	if err != nil {
		panic(err)
	}
	tr := &http.Transport{
		Proxy:           http.ProxyURL(proxyUrl),
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
	}
	client := &http.Client{
		Transport: tr,
		Timeout:   15 * time.Second,
	}
	resp, err := client.Get("https://httpbin.org/ip")
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		panic("Failed to get a valid response")
	}
	content, err := io.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}
	println("Response:", string(content))

}

如果我们的爬虫程序只有一个,那么上面的代码完全没有啥问题。但是,如果我们的爬虫程序不止一个呢?是不是 fetchProxyIPs() 的代码逻辑就得复制粘贴多次? 如果哪天我想更换代理服务商岂不是还得一个一个的去改代码?

那么,有没有一种方式,可以在我设置代理 IP 的时候,就设置一个固定的 IP,然后这个固定的 IP 再帮我"自动"去使用代理 IP 呢?

是的,隧道代理就是干这事儿的。

在软件开发中,没有什么是不能通过加一层中间件来解决问题的,如果有,那么就再加一层......

可能,我们最终需要写的代码,就类似这样:

go 复制代码
package main

import (
	"crypto/tls"
	"io"
	"net/http"
	"net/url"
	"time"
)

func main() {
    // 只需要配置隧道代理地址,无需管理代理池
	proxyUrl, err := url.Parse("http://127.0.0.1:9527")
	if err != nil {
		panic(err)
	}
	tr := &http.Transport{
		Proxy:           http.ProxyURL(proxyUrl),
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
	}
	client := &http.Client{
		Transport: tr,
		Timeout:   15 * time.Second,
	}
	resp, err := client.Get("https://httpbin.org/ip")
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		panic("Failed to get a valid response")
	}
	content, err := io.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}
	println("Response:", string(content))

}

http://127.0.0.1:9527 服务就是我们设定的隧道代理,当我们通过 http://127.0.0.1:9527 去设置代理时,http://127.0.0.1:9527 会自动帮我们切换代理 IP。

现在有很多代理 IP 服务商都有提供隧道代理服务的,但是,价格一般都不会太便宜。感兴趣的小伙伴们可以去了解了解。

其实,自己动手搭建一个隧道代理服务也不会太复杂,用 go 写一个代理转发程序也是可以的,但是,在这个应用场景下,还有更好的选择:OpenResty

OpenResty 其实是 Nginx + Lua JIT。Nginx 本身就擅长处理 TCP 连接,性能高,稳定成熟。

有小伙伴这时候就说了,不太会 Lua 脚本怎么办?

没关系,这里我将整个配置都贴出来,以供各位参考:

nginx 复制代码
worker_processes  16;

error_log  /usr/local/openresty/nginx/logs/error.log debug;

events {
    worker_connections  1024;
}


stream {

    # 自定义 TCP 日志格式定义
    # 包含连接的 IP、时间、协议、状态、流量、会话时长、上游地址及流量等
    log_format tcp_proxy '$remote_addr [$time_local] '
                         '$protocol $status $bytes_sent $bytes_received '
                         '$session_time "$upstream_addr" '
                         '"$upstream_bytes_sent" "$upstream_bytes_received" "$upstream_connect_time"';
    # 启用日志记录到指定文件,并使用自定义格式
    access_log /usr/local/openresty/nginx/logs/tcp-access.log tcp_proxy;
    open_log_file_cache off;

    # TCP 代理配置
    # upstream 块中定义一个占位 server
    # 注意:0.0.0.0:1101 实际不会使用,真正地址会被 balancer_by_lua_block 动态覆盖
    upstream real_server {
        server 0.0.0.0:1101;

        # 使用 balancer_by_lua_block 动态设置后端目标主机和端口
        balancer_by_lua_block {
            -- 检查 preread 阶段是否已经设置了 proxy_host 和 proxy_port
            -- 从 ngx.ctx 中获取代理服务器地址
            if not ngx.ctx.proxy_host or not ngx.ctx.proxy_port then
                ngx.log(ngx.ERR, "====>proxy_host or proxy_port is not set in ngx.ctx<====")
                return
            end

            -- 初始化 balancer
            local balancer = require "ngx.balancer"
            local host = ""
            local port = 0

            -- 从上下文中提取目标 IP 和端口
            host = ngx.ctx.proxy_host
            port = ngx.ctx.proxy_port
            -- 设置代理服务器地址
            local ok, err = balancer.set_current_peer(host, port)
            if not ok then
                ngx.log(ngx.ERR, "====>failed to set current peer: " .. tostring(err) .. "<====")
                return
            end
        }
    }

    # 定义 TCP server 模块(stream)监听端口和代理逻辑
    server {
        # preread_by_lua_block 在客户端连接建立时就会触发,用于预处理逻辑
        preread_by_lua_block {
            -- https://github.com/openresty/lua-resty-redis
            local redis = require "resty.redis"
            local redis_instance = redis:new()

            -- 设置 Redis 操作超时时间(毫秒)
            redis_instance:set_timeout(5000)

            -- 一些 redis 连接配置
            local rdb_host = "192.168.1.208"
            local rdb_port = 6379
            local rdb_pwd = ""
            local rdb_db = 1
            -- 存放代理服务器地址的 zset 表名称
            local zset_table_name = "tunnel_proxy_pool"

            -- 连接到 Redis
            local ok, err = redis_instance:connect(rdb_host, rdb_port)
            if not ok then
                ngx.log(ngx.ERR, "====>failed to connect to Redis: [" .. tostring(ok) .. "] err msg ==> " .. tostring(err) .. "<====")
                return
            end

            -- 选择数据库
            local ok, err = redis_instance:select(rdb_db)
            if not ok then
                ngx.log(ngx.ERR, "====>failed to select Redis DB: [" .. tostring(ok) .. "] err msg ==> " .. tostring(err) .. "<====")
                return
            end

            -- 如果设置了密码,则进行认证
            if rdb_pwd and rdb_pwd ~= "" then
                local ok, err = redis_instance:auth(rdb_pwd)
                if not ok then
                    ngx.log(ngx.ERR, "====>failed to auth Redis: [" .. tostring(ok) .. "] err msg ==> " .. tostring(err) .. "<====")
                    return
                end
            end

            -- 先检查 zset 表是否存在或者是否有数据
            local hosts_count, err = redis_instance:zcard(zset_table_name)
            if not hosts_count or hosts_count <= 0 then
                ngx.log(ngx.ERR, "====>no available proxy servers in Redis zset table: " .. tostring(zset_table_name) .. " ==> " .. tostring(err) .. "<====")
                return
            end
            -- 获取分数最低的前 1 个代理服务器地址
            local res, err = redis_instance:zrange(zset_table_name, 0, 0, "WITHSCORES")
            if not res or #res == 0 then
                ngx.log(ngx.ERR, "====>failed to get proxy server from Redis zset table: " .. tostring(zset_table_name) .. "<====")
                return
            end
            -- 解析结果(假设之前存入 zset 的元素类似 127.0.0.1:8080、127.0.0.1:8181 分数为使用次数)
            local proxy_ip, proxy_port = res[1]:match("([^:]+):(%d+)")
            if not proxy_ip or not proxy_port then
                ngx.log(ngx.ERR, "====>failed to parse proxy server address ==> " .. tostring(res[1]) .. "<====")
                return
            end
            -- 获取了当前代理服务器地址后,给其分数加 1,表示当前已经使用过一次
            local ok, err = redis_instance:zincrby(zset_table_name, 1, res[1])
            if not ok then
                ngx.log(ngx.ERR, "====>failed to increment proxy server score in Redis zset table: " .. tostring(zset_table_name) .. " ==> " .. tostring(err) .. "<====")
                return
            end

            -- 将获取到的代理服务器地址存入 ngx.ctx 中,供 balancer_by_lua_block 使用
            ngx.ctx.proxy_host = proxy_ip
            ngx.ctx.proxy_port = tonumber(proxy_port)
            ngx.log(ngx.INFO, "====>using proxy server ==> " .. tostring(proxy_ip) .. ":" .. tostring(proxy_port) .. "<====")

            -- 释放 Redis 连接,否则连接池将保留不完整的连接状态
            ok, err = redis_instance:set_keepalive(10000, 100)
            if not ok then
                ngx.log(ngx.ERR, "====>failed to set Redis keepalive: " .. tostring(err) .. "<====")
            end
        }

        # 对外暴露的监听端口
        listen 0.0.0.0:9527;
        # 设置代理的目标 upstream 名称
        proxy_pass real_server;
        proxy_connect_timeout 5s;
        proxy_timeout 15s;
    }

}

以上,其实我们就是借用 OpenResty 做了一层代理转发,你可以结合流程图来看看

那么,如何部署 OpenResty 呢?

可以直接使用下面的 docker-compose.yaml 文件:

yaml 复制代码
services:
  openresty:
    container_name: openresty_server
    image: openresty/openresty:1.25.3.2-5-centos7
    ports:
      - "9527:9527"
    volumes:
      - ./conf/tunnel_proxy_redis.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro
      - ./logs:/usr/local/openresty/nginx/logs

文件写好之后,直接在和 docker-compose.yaml 文件同级目录下执行 docker-compose up 即可启动 OpenResty 服务。

另外,还忘记说了一点:你需要自己写一个脚本,定时将可用的代理 IP 同步到 redis 中,上面的 Lua 脚本只是会从 redis 中取出可用的代理 IP 进行转发。

自动脚本干的活儿类似写入这样的数据

bash 复制代码
zadd tunnel_proxy_pool 0 127.0.0.1:9001 0 4127.0.0.1:9002 0 127.0.0.1:9003

大家感兴趣的,可以通过访问 https://github.com/pudongping/tunnel-proxy 获得源码。

相关推荐
序安InToo18 分钟前
第6课|注释与代码风格
后端·操作系统·嵌入式
xyy12318 分钟前
C#: Newtonsoft.Json 到 System.Text.Json 迁移避坑指南
后端
洋洋技术笔记21 分钟前
Spring Boot Web MVC配置详解
spring boot·后端
JxWang0521 分钟前
VS Code 配置 Markdown 环境
后端
navms24 分钟前
搞懂线程池,先把 Worker 机制啃明白
后端
JxWang0525 分钟前
离线数仓的优化及重构
后端
Nyarlathotep011326 分钟前
gin01:初探gin的启动
后端·go
JxWang0526 分钟前
安卓手机配置通用多屏协同及自动化脚本
后端
JxWang0527 分钟前
Windows Terminal 配置 oh-my-posh
后端
SimonKing43 分钟前
OpenCode AI编程助手如何添加Skills,优化项目!
java·后端·程序员