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

启用Stream Crypto的PHP WebSocket服务器HTTP请求字节丢失问题

PHP WebSocket服务器SSL/TLS启用后HTTP请求字节丢失问题

我用PHP 8.2搭建了WebSocket服务器,添加SSL/TLS功能后,启用stream_socket_enable_crypto能正常解密数据,但HTTP请求出现异常:

客户端发送的请求:

GET / HTTP/1.1
Host: example.com:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0
Accept: */*
...

服务器接收到的内容:

HTTP/1.1
Host: example.com:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0
Accept: */*
...

请问:字节丢失的原因是什么?如何恢复这些丢失的字节?


服务器相关代码

生成证书命令:

openssl genrsa -out wss.key 2048 && openssl req -key wss.key -new -x509 -days 365 -out wss.crt

PHP服务器代码:

error_reporting(E_ALL);
ini_set("display_errors", "1");

if(ob_get_level() == 0) ob_start();

$server = stream_socket_server("tcp://example.com:8080", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN);

stream_context_set_option($server, ["ssl" => [
    "local_cert" => __DIR__."/wss.crt",
    "local_pk" => __DIR__."/wss.key",
    "disable_compression" => true,
    "verify_peer" => false,
    "verify_peer_name" => false,
    "allow_self_signed" => true
]]);

$done = false;
while(!$done) {
    $sockets = ["server" => $server];

    if(stream_select($sockets, $w, $e, 0, 20) === false)
        show("ERROR");

    if(in_array($server, $sockets)) {
        $client = stream_socket_accept($server);
        $sockets[] = $client;
        stream_socket_enable_crypto($client, true, STREAM_CRYPTO_METHOD_TLS_SERVER);
    }

    foreach($sockets as $client) {
        if(!in_array($client, $sockets))
            continue;

        show(fread($client, 100));

        fclose($client);
        $done = true;
        show("EXIT");
    }
}

function show(...$data) {
    var_dump($data);
    @ob_flush();
    flush();
}

客户端JS代码:

url = "ws://example.com:8080";
socket = new WebSocket(url);
setTimeout(() => {
    socket.close();
    socket = new WebSocket(url);
}, 1000);

原因分析

问题核心在于TLS加密启用时机错误:客户端发起连接后会立即发送WebSocket握手的GET请求,而你的代码在stream_socket_accept之后才启用加密,此时客户端的请求数据已经抵达服务器缓冲区。这部分数据会被当作未加密的原始字节处理,而TLS握手过程会消耗或丢弃这部分数据,导致后续解密后的内容缺失开头的GET字节。

同时,代码中sockets数组混合了服务器socket和客户端socket,stream_select的处理逻辑混乱,进一步加剧了数据读取不完整的问题。

解决方案

1. 提前完成TLS握手

必须在客户端发送任何数据前完成TLS握手,也就是在stream_socket_accept后立即调用stream_socket_enable_crypto并等待握手完成:

if(in_array($server, $readSockets)) {
    $client = stream_socket_accept($server);
    if (!$client) continue;
    // 强制等待TLS握手完成
    if (!stream_socket_enable_crypto($client, true, STREAM_CRYPTO_METHOD_TLS_SERVER)) {
        show("TLS握手失败");
        fclose($client);
        continue;
    }
    $clients[] = $client;
}

2. 修复Socket处理逻辑

分开维护服务器socket和客户端socket,避免混合处理导致的逻辑错误:

$done = false;
$clients = []; // 单独维护客户端连接数组
while(!$done) {
    $readSockets = array_merge([$server], $clients);
    $writeSockets = [];
    $exceptSockets = [];

    if(stream_select($readSockets, $writeSockets, $exceptSockets, null) === false) {
        show("stream_select错误");
        break;
    }

    // 处理新连接
    if(in_array($server, $readSockets)) {
        $client = stream_socket_accept($server);
        if (!$client) continue;
        // 完成TLS握手
        if (!stream_socket_enable_crypto($client, true, STREAM_CRYPTO_METHOD_TLS_SERVER)) {
            show("TLS握手失败");
            fclose($client);
            continue;
        }
        $clients[] = $client;
        // 移除服务器socket避免重复处理
        $key = array_search($server, $readSockets);
        unset($readSockets[$key]);
    }

    // 处理客户端数据
    foreach($readSockets as $client) {
        $data = fread($client, 8192); // 增大读取缓冲区
        if ($data === false || $data === '') {
            // 客户端断开
            $key = array_search($client, $clients);
            unset($clients[$key]);
            fclose($client);
            $done = true;
            show("客户端断开,退出");
            continue;
        }
        show("收到数据:", $data);
        fclose($client);
        $done = true;
        show("EXIT");
    }
}

3. 客户端使用WSS协议

服务器启用SSL/TLS后,客户端必须使用wss://协议连接,否则会出现加密不匹配问题:

url = "wss://example.com:8080";
socket = new WebSocket(url);
setTimeout(() => {
    socket.close();
    socket = new WebSocket(url);
}, 1000);

内容的提问来源于stack exchange,提问作者Play_it

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 04:45:57