PHP 無原生 WebSocket 支援,需基於 socket 擴展手動實現 RFC 6455 協議。本文完整拆解握手驗證、Sec-WebSocket-Key 加密、數據幀編解碼、事件循環四大核心,提供可直接運行的服務端類與客戶端測試代碼。
Sec-WebSocket-Key258EAFA5-E914-47DA-95CA-C5AB0DC85B11,SHA-1 → Base64 生成 Sec-WebSocket-Accept101 Switching Protocols,連接升級為 WebSocket<?php
class WebSocketServer {
private $master;
private $sockets = [];
private $handshake = false;
private $clients = [];
public function __construct(string $address = '0.0.0.0', int $port = 4000) {
$this->master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($this->master, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($this->master, $address, $port);
socket_listen($this->master, 5);
$this->sockets[] = $this->master;
echo "Server started on $address:$port\n";
$this->loop();
}
private function loop() {
while (true) {
$read = $this->sockets;
$write = $except = null;
socket_select($read, $write, $except, null);
foreach ($read as $socket) {
if ($socket === $this->master) {
$this->acceptClient();
} else {
$this->handleClient($socket);
}
}
}
}
private function acceptClient() {
$client = socket_accept($this->master);
if ($client === false) return;
$this->sockets[] = $client;
$this->clients[$client] = ['handshake' => false];
echo "New client connected\n";
}
private function handleClient($socket) {
$bytes = @socket_recv($socket, $buffer, 4096, 0);
if ($bytes === 0 || $bytes === false) {
$this->closeClient($socket);
return;
}
$client = &$this->clients[$socket];
if (!$client['handshake']) {
$this->doHandshake($socket, $buffer);
} else {
$data = $this->decodeFrame($buffer);
$this->processMessage($socket, $data);
}
}
// 1. 提取 Sec-WebSocket-Key
private function getKey(string $req): ?string {
if (preg_match('/Sec-WebSocket-Key:\s*(.+)\r\n/i', $req, $m)) return trim($m[1]);
return null;
}
// 2. 加密生成 Accept
private function encryptKey(string $key): string {
$guid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
return base64_encode(sha1($key . $guid, true));
}
// 3. 完成握手
private function doHandshake($socket, string $req) {
$key = $this->getKey($req);
if (!$key) { $this->closeClient($socket); return; }
$accept = $this->encryptKey($key);
$response = "HTTP/1.1 101 Switching Protocols\r\n" .
"Upgrade: websocket\r\n" .
"Connection: Upgrade\r\n" .
"Sec-WebSocket-Accept: $accept\r\n\r\n";
socket_write($socket, $response, strlen($response));
$this->clients[$socket]['handshake'] = true;
echo "Handshake done\n";
}
// 4. 解碼數據幀 (RFC 6455)
private function decodeFrame(string $buffer): string {
$len = ord($buffer[1]) & 127;
$offset = 2;
if ($len === 126) { $offset = 4; $mask = substr($buffer, 4, 4); $data = substr($buffer, 8); }
elseif ($len === 127) { $offset = 10; $mask = substr($buffer, 10, 4); $data = substr($buffer, 14); }
else { $mask = substr($buffer, 2, 4); $data = substr($buffer, 6); }
$decoded = '';
for ($i = 0; $i < strlen($data); $i++) {
$decoded .= $data[$i] ^ $mask[$i % 4];
}
return $decoded;
}
// 5. 編碼發送幀
private function encodeFrame(string $msg): string {
$frame = chr(0x81); // FIN=1, Opcode=1 (text)
$len = strlen($msg);
if ($len <= 125) $frame .= chr($len);
elseif ($len <= 65535) $frame .= chr(126) . pack('n', $len);
else $frame .= chr(127) . pack('J', $len);
return $frame . $msg;
}
private function processMessage($socket, string $data) {
echo "Received: $data\n";
// Echo 回傳(聊天室模式)
$frame = $this->encodeFrame($data);
socket_write($socket, $frame, strlen($frame));
}
private function closeClient($socket) {
socket_close($socket);
$idx = array_search($socket, $this->sockets, true);
if ($idx !== false) unset($this->sockets[$idx]);
unset($this->clients[$socket]);
echo "Client disconnected\n";
}
}
// 啟動
$server = new WebSocketServer('0.0.0.0', 4000);
<!DOCTYPE html>
<html>
<script>
const ws = new WebSocket('ws://localhost:4000');
ws.onopen = () => console.log('握手成功');
ws.onmessage = e => console.log('收到:', e.data);
ws.onerror = e => console.error('錯誤:', e);
ws.onclose = () => console.log('連接關閉');
// 測試發送
setTimeout(() => ws.send('Hello from browser!'), 1000);
</script>
</html>
| 環節 | Node.js | PHP (純 Socket) | 難點 |
|---|---|---|---|
| 建立連接 | net.createServer() 一行 | socket_create/bind/listen/accept 多步 | 流程繁瑣 |
| 握手驗證 | crypto 模組幾行 | 正則提取 Key → SHA1+Base64 → 手寫響應頭 | 易漏 \r\n\r\n 導致失敗 |
| 數據幀解析 | ws 庫自動處理 | 手寫掩碼解碼、長度解析、分片重組 | RFC 細節多 |
| 併發模型 | Event Loop 原生 | socket_select() 轮询/多進程/ Swoole | 高併發需 Swoole/Workerman |
純 PHP Socket 實現 WebSocket 是理解協議本質的最佳練習,但生產務必選用成熟框架,專注業務邏輯而非協議細節。