启用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
相关产品推荐
相关产品推荐

