【使用Python临时搭建代理转发服务,内网穿透】

前置资源

  • 服务器一台,安装Nginx
  • 域名一个,并解析到服务器
  • 本地电脑一台

工作流程

请求:浏览器访问 域名 + 隧道 + Path 的方式,将请求转发到Server,Server 转发到 Client,并通过Client 请求APP接口;

响应:在获取APP返回结果后,Client将Response返回给Server,Server返回给浏览器/客户端。

  • Server端:运行在服务器上,通过nginx反向代理,通过域名访问
  • Client端:运行在和APP服务一台机器上,指向本地app域名
  • APP: 提供接口服务

前置操作

  1. 服务器开放 8080 端口防火墙
  2. 安全组添加 8080 入流量规则

Nginx 配置

nginx.conf

shell 复制代码
#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;

events {
    worker_connections  1024;
    # 开启多路复用,提升并发
    use epoll;
    multi_accept on;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    # 定义 main 日志格式(必须在 http 块内)
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  logs/access.log  main;
    error_log   logs/error.log  warn;

    sendfile        on;
    tcp_nopush     on;  # 开启:提升静态资源传输效率
    tcp_nodelay    on;  # 核心:禁用Nagle算法,降低小数据包延迟

    # 长连接配置(解决Postman慢的关键)
    keepalive_timeout  300s;  # 延长到300秒,默认65秒太短
    keepalive_requests 10000; # 单个长连接可处理的请求数(默认100)
    keepalive_disable msie6;  # 仅禁用IE6的长连接

    # 开启gzip压缩(提升传输速度)
    gzip on;
    gzip_min_length 1k;
    gzip_buffers 4 16k;
    gzip_http_version 1.1;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }

    # ========== 全局 SSL 优化配置(统一SSL缓存大小,避免冲突) ==========
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    ssl_session_cache shared:SSL:10m;  # 统一设置为10m,避免重复定义
    ssl_session_timeout 10m;           # SSL会话超时

    # ========== 80端口:HTTP 重定向到 HTTPS ==========
    server {
        listen       80;
        listen       [::]:80;  # 适配IPv6
        server_name  ${你的域名} www.${你的域名};
        access_log  logs/host.access.log  main;

        # 所有HTTP请求重定向到HTTPS
        return 301 https://$host$request_uri;

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }

    # ========== 443端口:HTTPS + 代理配置(核心) ==========
    server {
        listen       443 ssl;
        listen       [::]:443 ssl;  # 适配IPv6
        server_name  ${你的域名} www.${你的域名};

        access_log  logs/${你的域名}_access.log  main;
        error_log   logs/${你的域名}_error.log  warn;

        # SSL证书配置
        ssl_certificate      /etc/letsencrypt/live/${你的域名}/fullchain.pem;
        ssl_certificate_key  /etc/letsencrypt/live/${你的域名}/privkey.pem;

        # 移除重复的ssl_session_cache,使用全局配置
        ssl_ciphers  HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers  on;

        # 根路径(静态页面,可选)
        location / {
            root   html;
            index  index.html index.htm;
            expires 1d;  # 静态资源缓存1天
        }

        # ========== /proxy/ 代理配置(核心优化) ==========
        location /proxy/ {
            # 转发到本地8080端口
            proxy_pass http://127.0.0.1:8080/;

            # 基础转发配置
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            # WebSocket 核心配置
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection $connection_upgrade;

            # ========== 解决断连+慢的核心配置 ==========
            # 超时配置(避免Nginx主动断开连接)
            proxy_connect_timeout 60s;       # 连接超时
            proxy_send_timeout 60s;          # 发送超时
            proxy_read_timeout 300s;         # 读取超时(适配WebSocket长连接)
            # 缓冲区配置(解决数据阻塞)
            proxy_buffer_size 64k;           # 单个缓冲区大小
            proxy_buffers 4 64k;             # 缓冲区数量和大小
            proxy_busy_buffers_size 128k;    # 忙时缓冲区大小
            # 稳定性配置
            proxy_ignore_client_abort on;    # 忽略客户端主动断开(Postman断连不报错)
            proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
            # 强制开启长连接(解决Postman慢)
            proxy_set_header Connection "";  # HTTP/1.1长连接标识
        }

        # 错误页面配置
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}
[

Server

安装依赖

Python 3.8.13

powershell 复制代码
# 下载miniconda安装包
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
chmod +x Miniconda3-latest-Linux-x86_64.sh
# 安装miniconda
bash Miniconda3-latest-Linux-x86_64.sh
# 使配置生效
source ~/.bashrc
# 查看conda 版本
conda --version

# 创建Python3.8环境
conda create -n py38 python=3.8 -y
# 进入Python3.8环境
conda activate py38
# 安装依赖
pip install  eventlet==0.39.1 Flask==2.2.5  Flask-SocketIO==5.3.6  greenlet==3.1.1  python-socketio==5.16.1  requests==2.32.4

server 配置

config.ini

bash 复制代码
[server]
# 服务仅允许本机访问,测试阶段可使用 0.0.0.0
HOST = 127.0.0.1
PORT = 8080

[auth]
ALLOWED_AUTH = {"test": "123456@ASDFG"}

[security]
# ip白名单,配置可访问的ip地址或号段,测试阶段可配置 "0.0.0.0/0"全部ipv4地址,"::/0" 全部ipv6地址
# 允许启动client的ip白名单
CLIENT_IP_WHITELIST = ["223.0.0.0/32"]
# 允许浏览器/Postman等HTTP访问用户的IP白名单
WEB_IP_WHITELIST = ["223.0.0.0.0/32"]

[timeout]
GET_TIMEOUT = 5
POST_TIMEOUT = 5
RECV_TIMEOUT = 10.0
HEARTBEAT_INTERVAL = 10
HEARTBEAT_TIMEOUT = 30

[network]
BUFFER_SIZE = 16384

[logging]
# 日志级别:DEBUG/INFO/WARNING/ERROR/CRITICAL
LOG_LEVEL = DEBUG
# 日志文件路径
LOG_FILE = proxy_server.log
# 单个日志文件最大大小(MB)
LOG_MAX_BYTES = 10
# 日志文件备份数量
LOG_BACKUP_COUNT = 5
# 日志编码
LOG_ENCODING = utf-8
# 日志格式
LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
# 时间格式
LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
# 是否启用控制台日志
CONSOLE_LOG_ENABLE = True
# 控制台日志级别
CONSOLE_LOG_LEVEL = DEBUG

server.py 文件内容

python 复制代码
#! /usr/bin/python3
# -*- coding: utf-8 -*-
import socket
import threading
import time
import json
import uuid
import base64
import ipaddress
import hashlib
import logging
from logging.handlers import RotatingFileHandler
from collections import defaultdict
import select
import errno
import configparser
import ast
import os


def load_config(config_path='config.ini'):
    """
    加载配置文件,返回配置字典(包含日志配置)

    Args:
        config_path: 配置文件路径

    Returns:
        dict: 包含所有配置项的字典

    Raises:
        FileNotFoundError: 配置文件不存在
        configparser.Error: 配置文件格式错误
    """
    # 初始化配置解析器
    # config = configparser.ConfigParser()
    config = configparser.ConfigParser(interpolation=None)  # 关键修改


    # 检查配置文件是否存在
    if not os.path.exists(config_path):
        raise FileNotFoundError(f"配置文件 {config_path} 不存在")

    # 读取配置文件
    config.read(config_path, encoding='utf-8')

    # 构建配置字典
    app_config = {}

    # ========== 服务器配置 ==========
    app_config['HOST'] = config.get('server', 'HOST')
    app_config['PORT'] = config.getint('server', 'PORT')

    # ========== 认证配置 ==========
    auth_str = config.get('auth', 'ALLOWED_AUTH', )
    app_config['ALLOWED_AUTH'] = ast.literal_eval(auth_str)

    # ========== 安全配置 ==========
    client_ip_whitelist_str = config.get('security', 'CLIENT_IP_WHITELIST')
    app_config['CLIENT_IP_WHITELIST'] = ast.literal_eval(client_ip_whitelist_str)
    web_ip_whitelist_str = config.get('security', 'WEB_IP_WHITELIST')
    app_config['WEB_IP_WHITELIST'] = ast.literal_eval(web_ip_whitelist_str)


    # ========== 超时配置 ==========
    app_config['GET_TIMEOUT'] = config.getint('timeout', 'GET_TIMEOUT')
    app_config['POST_TIMEOUT'] = config.getint('timeout', 'POST_TIMEOUT')
    app_config['RECV_TIMEOUT'] = config.getfloat('timeout', 'RECV_TIMEOUT')
    app_config['HEARTBEAT_INTERVAL'] = config.getint('timeout', 'HEARTBEAT_INTERVAL')
    app_config['HEARTBEAT_TIMEOUT'] = config.getint('timeout', 'HEARTBEAT_TIMEOUT')

    # ========== 网络配置 ==========
    app_config['BUFFER_SIZE'] = config.getint('network', 'BUFFER_SIZE')

    # ========== 日志配置 ==========
    # 日志级别(转换为logging模块的常量)
    log_level_str = config.get('logging', 'LOG_LEVEL').upper()
    app_config['LOG_LEVEL'] = getattr(logging, log_level_str, logging.DEBUG)

    app_config['LOG_FILE'] = config.get('logging', 'LOG_FILE')
    # 转换为字节(配置文件中是MB)
    app_config['LOG_MAX_BYTES'] = config.getint('logging', 'LOG_MAX_BYTES') * 1024 * 1024
    app_config['LOG_BACKUP_COUNT'] = config.getint('logging', 'LOG_BACKUP_COUNT')
    app_config['LOG_ENCODING'] = config.get('logging', 'LOG_ENCODING')
    app_config['LOG_FORMAT'] = config.get('logging', 'LOG_FORMAT')
    app_config['LOG_DATE_FORMAT'] = config.get('logging', 'LOG_DATE_FORMAT')

    # 控制台日志配置
    app_config['CONSOLE_LOG_ENABLE'] = config.getboolean('logging', 'CONSOLE_LOG_ENABLE')
    console_log_level_str = config.get('logging', 'CONSOLE_LOG_LEVEL').upper()
    app_config['CONSOLE_LOG_LEVEL'] = getattr(logging, console_log_level_str, logging.DEBUG)

    return app_config


def init_logger(config, logger_name='UltimateProxyServer'):
    """
    根据配置初始化日志器

    Args:
        config: 加载后的配置字典
        logger_name: 日志器名称

    Returns:
        logging.Logger: 初始化后的日志器
    """
    # 获取日志器
    logger = logging.getLogger(logger_name)

    # 清空已有处理器(避免重复添加)
    logger.handlers.clear()

    # 设置日志器级别
    logger.setLevel(config['LOG_LEVEL'])

    # 定义日志格式器
    fmt = logging.Formatter(config['LOG_FORMAT'], config['LOG_DATE_FORMAT'])

    # ========== 文件日志处理器 ==========
    try:
        fh = RotatingFileHandler(
            config['LOG_FILE'],
            maxBytes=config['LOG_MAX_BYTES'],
            backupCount=config['LOG_BACKUP_COUNT'],
            encoding=config['LOG_ENCODING']
        )
        fh.setLevel(config['LOG_LEVEL'])
        fh.setFormatter(fmt)
        logger.addHandler(fh)
        logger.debug("文件日志处理器初始化成功")
    except Exception as e:
        print(f"文件日志初始化失败:{e}")
        # 记录到控制台(如果后续控制台日志初始化成功,会同步输出)
        logger.error(f"文件日志初始化失败:{e}")

    # ========== 控制台日志处理器 ==========
    if config['CONSOLE_LOG_ENABLE']:
        ch = logging.StreamHandler()
        ch.setLevel(config['CONSOLE_LOG_LEVEL'])
        ch.setFormatter(fmt)
        logger.addHandler(ch)
        logger.debug("控制台日志处理器初始化成功")

    # 设置根日志级别
    logging.getLogger().setLevel(config['LOG_LEVEL'])

    return logger

# 加载配置(在项目入口处调用)
try:
    config = load_config()
    # 导出为全局变量(保持和原代码用法一致)
    HOST = config['HOST']
    PORT = config['PORT']
    ALLOWED_AUTH = config['ALLOWED_AUTH']
    CLIENT_IP_WHITELIST = config['CLIENT_IP_WHITELIST']
    WEB_IP_WHITELIST = config['WEB_IP_WHITELIST']
    GET_TIMEOUT = config['GET_TIMEOUT']
    POST_TIMEOUT = config['POST_TIMEOUT']
    BUFFER_SIZE = config['BUFFER_SIZE']
    RECV_TIMEOUT = config['RECV_TIMEOUT']
    HEARTBEAT_INTERVAL = config['HEARTBEAT_INTERVAL']
    HEARTBEAT_TIMEOUT = config['HEARTBEAT_TIMEOUT']

    logger = init_logger(config)

    # 测试输出(可选)
    print("配置加载成功:")
    print(f"监听地址: {HOST}:{PORT}")
    print(f"允许的认证: {ALLOWED_AUTH}")
    print(f"GET超时: {GET_TIMEOUT}秒")

except Exception as e:
    print(f"配置加载失败: {e}")
    # 可以选择退出程序或使用默认值
    exit(1)



# ==================== 2. 全局存储 ====================
# 隧道映射:tunnel_id -> {ws_sock, local_host, local_port, client_ip, last_heartbeat, is_ready}
client_mapping = defaultdict(dict)
client_mapping_lock = threading.Lock()
# 请求回调:request_id -> (callback, timeout_at)
request_futures = {}
request_futures_lock = threading.Lock()
# 统计信息
stats = {
    'total_requests': 0,
    'success_requests': 0,
    'failed_requests': 0,
    'avg_response_time': 0.0,
    'last_reset': time.time()
}
stats_lock = threading.Lock()


# ==================== 4. WebSocket 工具函数 ====================
def generate_websocket_key(key):
    """生成 WebSocket 握手响应密钥"""
    GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
    sha1 = hashlib.sha1()
    sha1.update((key + GUID).encode('utf-8'))
    return base64.b64encode(sha1.digest()).decode('utf-8')


def parse_websocket_handshake(data):
    """解析 WebSocket 握手请求"""
    headers = {}
    lines = data.split('\r\n')
    for line in lines[1:]:
        if line == '':
            break
        if ':' in line:
            k, v = line.split(':', 1)
            headers[k.strip()] = v.strip()
    return headers


def do_websocket_handshake(client_sock, headers):
    """完成 WebSocket 握手(优化:精简响应头)"""
    sec_key = headers.get('Sec-WebSocket-Key')
    if not sec_key:
        return False

    accept_key = generate_websocket_key(sec_key)
    response = (
        "HTTP/1.1 101 Switching Protocols\r\n"
        "Upgrade: websocket\r\n"
        "Connection: Upgrade\r\n"
        f"Sec-WebSocket-Accept: {accept_key}\r\n"
        "Sec-WebSocket-Version: 13\r\n\r\n"  # 精简非必要头,加速握手
    )
    client_sock.send(response.encode('utf-8'))
    return True


def websocket_frame_encode(data):
    """编码 WebSocket 消息帧(优化:简化逻辑,加速编码)"""
    payload = data.encode('utf-8')
    payload_len = len(payload)

    # 第一个字节:FIN + TEXT
    frame = bytearray([0x81])

    # 第二个字节:无掩码 + 长度(服务端发送无需掩码)
    if payload_len <= 125:
        frame.append(payload_len)
    elif payload_len <= 65535:
        frame.append(126)
        frame.extend(payload_len.to_bytes(2, byteorder='big'))
    else:
        frame.append(127)
        frame.extend(payload_len.to_bytes(8, byteorder='big'))

    frame.extend(payload)
    return frame


def websocket_frame_decode(data):
    """解码 WebSocket 消息帧(优化:快速处理ping/pong)"""
    if len(data) < 2:
        return None, None

    # 解析第一个字节
    first_byte = data[0]
    opcode = first_byte & 0x0F

    # 快速处理ping/pong
    if opcode == 0x9:  # ping
        logger.debug("收到ping帧,立即回复pong")
        # 构造pong帧(直接复用payload)
        pong_frame = bytearray([0x8A]) + data[1:]
        return None, pong_frame
    elif opcode == 0xA:  # pong
        logger.debug("收到pong帧,更新心跳")
        return None, None

    # 解析文本帧
    second_byte = data[1]
    mask = (second_byte & 0x80) != 0
    payload_len = second_byte & 0x7F

    offset = 2
    if payload_len == 126:
        payload_len = int.from_bytes(data[2:4], byteorder='big')
        offset = 4
    elif payload_len == 127:
        payload_len = int.from_bytes(data[2:10], byteorder='big')
        offset = 10

    # 解析掩码
    mask_key = None
    if mask:
        mask_key = data[offset:offset + 4]
        offset += 4

    # 解析负载
    payload = data[offset:offset + payload_len]

    # 解掩码
    if mask and mask_key:
        decoded = bytearray()
        for i in range(payload_len):
            decoded.append(payload[i] ^ mask_key[i % 4])
        payload = decoded

    return payload.decode('utf-8', errors='ignore'), None


# ==================== 5. 通用工具函数 ====================
def check_basic_auth(auth_header):
    if not auth_header or not auth_header.startswith('Basic '):
        return False
    try:
        encoded = auth_header.split(' ')[1]
        decoded = base64.b64decode(encoded).decode('utf-8')
        u, p = decoded.split(':', 1)
        return ALLOWED_AUTH.get(u) == p
    except:
        return False


def check_client_ip_whitelist(client_ip):
    """校验内网穿透Client(WebSocket)的IP是否在白名单内"""
    if not client_ip:
        return False
    try:
        # 排除本地IP(生产环境可删除此段)
        if client_ip in ['127.0.0.1', '::1', 'localhost']:
            return True

        ip_obj = ipaddress.ip_address(client_ip)
        for cidr in CLIENT_IP_WHITELIST:
            if ip_obj in ipaddress.ip_network(cidr, strict=False):
                return True
        logger.warning(f"Client IP {client_ip} 不在CLIENT_IP_WHITELIST内!白名单:{CLIENT_IP_WHITELIST}")
        return False
    except Exception as e:
        logger.error(f"Client IP白名单校验异常:{e},IP:{client_ip}")
        return False


def check_web_ip_whitelist(web_ip):
    """校验浏览器/Postman(HTTP)的IP是否在白名单内"""
    if not web_ip:
        return False
    try:
        # 排除本地IP(生产环境可删除此段)
        if web_ip in ['127.0.0.1', '::1', 'localhost']:
            return True

        ip_obj = ipaddress.ip_address(web_ip)
        for cidr in WEB_IP_WHITELIST:
            if ip_obj in ipaddress.ip_network(cidr, strict=False):
                return True
        logger.warning(f"Web用户IP {web_ip} 不在WEB_IP_WHITELIST内!白名单:{WEB_IP_WHITELIST}")
        return False
    except Exception as e:
        logger.error(f"Web IP白名单校验异常:{e},IP:{web_ip}")
        return False

def update_stats(is_success, cost_time):
    with stats_lock:
        stats['total_requests'] += 1
        if is_success:
            stats['success_requests'] += 1
        else:
            stats['failed_requests'] += 1
        stats['avg_response_time'] = (stats['avg_response_time'] * 0.9) + (cost_time * 0.1)


def clean_expired_requests():
    """后台清理超时请求(修复:增加Socket有效性校验,避免误清理)"""
    while True:
        try:
            now = time.time()
            to_remove = []

            # 清理超时请求
            with request_futures_lock:
                for req_id, (_, timeout_at) in request_futures.items():
                    if now > timeout_at:
                        to_remove.append(req_id)

            with request_futures_lock:
                for req_id in to_remove:
                    request_futures.pop(req_id, None)
                    logger.info(f"清理超时请求:{req_id}")

            # 清理超时隧道(先检查Socket是否有效,再判断超时)
            with client_mapping_lock:
                expired_tunnels = []
                for tunnel_id, info in client_mapping.items():
                    last_hb = info.get('last_heartbeat', 0)
                    ws_sock = info.get('ws_sock')
                    # 双重判断:1.心跳超时 2.Socket已关闭 或 超时超过2倍阈值
                    timeout_condition = now - last_hb > HEARTBEAT_TIMEOUT
                    sock_invalid = False
                    try:
                        if ws_sock and ws_sock.fileno() == -1:
                            sock_invalid = True
                    except:
                        sock_invalid = True

                    if timeout_condition and (sock_invalid or now - last_hb > 2 * HEARTBEAT_TIMEOUT):
                        expired_tunnels.append(tunnel_id)
                        # 安全关闭Socket
                        try:
                            if ws_sock and ws_sock.fileno() != -1:
                                ws_sock.shutdown(socket.SHUT_RDWR)
                                ws_sock.close()
                        except:
                            pass
                for tunnel_id in expired_tunnels:
                    del client_mapping[tunnel_id]
                    logger.info(f"清理超时隧道:{tunnel_id}")

            time.sleep(5)  # 降低清理频率,减少CPU占用
        except Exception as e:
            logger.error(f"清理超时请求异常:{e}", exc_info=True)


def safe_recv(client_sock, buffer_size, timeout=RECV_TIMEOUT):
    """安全的套接字接收函数(优化:非阻塞+快速返回)"""
    try:
        client_sock.settimeout(timeout)
        data = b''
        start_time = time.time()

        while True:
            if time.time() - start_time > timeout:
                logger.warning("套接字接收超时,快速返回已有数据")
                return data.decode('utf-8', errors='ignore') if data else None

            # 非阻塞等待,减少CPU占用
            ready = select.select([client_sock], [], [], 0.1)
            if not ready[0]:
                continue

            chunk = client_sock.recv(buffer_size)
            if not chunk:
                break
            data += chunk

            # HTTP请求头接收完成后立即返回
            if b'\r\n\r\n' in data:
                break

        return data.decode('utf-8', errors='ignore') if data else None

    except socket.timeout:
        logger.warning("套接字接收超时")
        return None
    except Exception as e:
        logger.error(f"安全接收数据异常:{e}", exc_info=True)
        return None


def unescape_json_chinese(json_str):
    """简化JSON中文转义(失败时直接返回原字符串)"""
    try:
        data = json.loads(json_str)
        return json.dumps(data, ensure_ascii=False)
    except:
        return json_str


# ==================== 6. WebSocket 客户端处理线程(Socket校验+心跳处理) ====================
def handle_websocket_client(client_sock, client_ip):
    """处理 WebSocket 客户端(核心修复:增加Socket校验+完善清理逻辑)"""
    client_id = id(client_sock)
    registered_tunnel = None
    logger.info(f"WebSocket 客户端连接:{client_ip}(ID:{client_id})")

    # 新增:标记连接是否存活,避免死循环
    is_connected = True
    # 新增:线程安全的心跳时间存储
    last_heartbeat = [time.time()]

    try:
        client_sock.settimeout(RECV_TIMEOUT)

        while is_connected:
            # 非阻塞等待数据,避免死循环
            ready = select.select([client_sock], [], [], HEARTBEAT_INTERVAL)
            if not ready[0]:
                # 发送心跳包(仅当超过心跳间隔时)
                if time.time() - last_heartbeat[0] > HEARTBEAT_INTERVAL:
                    try:
                        # 发送心跳前先校验Socket是否有效
                        if client_sock.fileno() == -1:
                            logger.warning(f"客户端 {client_ip} Socket已关闭(fileno=-1),跳过心跳发送")
                            is_connected = False
                            break

                        # 额外校验:检查Socket是否可写
                        write_ready = select.select([], [client_sock], [], 0)
                        if not write_ready[1]:
                            logger.warning(f"客户端 {client_ip} Socket不可写,心跳发送失败")
                            is_connected = False
                            break

                        # 发送心跳包
                        ping_frame = bytearray([0x89, 0x00])  # FIN + PING
                        client_sock.send(ping_frame)
                        last_heartbeat[0] = time.time()
                        logger.debug(f"发送心跳ping到客户端 {client_ip}")
                    except OSError as e:
                        # 专门捕获文件描述符错误(Errno 9)
                        if e.errno == errno.EBADF:
                            logger.error(f"发送心跳失败:[Errno 9] Bad file descriptor(客户端:{client_ip})")
                        else:
                            logger.error(f"发送心跳失败:{e}(客户端:{client_ip})")
                        is_connected = False
                        break
                    except Exception as e:
                        logger.error(f"发送心跳失败:{e}(客户端:{client_ip})")
                        is_connected = False
                        break
                continue

            # 读取数据前先检查连接状态
            if not is_connected:
                break

            # 读取数据增加异常捕获
            try:
                data = client_sock.recv(BUFFER_SIZE)
            except socket.timeout:
                logger.warning(f"客户端 {client_ip} 数据接收超时")
                continue
            except OSError as e:
                logger.error(f"客户端 {client_ip} 接收数据失败:{e}")
                is_connected = False
                break

            if not data:
                logger.info(f"WebSocket 客户端 {client_ip} 关闭连接(无数据)")
                is_connected = False
                break

            # 解码(自动处理ping/pong)
            msg, pong_frame = websocket_frame_decode(data)
            if pong_frame:
                # 回复pong前先校验Socket有效性
                try:
                    if client_sock.fileno() != -1:
                        client_sock.send(pong_frame)
                        last_heartbeat[0] = time.time()
                        # 同步更新client_mapping中的心跳时间
                        if registered_tunnel:
                            with client_mapping_lock:
                                if registered_tunnel in client_mapping:
                                    client_mapping[registered_tunnel]['last_heartbeat'] = last_heartbeat[0]
                        logger.debug(f"收到ping帧,立即回复pong(客户端:{client_ip})")
                except Exception as e:
                    logger.error(f"回复pong失败:{e}(客户端:{client_ip})")
                    is_connected = False
                continue
            if not msg:
                continue

            # 更新心跳时间(线程安全)
            last_heartbeat[0] = time.time()
            # 同步更新client_mapping中的心跳时间
            if registered_tunnel:
                with client_mapping_lock:
                    if registered_tunnel in client_mapping:
                        client_mapping[registered_tunnel]['last_heartbeat'] = last_heartbeat[0]

            # 异步处理消息,避免阻塞主线程
            def process_msg_async(msg):
                nonlocal registered_tunnel
                try:
                    msg_json = json.loads(msg)
                    msg_type = msg_json.get('type')

                    # 注册隧道立即响应,不做任何校验阻塞
                    if msg_type == 'register_tunnel':
                        tunnel_id = msg_json.get('tunnel_id')
                        local_host = msg_json.get('local_host')
                        local_port = msg_json.get('local_port')

                        # 快速参数校验
                        if not all([tunnel_id, local_host, local_port]):
                            # 发送响应前校验Socket
                            if client_sock.fileno() != -1:
                                resp = json.dumps({'type': 'register_failed', 'msg': '参数缺失'}, ensure_ascii=False)
                                client_sock.send(websocket_frame_encode(resp))
                            logger.warning(f"隧道注册参数缺失:{msg[:100]}(客户端:{client_ip})")
                            return

                        # 异步注册(不阻塞)
                        with client_mapping_lock:
                            client_mapping[tunnel_id] = {
                                'ws_sock': client_sock,
                                'local_host': local_host,
                                'local_port': local_port,
                                'client_ip': client_ip,
                                'client_id': client_id,
                                'create_time': time.time(),
                                'last_heartbeat': last_heartbeat[0],  # 使用最新心跳时间
                                'is_ready': True  # 标记为就绪,允许立即转发请求
                            }
                        registered_tunnel = tunnel_id

                        # 立即返回注册成功,不等待任何操作
                        # 发送响应前校验Socket
                        if client_sock.fileno() != -1:
                            resp = json.dumps({
                                'type': 'register_success',
                                'msg': f'隧道 {tunnel_id} 注册成功'
                            }, ensure_ascii=False)
                            client_sock.send(websocket_frame_encode(resp))
                        logger.info(f"隧道 {tunnel_id} 注册成功(客户端:{client_ip})- 快速响应")

                    # 代理响应快速处理
                    elif msg_type == 'proxy_response':
                        req_id = msg_json.get('request_id')
                        resp_data = msg_json.get('data')

                        if isinstance(resp_data, dict) and 'data' in resp_data:
                            resp_data['data'] = unescape_json_chinese(resp_data['data'])

                        # 快速回调,减少锁持有时间
                        with request_futures_lock:
                            callback_info = request_futures.pop(req_id, None)
                        if callback_info:
                            callback = callback_info[0]
                            callback(resp_data)
                            logger.info(f"请求 {req_id} 响应已处理(客户端:{client_ip})")

                except json.JSONDecodeError as e:
                    logger.error(f"JSON 解析失败:{e},消息内容:{msg[:100]}(客户端:{client_ip})")
                except Exception as e:
                    logger.error(f"解析客户端消息异常:{e}(客户端:{client_ip})", exc_info=True)

            # 新开线程处理消息,主线程继续监听
            threading.Thread(target=process_msg_async, args=(msg,), daemon=True).start()

    except socket.timeout:
        logger.warning(f"WebSocket 客户端 {client_ip} 超时,主动断开(ID:{client_id})")
    except Exception as e:
        logger.error(f"WebSocket 客户端 {client_ip} 异常:{e}(ID:{client_id})", exc_info=True)
    finally:
        # 完善隧道清理逻辑,增加存在性检查
        if registered_tunnel:
            with client_mapping_lock:
                if registered_tunnel in client_mapping:
                    tunnel_info = client_mapping[registered_tunnel]
                    if tunnel_info.get('client_id') == client_id:
                        del client_mapping[registered_tunnel]
                        logger.info(f"隧道 {registered_tunnel} 已清理(客户端 {client_ip} 断开)")
                else:
                    logger.debug(f"隧道 {registered_tunnel} 已不存在,无需清理")
        # 安全关闭Socket,先shutdown再close
        try:
            if client_sock.fileno() != -1:
                client_sock.shutdown(socket.SHUT_RDWR)
                client_sock.close()
                logger.debug(f"客户端 {client_ip} Socket已关闭(ID:{client_id})")
        except Exception as e:
            logger.debug(f"关闭客户端Socket时忽略异常:{e}(客户端:{client_ip})")
        logger.info(f"WebSocket 客户端 {client_ip} 断开连接(ID:{client_id})")


# ==================== HTTP 请求处理(核心优化:复用连接+快速转发) ====================
def handle_http_request(client_sock, client_ip, initial_data=None):
    """处理 HTTP 请求(核心优化:减少阻塞+快速超时)"""
    try:
        # 快速接收请求数据
        data = initial_data if initial_data else safe_recv(client_sock, BUFFER_SIZE)
        if not data:
            client_sock.close()
            return

        lines = data.split('\r\n')
        request_line = lines[0].strip()
        if not request_line:
            client_sock.close()
            return

        method, path, protocol = request_line.split(' ', 2)
        req_id = str(uuid.uuid4())
        start_time = time.time()

        logger.info(f"处理 HTTP 请求:{method} {path}(客户端:{client_ip})")

        # 快速解析请求头和体
        headers = {}
        body = ''
        content_length = 0
        auth_header = ''

        for line in lines[1:]:
            if line == '':
                body_start = data.find('\r\n\r\n') + 4
                body = data[body_start:]
                break
            if ':' in line:
                k, v = line.split(':', 1)
                headers[k.strip()] = v.strip()
                if k.lower() == 'content-length':
                    try:
                        content_length = int(v.strip())
                    except:
                        content_length = 0
                if k.lower() == 'authorization':
                    auth_header = v.strip()

        # 补充读取剩余请求体(超时则放弃)
        if content_length > len(body):
            remaining = content_length - len(body)
            remaining_data = safe_recv(client_sock, remaining, timeout=2)  # 仅等待2秒
            if remaining_data:
                body += remaining_data

        # 解析 GET 参数
        from urllib.parse import urlparse, parse_qs
        parsed_url = urlparse(path)
        path = parsed_url.path
        query_params = parse_qs(parsed_url.query)
        query_params = {k: v[0] if len(v) > 0 else '' for k, v in query_params.items()}

        # 1. IP 白名单校验(快速返回)
        if not check_web_ip_whitelist(client_ip):
            response = (
                    "HTTP/1.1 403 Forbidden\r\n"
                    "Content-Type: application/json; charset=utf-8\r\n"
                    "Connection: close\r\n\r\n"
                    + json.dumps({'code': 403, 'msg': f'WEB IP {client_ip} 不在白名单内'}, ensure_ascii=False)
            )
            client_sock.send(response.encode('utf-8'))
            client_sock.close()
            update_stats(False, time.time() - start_time)
            return

        # 2. Basic Auth 校验(快速返回)
        if not check_basic_auth(auth_header):
            response = (
                    "HTTP/1.1 401 Unauthorized\r\n"
                    "Content-Type: application/json; charset=utf-8\r\n"
                    'WWW-Authenticate: Basic realm="Proxy"\r\n'
                    "Connection: close\r\n\r\n"
                    + json.dumps({'code': 401, 'msg': '认证缺失'},
                                 ensure_ascii=False)
            )
            client_sock.send(response.encode('utf-8'))
            client_sock.close()
            update_stats(False, time.time() - start_time)
            return

        # 3. 监控接口(快速返回)
        if path == '/monitor':
            with stats_lock:
                uptime = time.time() - stats['last_reset']
                qps = stats['total_requests'] / uptime if uptime > 0 else 0
                success_rate = (stats['success_requests'] / stats['total_requests'] * 100) if stats[
                                                                                                  'total_requests'] > 0 else 0

                monitor_data = {
                    '运行时长(秒)': round(uptime, 2),
                    '总请求数': stats['total_requests'],
                    '成功请求数': stats['success_requests'],
                    '失败请求数': stats['failed_requests'],
                    '成功率': f"{success_rate:.2f}%",
                    '平均响应时间': f"{stats['avg_response_time'] * 1000:.2f}ms",
                    'QPS': f"{qps:.2f}",
                    '当前注册隧道数': len(client_mapping)
                }

            response = (
                    "HTTP/1.1 200 OK\r\n"
                    "Content-Type: application/json; charset=utf-8\r\n"
                    "Connection: keep-alive\r\n\r\n"
                    + json.dumps(monitor_data, ensure_ascii=False, indent=2)
            )
            client_sock.send(response.encode('utf-8'))
            client_sock.close()
            update_stats(True, time.time() - start_time)
            return

        # 4. 解析隧道 ID(快速解析)
        path_parts = path.strip('/').split('/', 1)
        tunnel_id = path_parts[0] if path_parts else ''
        req_path = path_parts[1] if len(path_parts) > 1 else ''

        # 5. 检查隧道是否存在(加锁后快速返回)
        with client_mapping_lock:
            tunnel_info = client_mapping.get(tunnel_id, {})
        if not tunnel_info or not tunnel_info.get('is_ready'):
            response = (
                    "HTTP/1.1 404 Not Found\r\n"
                    "Content-Type: application/json; charset=utf-8\r\n"
                    "Connection: close\r\n\r\n"
                    + json.dumps({'code': 404, 'msg': f'隧道 {tunnel_id} 未注册或未就绪'}, ensure_ascii=False)
            )
            client_sock.send(response.encode('utf-8'))
            client_sock.close()
            update_stats(False, time.time() - start_time)
            return

        # 6. 转发请求到客户端(核心:异步转发+快速超时)
        timeout = GET_TIMEOUT if method == 'GET' else POST_TIMEOUT
        resp_data = None
        resp_event = threading.Event()

        def callback(data):
            nonlocal resp_data
            resp_data = data
            resp_event.set()

        # 存储回调(短超时)
        with request_futures_lock:
            request_futures[req_id] = (callback, time.time() + timeout)

        # 构造请求消息(精简字段)
        req_msg = json.dumps({
            'type': 'proxy_request',
            'request_id': req_id,
            'data': {
                'method': method,
                'path': req_path,
                'args': query_params,
                'data': body,
                'headers': headers,
                'remote_ip': client_ip
            }
        }, ensure_ascii=False)

        try:
            # 异步发送请求,不阻塞
            def send_req_async():
                try:
                    with client_mapping_lock:
                        if tunnel_id in client_mapping:
                            tunnel_info = client_mapping[tunnel_id]
                            # 发送请求前校验Socket有效性
                            if tunnel_info['ws_sock'].fileno() != -1:
                                tunnel_info['ws_sock'].send(websocket_frame_encode(req_msg))
                    logger.info(f"请求 {req_id} 异步转发到隧道 {tunnel_id}")
                except Exception as e:
                    logger.error(f"异步发送请求失败:{e}")
                    resp_event.set()  # 触发超时

            threading.Thread(target=send_req_async, daemon=True).start()

            # 等待响应(严格超时)
            resp_event.wait(timeout=timeout)

            # 超时处理(快速返回)
            if not resp_data:
                response = (
                        "HTTP/1.1 504 Gateway Timeout\r\n"
                        "Content-Type: application/json; charset=utf-8\r\n"
                        "Connection: close\r\n\r\n"
                        + json.dumps({'code': 504, 'msg': '请求超时(5秒)'}, ensure_ascii=False)
                )
                client_sock.send(response.encode('utf-8'))
                update_stats(False, time.time() - start_time)
            else:
                # 快速构造响应
                status_code = resp_data.get('status', 200)
                resp_headers = resp_data.get('headers', {})
                resp_body = resp_data.get('data', '')

                if isinstance(resp_body, str):
                    resp_body = unescape_json_chinese(resp_body)
                elif isinstance(resp_body, dict):
                    resp_body = json.dumps(resp_body, ensure_ascii=False)

                # 构造响应头(精简)
                response_headers = [f"HTTP/1.1 {status_code} OK\r\n"]
                response_headers.append("Content-Type: application/json; charset=utf-8\r\n")
                response_headers.append("Connection: keep-alive\r\n")
                response_headers.append("\r\n")

                response = ''.join(response_headers) + resp_body
                client_sock.send(response.encode('utf-8'))
                update_stats(True, time.time() - start_time)
                logger.info(f"请求 {req_id} 处理成功,耗时:{time.time() - start_time:.2f}秒")

        except Exception as e:
            response = (
                    "HTTP/1.1 500 Internal Server Error\r\n"
                    "Content-Type: application/json; charset=utf-8\r\n"
                    "Connection: close\r\n\r\n"
                    + json.dumps({'code': 500, 'msg': f'服务端错误:{str(e)[:50]}'}, ensure_ascii=False)
            )
            client_sock.send(response.encode('utf-8'))
            update_stats(False, time.time() - start_time)
            logger.error(f"请求 {req_id} 处理异常:{e}", exc_info=True)

        finally:
            # 快速清理回调
            with request_futures_lock:
                request_futures.pop(req_id, None)
            client_sock.close()

    except Exception as e:
        logger.error(f"处理 HTTP 请求异常:{e}", exc_info=True)
        try:
            client_sock.send(
                b"HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/plain; charset=utf-8\r\nConnection: close\r\n\r\nServer Error")
        except:
            pass
        client_sock.close()


# ==================== 主服务(优化:连接复用+快速处理) ====================
def main():
    """启动主服务(双白名单校验)"""
    # 启动清理线程
    cleaner_thread = threading.Thread(target=clean_expired_requests, daemon=True)
    cleaner_thread.start()

    # 创建 TCP 套接字
    server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
    server_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
    server_sock.bind((HOST, PORT))
    server_sock.listen(500)
    server_sock.settimeout(None)

    logger.info("=" * 80)
    logger.info("🚀 双IP白名单版代理服务启动成功!")
    logger.info(f"🔧 监听地址:http://{HOST}:{PORT}")
    logger.info(f"📊 监控地址:http://{HOST}:{PORT}/monitor")
    logger.info(f"🔒 Client IP白名单:{CLIENT_IP_WHITELIST}")
    logger.info(f"🔒 Web IP白名单:{WEB_IP_WHITELIST}")
    logger.info("=" * 80)

    # 主循环
    while True:
        try:
            client_sock, client_addr = server_sock.accept()
            socket_ip = client_addr[0]

            client_sock.settimeout(RECV_TIMEOUT)
            client_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

            handshake_data = safe_recv(client_sock, BUFFER_SIZE)
            if not handshake_data:
                client_sock.close()
                continue

            if 'Upgrade: websocket' in handshake_data:
                # ========== 1. WebSocket连接(Client):校验CLIENT_IP_WHITELIST ==========
                headers = parse_websocket_handshake(handshake_data)
                # 获取真实Client IP
                real_client_ip = headers.get('X-Real-IP', headers.get('X-Forwarded-For', socket_ip))
                if ',' in real_client_ip:
                    real_client_ip = real_client_ip.split(',')[0].strip()

                # 校验Client IP白名单
                if not check_client_ip_whitelist(real_client_ip):
                    logger.error(f"Client {real_client_ip} 不在白名单内,拒绝WebSocket连接!")
                    response = (
                            "HTTP/1.1 403 Forbidden\r\n"
                            "Content-Type: application/json; charset=utf-8\r\n\r\n"
                            + json.dumps({'code': 403, 'msg': 'Client IP不在白名单内'}, ensure_ascii=False)
                    )
                    client_sock.send(response.encode('utf-8'))
                    client_sock.close()
                    continue

                # 校验通过,完成握手并处理
                if do_websocket_handshake(client_sock, headers):
                    ws_thread = threading.Thread(
                        target=handle_websocket_client,
                        args=(client_sock, real_client_ip),
                        daemon=True
                    )
                    ws_thread.start()
                else:
                    client_sock.close()
            else:
                # ========== 2. HTTP请求(Web端):校验WEB_IP_WHITELIST ==========
                # 获取真实Web用户IP
                real_web_ip = socket_ip
                try:
                    # 解析X-Real-IP头(Nginx传递的真实IP)
                    for line in handshake_data.split('\r\n'):
                        if line.lower().startswith('x-real-ip:'):
                            real_web_ip = line.split(':', 1)[1].strip()
                            break
                    # 备用:解析X-Forwarded-For
                    if real_web_ip == socket_ip:
                        for line in handshake_data.split('\r\n'):
                            if line.lower().startswith('x-forwarded-for:'):
                                real_web_ip = line.split(':', 1)[1].strip().split(',')[0].strip()
                                break
                except:
                    pass

                # 校验Web IP白名单
                if not check_web_ip_whitelist(real_web_ip):
                    logger.error(f"Web用户 {real_web_ip} 不在白名单内,拒绝HTTP访问!")
                    response = (
                            "HTTP/1.1 403 Forbidden\r\n"
                            "Content-Type: application/json; charset=utf-8\r\n\r\n"
                            + json.dumps({'code': 403, 'msg': 'Web IP不在白名单内.'}, ensure_ascii=False)
                    )
                    client_sock.send(response.encode('utf-8'))
                    client_sock.close()
                    continue

                # 校验通过,处理HTTP请求
                http_thread = threading.Thread(
                    target=handle_http_request,
                    args=(client_sock, real_web_ip, handshake_data),
                    daemon=True
                )
                http_thread.start()

        except Exception as e:
            logger.error(f"服务端主循环异常:{e}", exc_info=True)
            continue

if __name__ == '__main__':
    main()

后台启动脚本(临时)

文件名:run_proxy_server.sh

powershell 复制代码
nohup python3 ${你的文件目录}/proxy_server.py > ${你的日志目录}/log.out 2>&1 &

重启脚本(临时)

文件名:restart_proxy_server.sh

powershell 复制代码
pkill -f proxy_server
echo "==== proxy_server 进程已杀死 ===="
sleep 2

echo "==== proxy_server 开始重启 ===="
${你的文件目录}/run_proxy_server.sh
sleep 2

ps aux |grep proxy_server
echo "==== proxy_server 重启成功 ===="

Client

Client依赖

Python 3.9.6

bash 复制代码
pip install flask==3.1.3 flask-socketio==5.6.1 requests==2.32.5  eventlet==0.40.4  websocket-client==1.9.0 python-socketio==5.16.1 greenlet==2.0.2

client 文件内容

python 复制代码
#! /usr/bin/python3
# -*- coding: utf-8 -*-
import websocket
import json
import requests
import time
import logging
from logging.handlers import RotatingFileHandler
import configparser
import os
import ssl
import threading
import sys


# ==================== 1. 加载配置(增强容错+绝对路径) ====================
def load_config():
    """加载配置文件(容错处理+绝对路径)"""
    config = configparser.ConfigParser()
    # 获取当前脚本目录,确保配置文件路径正确
    script_dir = os.path.dirname(os.path.abspath(__file__))
    config_path = os.path.join(script_dir, 'config.ini')

    # 检查配置文件是否存在
    if not os.path.exists(config_path):
        raise FileNotFoundError(f"配置文件不存在:{config_path}")

    # 读取配置
    config.read(config_path, encoding='utf-8')

    # 检查必要的节是否存在
    required_sections = ['Server', 'Local', 'Reconnect', 'WebSocket']
    for section in required_sections:
        if not config.has_section(section):
            raise configparser.NoSectionError(f"配置文件缺少 [{section}] 节")

    return {
        'server_url': config.get('Server', 'server_url'),
        'tunnel_id': config.get('Server', 'tunnel_id'),
        'local_host': config.get('Local', 'local_host'),
        'local_port': config.getint('Local', 'local_port'),
        'local_timeout': config.getint('Local', 'local_request_timeout'),
        'reconnect_interval': config.getint('Reconnect', 'reconnect_interval'),
        'max_reconnect': config.getint('Reconnect', 'max_reconnect_retries'),
        # WebSocket心跳配置(仅用于内置机制)
        'ping_interval': config.getint('WebSocket', 'ping_interval'),
        'ping_timeout': config.getint('WebSocket', 'ping_timeout')
    }


# 容错加载配置
try:
    cfg = load_config()
except Exception as e:
    print(f"❌ 加载配置文件失败:{e}")
    print("📌 请检查:")
    print("  1. config.ini 是否和client_v1.py在同一目录")
    print("  2. config.ini 格式是否正确(无拼写错误)")
    sys.exit(1)

# ==================== 2. 日志初始化 ====================
logger = logging.getLogger('SimpleTunnelClient')
logger.setLevel(logging.DEBUG)
log_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s', '%Y-%m-%d %H:%M:%S')

# 文件日志
file_handler = RotatingFileHandler(
    'simple_client.log',
    maxBytes=10 * 1024 * 1024,
    backupCount=5,
    encoding='utf-8'
)
file_handler.setFormatter(log_format)
file_handler.setLevel(logging.DEBUG)
logger.addHandler(file_handler)

# 控制台日志
console_handler = logging.StreamHandler()
console_handler.setFormatter(log_format)
console_handler.setLevel(logging.INFO)
logger.addHandler(console_handler)

logging.getLogger('requests').setLevel(logging.WARNING)
logging.getLogger('urllib3').setLevel(logging.WARNING)

# ==================== 3. 全局变量 ====================
ws_global = None
ws_lock = threading.Lock()
ws_connected = False

# ==================== 全局重连控制 ====================
reconnecting = False  # 防止重复重连
last_connect_time = 0  # 最后连接时间(防抖)

# ==================== 4. 工具函数:极简安全的连接状态检查 ====================
def is_ws_connected(ws):
    """
    极简安全的WebSocket连接状态检查(避免属性错误)
    :param ws: WebSocketApp对象
    :return: bool
    """
    if ws is None:
        return False
    return ws_connected  # 直接使用全局变量,函数内无修改,无需global


# ==================== 5. 核心逻辑 ====================
def on_message(ws, message):
    """处理服务端消息(异步处理)"""
    threading.Thread(target=handle_proxy_request, args=(ws, message), daemon=True).start()


def handle_proxy_request(ws, message):
    """异步处理代理请求(所有global声明前置)"""
    global ws_connected

    try:
        logger.debug(f"📥 收到服务端原始消息:{message}")

        msg = json.loads(message)
        msg_type = msg.get('type')

        if msg_type == 'proxy_request':
            req_id = msg.get('request_id')
            req_data = msg.get('data')

            logger.info(f"===== 处理代理请求 [{req_id}] =====")
            logger.info(f"🔧 请求方法:{req_data['method']}")
            logger.info(f"🔗 请求路径:{req_data['path']}")
            logger.info(f"📝 请求参数:{req_data['args']}")
            logger.info(f"📨 请求头:{req_data['headers']}")

            req_body = req_data.get('data', '')
            if len(req_body) > 500:
                logger.info(f"📦 请求体:{req_body[:500]}...(长度:{len(req_body)})")
            else:
                logger.info(f"📦 请求体:{req_body}")

            # 构造本地请求地址
            local_url = f"http://{cfg['local_host']}:{cfg['local_port']}/{req_data['path'].lstrip('/')}"
            logger.info(f"🔄 转发到本地服务:{local_url}")

            try:
                # 完整转发请求头
                headers = req_data['headers']
                headers['Content-Type'] = headers.get('Content-Type', 'application/json; charset=utf-8')
                headers['Accept-Charset'] = 'utf-8'

                # 使用session复用连接
                session = requests.Session()
                resp = session.request(
                    method=req_data['method'],
                    url=local_url,
                    headers=headers,
                    data=req_data['data'].encode('utf-8') if isinstance(req_data['data'], str) else req_data['data'],
                    params=req_data['args'],
                    timeout=cfg['local_timeout'],
                    verify=False,
                    allow_redirects=True
                )
                session.close()

                logger.info(f"✅ 本地服务响应状态码:{resp.status_code}")
                logger.info(f"📄 本地响应体:{resp.text[:500]}")

                # 构造响应
                resp_msg = {
                    'type': 'proxy_response',
                    'request_id': req_id,
                    'data': {
                        'status': resp.status_code,
                        'headers': dict(resp.headers),
                        'data': resp.text
                    }
                }

                # 移除Content-Length避免冲突
                if 'Content-Length' in resp_msg['data']['headers']:
                    del resp_msg['data']['headers']['Content-Length']

                # 发送响应(极简安全版)
                with ws_lock:
                    if is_ws_connected(ws):
                        try:
                            resp_msg_str = json.dumps(resp_msg, ensure_ascii=False)
                            ws.send(resp_msg_str)
                            logger.info(f"✅ 响应已发送:{req_id}")
                        except Exception as send_e:
                            logger.error(f"❌ 发送响应失败:{send_e}", exc_info=True)
                            # 发送失败标记连接断开
                            ws_connected = False

                logger.info(f"✅ 请求 [{req_id}] 代理完成")

            except requests.exceptions.ConnectionError:
                err_data = {
                    'status': 'failed',
                    'code': 503,
                    'msg': f"本地服务不可达 {cfg['local_host']}:{cfg['local_port']}",
                    'data': ''
                }
                err_msg = {
                    'type': 'proxy_response',
                    'request_id': req_id,
                    'data': {
                        'status': 503,
                        'headers': {'Content-Type': 'application/json; charset=utf-8'},
                        'data': json.dumps(err_data, ensure_ascii=False)
                    }
                }
                with ws_lock:
                    if ws_connected:
                        try:
                            ws.send(json.dumps(err_msg, ensure_ascii=False))
                        except Exception:
                            pass
                logger.error(f"❌ 请求 [{req_id}] 本地服务不可达")

            except requests.exceptions.Timeout:
                err_data = {
                    'status': 'failed',
                    'code': 504,
                    'msg': f"本地服务请求超时({cfg['local_timeout']}秒)",
                    'data': ''
                }
                err_msg = {
                    'type': 'proxy_response',
                    'request_id': req_id,
                    'data': {
                        'status': 504,
                        'headers': {'Content-Type': 'application/json; charset=utf-8'},
                        'data': json.dumps(err_data, ensure_ascii=False)
                    }
                }
                with ws_lock:
                    if ws_connected:
                        try:
                            ws.send(json.dumps(err_msg, ensure_ascii=False))
                        except Exception:
                            pass
                logger.error(f"❌ 请求 [{req_id}] 本地服务超时")

            except Exception as e:
                err_data = {
                    'status': 'failed',
                    'code': 500,
                    'msg': f"客户端代理错误:{str(e)}",
                    'data': ''
                }
                err_msg = {
                    'type': 'proxy_response',
                    'request_id': req_id,
                    'data': {
                        'status': 500,
                        'headers': {'Content-Type': 'application/json; charset=utf-8'},
                        'data': json.dumps(err_data, ensure_ascii=False)
                    }
                }
                with ws_lock:
                    if ws_connected:
                        try:
                            ws.send(json.dumps(err_msg, ensure_ascii=False))
                        except Exception:
                            pass
                logger.error(f"❌ 请求 [{req_id}] 处理异常", exc_info=True)

        elif msg_type == 'register_success':
            ws_connected = True
            logger.info(f"✅ 隧道 {cfg['tunnel_id']} 注册成功")
            print(f"✅ 隧道 {cfg['tunnel_id']} 注册成功")

        elif msg_type == 'register_failed':
            logger.error(f"❌ 隧道注册失败:{msg.get('msg')}")
            print(f"❌ 隧道注册失败:{msg.get('msg')}")

        else:
            logger.warning(f"⚠️ 收到未知类型消息:{msg_type}")

    except json.JSONDecodeError as e:
        logger.error(f"❌ 消息解析失败:{e},原始消息:{message}")
    except Exception as e:
        logger.error(f"❌ 处理消息异常", exc_info=True)


def on_error(ws, error):
    """WebSocket错误处理(global声明前置)"""
    global ws_connected
    ws_connected = False
    logger.error(f"❌ WebSocket 错误:{error}", exc_info=True)
    print(f"❌ WebSocket 错误:{error}")


def on_close(ws, close_status_code, close_msg):
    """WebSocket关闭处理(global声明前置)"""
    global ws_connected, ws_global
    ws_connected = False
    ws_global = None
    logger.warning(f"⚠️ 与服务端断开连接")
    logger.info(f"断开状态码:{close_status_code},原因:{close_msg}")
    print("❌ 与服务端断开连接,准备重连...")


def on_open(ws):
    """连接成功后异步注册隧道(不阻塞请求)"""
    global ws_connected, ws_global
    ws_connected = True
    ws_global = ws

    logger.info(f"✅ 已连接服务端:{cfg['server_url']}")
    print(f"✅ 已连接服务端:{cfg['server_url']}")

    # 核心修改:异步发送注册消息,不阻塞主线程
    def send_register_async():
        try:
            register_msg = {
                'type': 'register_tunnel',
                'tunnel_id': cfg['tunnel_id'],
                'local_host': cfg['local_host'],
                'local_port': cfg['local_port']
            }
            register_msg_str = json.dumps(register_msg, ensure_ascii=False)
            ws.send(register_msg_str)
            logger.info(f"📤 异步发送隧道注册请求:{cfg['tunnel_id']}")
            # 注册超时兜底:10秒未收到success则强制标记为已连接
            time.sleep(10)
            global ws_connected
            if not ws_connected:  # 防止注册失败导致连接标记为断开
                logger.warning("⚠️ 注册隧道超时,强制标记为已连接")
                ws_connected = True
        except Exception as e:
            logger.error(f"❌ 异步注册失败:{e}")

    # 新开线程发送注册消息,不阻塞请求转发
    threading.Thread(target=send_register_async, daemon=True).start()


# (添加防抖)
def connect_server():
    """连接服务端(带重连+防抖)"""
    global ws_global, ws_connected, reconnecting, last_connect_time
    retry_count = 0
    logger.info("=" * 80)
    logger.info("📡 极简隧道客户端启动")
    logger.info(f"🌐 服务端地址:{cfg['server_url']}")
    logger.info(f"🔗 本地服务:{cfg['local_host']}:{cfg['local_port']}")
    logger.info(f"🔑 隧道ID:{cfg['tunnel_id']}")
    logger.info(f"❤️ 内置心跳间隔:{cfg['ping_interval']}秒,超时:{cfg['ping_timeout']}秒")
    logger.info("=" * 80)

    while cfg['max_reconnect'] == 0 or retry_count < cfg['max_reconnect']:
        # 防抖:1秒内不重复重连
        if time.time() - last_connect_time < 1:
            time.sleep(1)
            continue

        if reconnecting:
            continue

        try:
            reconnecting = True
            last_connect_time = time.time()
            logger.info(f"📞 尝试连接服务端(第 {retry_count + 1} 次)...")

            # 清理旧连接
            with ws_lock:
                if ws_global:
                    try:
                        ws_global.close()
                    except Exception:
                        pass
                    ws_global = None
                    ws_connected = False

            # 创建WebSocketApp(优化心跳参数)
            ws = websocket.WebSocketApp(
                cfg['server_url'].rstrip('、'),  # 修复URL末尾的多余符号
                on_open=on_open,
                on_message=on_message,
                on_error=on_error,
                on_close=on_close
            )

            # 运行WebSocket(优化心跳配置)
            ws.run_forever(
                sslopt={"cert_reqs": ssl.CERT_NONE, "check_hostname": False},
                ping_interval=10,  # 缩短心跳间隔(10秒)
                ping_timeout=5,  # 缩短心跳超时(5秒)
                # 新增:重连前等待
                reconnect=5
            )

        except Exception as e:
            logger.error(f"❌ 连接失败", exc_info=True)
            print(f"❌ 连接失败:{e}")
        finally:
            reconnecting = False

        retry_count += 1
        if cfg['max_reconnect'] == 0 or retry_count < cfg['max_reconnect']:
            logger.info(f"⏳ {cfg['reconnect_interval']} 秒后重试...")
            time.sleep(cfg['reconnect_interval'])

    logger.error("❌ 达到最大重连次数,客户端退出")
    print("❌ 达到最大重连次数,退出")


# (添加注册超时检测)
def on_open(ws):
    """连接成功后注册隧道(添加超时检测)"""
    global ws_connected, ws_global
    ws_connected = True
    ws_global = ws

    logger.info(f"✅ 已连接服务端:{cfg['server_url']}")
    print(f"✅ 已连接服务端:{cfg['server_url']}")

    # 发送注册消息
    register_msg = {
        'type': 'register_tunnel',
        'tunnel_id': cfg['tunnel_id'],
        'local_host': cfg['local_host'],
        'local_port': cfg['local_port']
    }
    register_msg_str = json.dumps(register_msg, ensure_ascii=False)

    # 注册超时检测:5秒未收到注册成功则断开重连
    def check_register_timeout():
        global ws_connected
        time.sleep(5)
        if ws_connected and not any([msg_type == 'register_success' for msg_type in ['register_success']]):
            logger.warning("⚠️ 注册隧道超时,断开重连")
            ws.close()

            ws_connected = False

    threading.Thread(target=check_register_timeout, daemon=True).start()

    try:
        ws.send(register_msg_str)
        logger.info(f"📤 发送隧道注册请求:{cfg['tunnel_id']}")
    except Exception as e:
        logger.error(f"❌ 发送注册消息失败:{e}")
        ws_connected = False


if __name__ == '__main__':
    # 安装依赖
    try:
        import websocket
    except ImportError:
        logger.warning("websocket-client 未安装,正在安装...")
        os.system('pip3 install websocket-client')
        import websocket

    try:
        import requests
    except ImportError:
        logger.warning("requests 未安装,正在安装...")
        os.system('pip3 install requests')
        import requests

    # 启动连接
    connect_server()

Client 配置

新建 config.ini 文件,赋值一下内容

复制代码
[Server]
server_url = wss://${你的域名}/proxy/
tunnel_id = my_local_service # 隧道,这里随便写

[Local]
local_host = 127.0.0.1 # 本地服务地址
local_port = 3000  # 本地服务端口
local_request_timeout = 3

[Reconnect]
reconnect_interval = 3
max_reconnect_retries = 0

[WebSocket]
ping_interval = 25
ping_timeout = 10

APP 本地接口服务

python 复制代码
from flask import Flask, jsonify, request  # 新增:导入 request 用于获取 POST 参数
import time


app = Flask(__name__)


@app.route('/')
def index():
    print("Hello, 内网穿透测试!")
    return "Hello, 内网穿透测试!"


@app.route('/api/test')
def test_api():
    print({"code": 200, "msg": "测试成功", "data": "Hello World"})
    return jsonify({"code": 200, "msg": "测试成功", "data": "Hello World"})


# 新增 POST 接口 
@app.route('/api/test1', methods=['POST'])
def post_test_api():
    # 获取 POST 参数(支持 JSON/表单格式)
    if request.is_json:
        params = request.get_json()  # JSON 格式参数
    else:
        params = request.form  # 表单格式参数

    # 打印参数(便于调试)
    print(f"收到 POST 请求,参数:{params}")

    # 构造响应
    response = {
        "code": 200,
        "msg": "POST 请求成功",
        "data": {
            "received_params": params,
            "method": "POST",
            "timestamp": time.time()  # 可选:添加时间戳
        }
    }
    print(response)
    return jsonify(response)



if __name__ == '__main__':
    # 新增:启动 Flask 服务(允许外部访问)
    app.run(host='0.0.0.0', port=3000, debug=False, threaded=True)

测试

1、将Client所在机器的ip地址在Server端加白,将浏览器所在机器的ip在Server端加白;

浏览器访问 https://你的域名/proxy/{你的域名}/proxy/你的域名/proxy/{client配置文件内的隧道}${APP服务接口路径} 使用Server端配置的user:password BasciAuth 认证 ,可以正常返回。

相关推荐
测试19981 小时前
软件测试之压力测试详解
自动化测试·软件测试·python·测试用例·接口测试·压力测试·性能测试
深耕AI1 小时前
【 从零开始的VS Code Python环境配置:uv】
开发语言·python·uv
七夜zippoe2 小时前
Python配置管理革命:pydantic-settings + 动态热更新实战
python·热更新·配置中心·配置管理·pydantic·类型安全
DarkAthena2 小时前
【GaussDB】排查ARM64环境上gaussdb的python驱动(psycopg3)coredump的问题
python·gaussdb
SEO-狼术2 小时前
Convert HTML Tables to PDF in Python
开发语言·python·pdf
von Neumann2 小时前
大模型从入门到应用——HuggingFace:Transformers-[零基础快速上手:自然语言处理任务]
人工智能·python·ai·自然语言处理·大模型·aigc·transformer
TheLegendMe2 小时前
Python 基础语法练习题
开发语言·python
熊猫_豆豆2 小时前
无人机表演点云路径规划(Python版)
开发语言·python·无人机·路径规划
mr_LuoWei20092 小时前
自定义的中文脚本解释器来实现对excel自动化处理(一)
python·自动化·excel