首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 實現 WebSocket 服務端完整指南:從握手協議到數據幀解析的純 Socket 實戰

     編輯:PHP 相關知識

PHP 無原生 WebSocket 支援,需基於 socket 擴展手動實現 RFC 6455 協議。本文完整拆解握手驗證、Sec-WebSocket-Key 加密、數據幀編解碼、事件循環四大核心,提供可直接運行的服務端類與客戶端測試代碼。

WebSocket 協議核心流程

  1. 客戶端發起 HTTP Upgrade 請求,攜帶 Sec-WebSocket-Key
  2. 服務端讀取 Key,拼接 GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11,SHA-1 → Base64 生成 Sec-WebSocket-Accept
  3. 服務端回應 101 Switching Protocols,連接升級為 WebSocket
  4. 雙向通信採用數據幀:文本/二進制/關閉/心跳幀,採用掩碼機制

完整服務端實現

<?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.jsPHP (純 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

生產環境建議

  • 不要裸用 socket_select:單進程輪詢無法支撐高併發,改用 Swoole、Workerman、ReactPHP 等事件驅動框架。
  • SSL/TLS (WSS):需在 Socket 層加入 OpenSSL 上下文,或反向代理 Nginx 終結 TLS。
  • 心跳保活:定時發送 Ping 幀(Opcode 0x9),客戶端回 Pong(0xA),檢測斷連。
  • 分片重組:大訊息會被拆分為多幀(FIN=0),需緩存重組。
  • 安全:驗證 Origin、限制連接數、輸入長度檢查、速率限制。

版本兼容性提醒

  • 現代瀏覽器均為 HyBi-13 / RFC 6455 (Version 13),僅需實現此版本。
  • 舊版 (Hixie-76) 需雙 Key 複雜加密,已棄用,無需支援。

純 PHP Socket 實現 WebSocket 是理解協議本質的最佳練習,但生產務必選用成熟框架,專注業務邏輯而非協議細節。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved