You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Apache环境下PHP反向代理WebSocket请求实现故障排查

PHP反向代理WebSocket长连接阻塞问题

问题背景

现有noVNC客户端通过WebSocket协议接入服务,链路设计如下:

  • 公网HTTPS请求先到达NGINX反向代理,转发至内网Apache服务器
  • Apache上运行index.php校验请求会话合法性,校验通过后将请求转发至后端支持WebSocket的VNC服务器(地址192.168.100.13:5900)
  • 跳过Apache/PHP层让NGINX直连VNC服务时所有功能正常,加入PHP中间层后连接始终失败

当前NGINX配置如下:

server {
    listen 443 ssl;
    server_name vnc.domain.com;
    gzip off;

    server_tokens off;

    location / {
            proxy_set_header X-Forwarded-Host $host:$server_port;
            proxy_set_header X-Forwarded-Server $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;


            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";

            proxy_pass https://webapi.lan/;
    }
ssl_certificate /etc/letsencrypt/live/vnc.domain.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/vnc.domain.com/privkey.pem; # managed by Certbot


}

server {
    listen 80;
    server_name vnc.domain.com;
    return 301 https://$host$request_uri;
}

排查过程

  1. 最初使用echo file_get_contents('proxypage');实现转发,所有请求头、响应头全部丢失,连接失败。
  2. 改用cURL实现转发,逐步手动补全协议切换相关头:手动返回101切换响应码、添加Upgrade: websocket和Connection: Upgrade头、补传Sec-WebSocket-Accept头,但始终提示该头值错误,原因是请求头未完整透传给后端VNC服务,初始cURL代码如下:
    function proxy_pass_vnc( )
    {
    $options = [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HEADER         => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_ENCODING       => "",
        CURLOPT_AUTOREFERER    => true,
        CURLOPT_CONNECTTIMEOUT => 120,
        CURLOPT_TIMEOUT        => 120,
        CURLOPT_MAXREDIRS      => 10,
    ];
    
    $ch = curl_init('http://192.168.100.13:5900');
    curl_setopt_array($ch, $options);
    $remoteSite = curl_exec($ch);
    $header = curl_getinfo($ch);
    curl_close($ch);
    
    header('Upgrade: websocket');
    header('Connection: Upgrade');
    header('Sec-WebSocket-Accept: ' . $header['Sec-WebSocket-Accept']);
    http_response_code(101);
    echo $remoteSite;
    }
    
  3. 优化cURL逻辑,自动透传所有入站请求头、自动返回所有后端响应头后,代理普通HTTP服务正常,但代理WebSocket服务时先出现504超时,调高NGINX超时后发现代码阻塞在curl_exec($ch)处,直到cURL 2分钟超时才返回。经调试确认头信息透传完全正确,阻塞原因是cURL会等待后端连接关闭才返回结果,而WebSocket是长连接,握手后不会主动断开,导致无法完成后续双向实时通信。优化后的cURL代码如下:
    $vnc_request_headers = apache_request_headers();
    $vnc_request_headers_simple_array = [];
    foreach ($vnc_request_headers as $vnc_request_header => $value) {
        array_push($vnc_request_headers_simple_array, $vnc_request_header . ': ' . $value);
    }
    
    $options = [
        CURLOPT_HTTPHEADER     => $vnc_request_headers_simple_array,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HEADER         => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_ENCODING       => "",
        CURLOPT_AUTOREFERER    => true,
        CURLOPT_CONNECTTIMEOUT => 120,
        CURLOPT_TIMEOUT        => 120,
        CURLOPT_MAXREDIRS      => 10,
    ];
    
    $ch = curl_init('http://192.168.100.13:5900');
    curl_setopt_array($ch, $options);
    
    $vnc_response_headers = [];
    curl_setopt($ch, CURLOPT_HEADERFUNCTION,
    function($curl, $header) use (&$vnc_response_headers)
        {
            $len = strlen($header);
            $header = explode(':', $header, 2);
            if (count($header) < 2) return $len;
            $vnc_response_headers[strtolower(trim($header[0]))][] = trim($header[1]);
            return $len;
        }
    );
    
    $vnc_response = curl_exec($ch);
    $vnc_header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
    $vnc_httpCode = curl_getinfo($ch , CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    $vnc_body = substr($vnc_response, $vnc_header_size);
    
    http_response_code($vnc_httpCode);
    
    foreach ($vnc_response_headers as $vnc_response_header => $array) {
        foreach($array as $vnc_response_header_value) {
            header($vnc_response_header . ": " . $vnc_response_header_value);
        }
    }
    
    echo $vnc_body;
    

根本原因

cURL从设计层面就是为一次性短连接HTTP请求服务的,核心逻辑是等待服务端返回完整响应、连接关闭后才会把数据交回上层代码,完全不支持WebSocket握手后的全双工实时通信场景,无论怎么调整cURL参数都解决不了阻塞问题,属于选型错误。

解决方案

方案1:鉴权逻辑前移,用Apache原生模块代理WebSocket(优先推荐)

长连接代理本身就是Web服务器的核心能力,用Apache原生模块处理比PHP实现性能高一个数量级,稳定性也更好,不需要处理复杂的非阻塞IO逻辑:

  1. 开启Apache所需模块:
    a2enmod proxy proxy_wstunnel proxy_http rewrite
    systemctl restart apache2
    
  2. 把PHP中的会话校验逻辑前移到Apache层:如果是PHP默认文件会话,可直接通过Apache读取会话校验;如果是自定义Token/Cookie鉴权,可直接通过Rewrite规则判断合法性,校验不通过直接返回403。校验通过后通过Apache原生代理规则转发WebSocket请求,不需要经过PHP处理,示例配置片段:
    <VirtualHost *:443>
        ServerName webapi.lan
        # 此处配置SSL证书、其他站点规则
        RewriteEngine On
    
        # 替换为实际的会话校验规则,校验不通过直接拦截
        RewriteCond %{HTTP_COOKIE} !PHPSESSID=[有效会话匹配规则] [NC]
        RewriteRule .* - [F,L]
    
        # WebSocket代理配置
        ProxyPreserveHost On
        ProxyRequests Off
        # 自动识别WebSocket升级请求走ws协议转发
        RewriteCond %{HTTP:Upgrade} websocket [NC]
        RewriteCond %{HTTP:Connection} upgrade [NC]
        RewriteRule /(.*) "ws://192.168.100.13:5900/$1" [P,L]
        # 普通HTTP请求转发规则根据实际业务配置
    </VirtualHost>
    
  3. 该方案下Apache会自动处理WebSocket握手、长连接保活、双向数据实时转发,和NGINX直连VNC的效果完全一致,没有额外性能损耗。

方案2:必须用PHP处理时,放弃cURL,用非阻塞Socket实现双向转发

如果鉴权逻辑极其复杂无法前移到Web服务器层,必须用PHP实现,需要直接用PHP Socket扩展手写非阻塞双向转发逻辑,不能使用cURL:

  1. 核心实现逻辑:
    • 会话校验通过后,用socket_create建立到后端VNC服务的TCP连接
    • 读取客户端发来的WebSocket握手请求,原样转发给后端VNC服务
    • 读取后端返回的101握手响应,关闭输出缓冲立刻flush返回给客户端
    • 设置PHP脚本超时时间为0(永不超时),以非阻塞模式同时监听客户端、后端VNC两个连接的可读事件:
      • 客户端有数据到达就立刻转发给后端
      • 后端有数据返回就立刻转发给客户端
      • 任意一端主动断开连接,就关闭两端套接字,结束脚本
  2. 注意事项:
    • 该方案下一个WebSocket长连接会长期占用一个PHP进程(FPM/Apache工作进程),并发稍高就会占满PHP进程池,导致其他PHP请求无法响应,仅适合低并发内部场景使用
    • 必须关闭PHP、Web服务器的所有输出缓冲,否则会出现数据延迟
    • 需要调整PHP、Web服务器的长连接超时配置,避免连接被中途强制断开

内容的提问来源于stack exchange,提问作者Héctor

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 04:18:17