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

PHP + HTML5 WebSocket 實現即時聊天室完整源碼解析

     編輯:PHP 綜合知識

WebSocket 實現瀏覽器與服務器雙向通信,是構建即時應用的關鍵技術。本文提供基於原生 PHP Socket 實現的聊天室完整服務端代碼,涵蓋握手協議、數據幂編解碼、用戶管理、消息廣播等核心邏輯。

服務端核心實現

<?php
error_reporting(E_ALL);
ob_implicit_flush();

$server = new ChatServer('127.0.0.1', 8000);
$server->run();

class ChatServer {
    public $sockets = [];
    public $users = [];
    public $master;

    public function __construct($address, $port) {
        $this->master = $this->createSocket($address, $port);
        $this->sockets['master'] = $this->master;
    }

    public function run() {
        while (true) {
            $changes = $this->sockets;
            socket_select($changes, $write = null, $except = null, null);
            
            foreach ($changes as $sock) {
                if ($sock === $this->master) {
                    $this->acceptConnection();
                } else {
                    $this->handleMessage($sock);
                }
            }
        }
    }

    // 接受新連接
    private function acceptConnection() {
        $client = socket_accept($this->master);
        $this->sockets[] = $client;
        $this->users[] = ['socket' => $client, 'handshaked' => false];
    }

    // 處理消息/握手
    private function handleMessage($sock) {
        $len = socket_recv($sock, $buffer, 2048, 0);
        $index = $this->findUserIndex($sock);
        
        if ($len < 7) { // 斷開連接
            $this->disconnect($index);
            return;
        }

        if (!$this->users[$index]['handshaked']) {
            $this->doHandshake($index, $buffer);
        } else {
            $data = $this->decode($buffer);
            $this->dispatch($index, $data);
        }
    }

    // WebSocket 握手協議
    private function doHandshake($index, $buffer) {
        $key = $this->extractSecKey($buffer);
        $accept = base64_encode(sha1($key . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true));
        
        $response = "HTTP/1.1 101 Switching Protocols\r\n"
                  . "Upgrade: websocket\r\n"
                  . "Sec-WebSocket-Version: 13\r\n"
                  . "Connection: Upgrade\r\n"
                  . "Sec-WebSocket-Accept: $accept\r\n\r\n";
        
        socket_write($this->users[$index]['socket'], $response);
        $this->users[$index]['handshaked'] = true;
    }

    // 數據幂解碼 (RFC 6455)
    private function decode($str) {
        $mask = [];
        $data = '';
        $msg = unpack('H*', $str);
        $head = substr($msg[1], 0, 2);
        
        if (hexdec($head[1]) === 8) return false; // 關閉幂
        
        if (hexdec($head[1]) === 1) { // 文本幂
            $mask[] = hexdec(substr($msg[1], 4, 2));
            $mask[] = hexdec(substr($msg[1], 6, 2));
            $mask[] = hexdec(substr($msg[1], 8, 2));
            $mask[] = hexdec(substr($msg[1], 10, 2));
            
            $s = 12;
            $e = strlen($msg[1]) - 2;
            for ($i = $s, $n = 0; $i <= $e; $i += 2, $n++) {
                $data .= chr($mask[$n % 4] ^ hexdec(substr($msg[1], $i, 2)));
            }
        }
        return $data;
    }

    // 數據幂編碼
    private function encode($msg) {
        $frame = [chr(129)]; // FIN=1, OPCODE=1 (text)
        $len = strlen($msg);
        $frame[] = $len < 126 ? chr($len) : ($len < 65536 ? chr(126) . pack('n', $len) : chr(127) . pack('J', $len));
        $frame[] = $msg;
        return implode('', $frame);
    }

    // 消息分發
    private function dispatch($index, $msg) {
        parse_str($msg, $data);
        
        if ($data['type'] === 'add') {
            $this->users[$index]['name'] = $data['ming'];
            $this->broadcast(['add' => true, 'msg' => "歡迎 {$data['ming']} 加入!", 'users' => $this->getUserList()]);
        } elseif ($data['type'] === 'ltiao') {
            $this->sendTo($data['key'], ['msg' => $data['nr']], $index);
        }
    }

    // 廣播/私聊發送
    private function broadcast($data, $exclude = null) {
        $encoded = $this->encode(json_encode($data));
        foreach ($this->users as $i => $user) {
            if ($i !== $exclude) socket_write($user['socket'], $encoded);
        }
    }

    private function sendTo($targetIndex, $data, $fromIndex) {
        $encoded = $this->encode(json_encode($data));
        socket_write($this->users[$targetIndex]['socket'], $encoded);
        socket_write($this->users[$fromIndex]['socket'], $encoded); // 回顯給自己
    }

    // 工具方法
    private function findUserIndex($sock) { /* ... */ }
    private function disconnect($index) { /* ... */ }
    private function getUserList() { /* ... */ }
    private function createSocket($addr, $port) { /* ... */ }
    private function extractSecKey($buffer) { /* ... */ }
}

關鍵技術點解析

1. WebSocket 握手流程

  1. 客戶端發送 Upgrade 請求,包含 Sec-WebSocket-Key
  2. 服務器拼接 GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11,SHA1 加密後 Base64
  3. 返回 101 狀態碼完成協議升級

2. 數據幂格式 (RFC 6455)

0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+---------------------------------------------------------------+

3. I/O 多路複用

socket_select() 監聽多個連接,單進程處理高併發,避免多進程/線程開銷。

生產環境改進建議

  • 使用成熟庫:Ratchet、Swoole、Workerman 提供穩健的事件驅動框架
  • 進程管理:Systemd/Supervisor 守護進程,支持平滑重啟
  • 水平擴展:Redis Pub/Sub 實現多進程/多服務器消息同步
  • 心跳檢測:定期 Ping/Pong 維持長連接,清理僵尸連接
  • SSL 支持:WSS 協議保障傳輸安全

前端接入示例

const ws = new WebSocket('ws://127.0.0.1:8000');
ws.onopen = () => ws.send('type=add&ming=' + encodeURIComponent(username));
ws.onmessage = (e) => { /* 渲染消息 */ };
ws.send('type=ltiao&nr=' + encodeURIComponent(msg) + '&key=' + targetId);

原生 Socket 實現適合學習協議細節,生產環境強烈建議使用 Swoole 或 Workerman 等成熟框架。

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