PHP原生实现websocket在线聊天室

发布于:2025-09-05 ⋅ 阅读:(15) ⋅ 点赞:(0)
一、为什么写这篇文章

websocket几乎是每一名前端和后端程序员都逃不过的一个节点,但没接触过的话,真的会完全一头雾水,根本不知道从哪里下手,尤其是来了相关开发任务时。

我一直坚信,看100篇文章,学100个教程,不如自己动手实操一遍。跑通整个流程,你自然就对其一知半解了,这之后再去研究源码是怎么实现的,就会事半功倍。

但要注意的是,本代码仅供初步了解websocket的前后端交互逻辑,以及代码实现,它仅仅是一个demo,请勿用于生产环境,生产环境请使用更为可靠更为成熟的Swoole/Workerman/Golang + Gorilla WebSocket 来实现。

二、实现效果
1、前端用户页面

在这里插入图片描述

2、PHP后台cli

在这里插入图片描述

三、开发环境部署
1、开发环境介绍

PHP:8.4.12(docker容器:haveyb/php)

Nginx:1.27(docker容器:openresty/openresty)

另外提一嘴,现在阿里源清华源163源等国内docker镜像源都用不了了,但还是有一些能用的,可以参考 docker更换国内加速镜像源2025

2、拉取PHP镜像
docker pull haveyb/php
3、创建PHP容器
docker run -itd --name php --privileged -p 9000:9000 -p 9501:9501 -v ~/Desktop/code/docker:/data haveyb/php
4、拉取openresty镜像

openresty是一个比较成熟的nginx+lua结合体的项目

docker pull openresty/openresty
5、创建nginx容器
docker run -itd --name openresty --privileged -p 80:80 -p 443:443 -v ~/Desktop/code/docker:/data openresty/openresty
6、宿主机创建docker网络,用于容器通信
docker network create --driver bridge haveyb
7、PHP容器加入该网络
docker network connect haveyb php
8、Nginx容器加入网络
docker network connect haveyb openresty

原文链接:PHP原生实现websocket在线聊天室

四、代码部分
1、前端代码 index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket聊天</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
    <script>
        tailwind.config = {
            theme: {
                extend: {
                    colors: {
                        primary: '#3B82F6',
                        secondary: '#10B981',
                        system: '#6B7280',
                        background: '#F3F4F6',
                    },
                    fontFamily: {
                        sans: ['Inter', 'system-ui', 'sans-serif'],
                    },
                }
            }
        }
    </script>
    <style type="text/tailwindcss">
        @layer utilities {
            .content-auto {
                content-visibility: auto;
            }
            .message-container {
                @apply w-full h-[70vh] overflow-y-auto p-4 bg-background rounded-t-lg;
            }
            .message {
                @apply max-w-[80%] my-2 px-4 py-2 rounded-lg;
            }
            .message-sent {
                @apply bg-secondary text-white ml-auto;
            }
            .message-received {
                @apply bg-white text-gray-800 mr-auto border border-gray-200;
            }
            .message-system {
                @apply bg-system text-white mx-auto text-sm;
            }
            .input-area {
                @apply w-full p-4 bg-white rounded-b-lg border-t border-gray-200 flex gap-2;
            }
            .chat-container {
                @apply max-w-2xl mx-auto mt-8 border border-gray-300 rounded-lg shadow-lg overflow-hidden;
            }
        }
    </style>
</head>
<body class="bg-gray-100">
<div class="chat-container">
    <div class="bg-primary text-white p-4">
        <h1 class="text-xl font-bold flex items-center">
            <i class="fa fa-comments-o mr-2"></i>WebSocket实时聊天
        </h1>
        <div class="text-sm mt-1 flex items-center" id="connectionStatus">
            <span class="inline-block w-2 h-2 bg-red-400 rounded-full mr-2" id="statusIndicator"></span>
            <span>未连接</span>
        </div>
    </div>

    <!-- 消息容器 -->
    <div id="messages" class="message-container"></div>

    <!-- 输入区域 -->
    <div class="input-area">
        <input
                type="text"
                id="messageInput"
                placeholder="输入消息后按回车发送..."
                class="flex-1 px-4 py-2 border border-gray-300 rounded-full focus:outline-none focus:ring-2 focus:ring-primary"
        >
        <button
                onclick="sendMessage()"
                class="bg-primary hover:bg-primary/90 text-white px-6 py-2 rounded-full transition-all"
        >
            <i class="fa fa-paper-plane"></i>
        </button>
    </div>
</div>

<script>
    // 全局变量
    let websocket = null;
    let messagesContainer = null;
    let messageInput = null;
    let statusIndicator = null;

    // 页面加载完成后初始化
    window.onload = function() {
        // 初始化DOM元素引用
        messagesContainer = document.getElementById('messages');
        messageInput = document.getElementById('messageInput');
        statusIndicator = document.getElementById('statusIndicator');

        // 验证必要元素
        if (!messagesContainer) {
            console.error('错误:未找到消息容器元素');
            alert('页面加载出错,请刷新重试');
            return;
        }

        // 绑定回车键发送
        messageInput.addEventListener('keypress', function(e) {
            if (e.key === 'Enter') {
                sendMessage();
            }
        });

        // 初始化WebSocket连接
        initWebSocket();
    };

    // 初始化WebSocket连接 - 重点修改这里
    function initWebSocket() {
        // ********** 根据你的实际配置修改这里 **********
        // 替换为你的Nginx服务器域名或IP,以及对应的wss路径
        // 例如:你的Nginx配置了server_name test,则使用下面的地址
        const wsUri = 'wss://test/wss';

        console.log('尝试连接到WebSocket服务器:', wsUri);
        websocket = new WebSocket(wsUri);

        // 连接成功回调
        websocket.onopen = function() {
            console.log('WebSocket连接已打开');
            updateConnectionStatus(true);
            addMessage('system', '已成功连接到聊天服务器');
        };

        // 接收消息回调
        websocket.onmessage = function(evt) {
            console.log('收到消息:', evt.data);
            addMessage('received', evt.data);
        };

        // 连接关闭回调
        websocket.onclose = function() {
            console.log('WebSocket连接已关闭');
            updateConnectionStatus(false);
            addMessage('system', '与服务器的连接已断开,3秒后尝试重连...');
            setTimeout(initWebSocket, 3000);
        };

        // 错误回调
        websocket.onerror = function(error) {
            console.error('WebSocket错误:', error);
            addMessage('system', '连接发生错误,请检查网络');
        };
    }

    // 发送消息
    function sendMessage() {
        if (!messageInput) return;

        const message = messageInput.value.trim();
        if (!message) return;

        if (!websocket || websocket.readyState !== WebSocket.OPEN) {
            addMessage('system', '连接未就绪,请稍后再试');
            return;
        }

        try {
            websocket.send(message);
            addMessage('sent', message);
            messageInput.value = '';
        } catch (e) {
            console.error('发送消息失败:', e);
            addMessage('system', '消息发送失败');
        }
    }

    // 添加消息到界面
    function addMessage(type, content) {
        if (!messagesContainer) {
            console.error('消息容器不存在,无法添加消息');
            return;
        }

        const messageDiv = document.createElement('div');
        messageDiv.className = `message message-${type}`;
        messageDiv.textContent = content;

        messagesContainer.appendChild(messageDiv);
        messagesContainer.scrollTop = messagesContainer.scrollHeight;
    }

    // 更新连接状态显示
    function updateConnectionStatus(isConnected) {
        if (!statusIndicator) return;

        if (isConnected) {
            statusIndicator.className = 'inline-block w-2 h-2 bg-green-400 rounded-full mr-2';
            statusIndicator.nextElementSibling.textContent = '已连接';
        } else {
            statusIndicator.className = 'inline-block w-2 h-2 bg-red-400 rounded-full mr-2';
            statusIndicator.nextElementSibling.textContent = '未连接';
        }
    }
</script>
</body>
</html>
2、PHP代码 websocket_server.php
<?php
class WebSocketServer {
    private $host;
    private $port;
    private $serverSocket;
    private $clients = [];  // 存储客户端socket(键为对象ID,值为Socket对象)
    private $clientCount = 0;

    public function __construct($host = '0.0.0.0', $port = 9501) {
        $this->host = $host;
        $this->port = $port;
        $this->initServer();
    }

    // 初始化服务器socket
    private function initServer() {
        // 创建服务器socket(PHP 8.1+ 返回Socket对象)
        $this->serverSocket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        if (!$this->serverSocket) {
            die("Socket创建失败: " . socket_strerror(socket_last_error()));
        }

        // 设置socket选项
        socket_set_option($this->serverSocket, SOL_SOCKET, SO_REUSEADDR, 1);
        socket_set_nonblock($this->serverSocket);

        // 绑定并监听
        if (!socket_bind($this->serverSocket, $this->host, $this->port)) {
            die("Socket绑定失败: " . socket_strerror(socket_last_error()));
        }
        if (!socket_listen($this->serverSocket, 10)) {
            die("Socket监听失败: " . socket_strerror(socket_last_error()));
        }

        echo "WebSocket服务器已启动,监听 {$this->host}:{$this->port}\n";
        echo "等待客户端连接...\n";
    }

    // 主运行循环
    public function run() {
        while (true) {
            $readSockets = $this->getAllSockets();
            $writeSockets = null;
            $exceptSockets = null;

            // 监听可读socket
            $numChanged = socket_select($readSockets, $writeSockets, $exceptSockets, 1);
            if ($numChanged === false) {
                echo "socket_select错误: " . socket_strerror(socket_last_error()) . "\n";
                continue;
            }
            if ($numChanged === 0) {
                continue;
            }

            // 处理可读socket
            foreach ($readSockets as $socket) {
                if ($socket === $this->serverSocket) {
                    $this->handleNewConnection();  // 新客户端连接
                } else {
                    $this->handleClientMessage($socket);  // 已有客户端消息
                }
            }
        }
    }

    // 获取所有需要监听的socket
    private function getAllSockets() {
        $sockets = [$this->serverSocket];
        // 只保留有效的客户端socket
        foreach ($this->clients as $socket) {
            if ($socket instanceof Socket) {  // 检查是否为Socket对象
                $sockets[] = $socket;
            }
        }
        return $sockets;
    }

    // 处理新客户端连接
    private function handleNewConnection() {
        // 接受新连接(PHP 8.1+ 返回Socket对象)
        $newClient = @socket_accept($this->serverSocket);
        if (!$newClient || !($newClient instanceof Socket)) {
            echo "新客户端连接无效(非Socket对象)\n";
            return;
        }

        // 关键修复:用spl_object_id()获取对象唯一ID(替代(int)转换)
        $clientId = spl_object_id($newClient);
        echo "新客户端尝试连接,Socket对象ID: {$clientId}\n";

        // 执行握手
        if ($this->performHandshake($newClient)) {
            // 用对象ID作为键存储客户端
            $this->clients[$clientId] = $newClient;
            $this->clientCount = count($this->clients);
            echo "✅ 新客户端连接成功,当前在线: {$this->clientCount}\n";
            // 广播新用户加入
            $this->broadcast("系统通知: 新用户加入,当前在线{$this->clientCount}人", $newClient);
        } else {
            // 握手失败,关闭连接
            socket_close($newClient);
            echo "❌ 客户端握手失败,已关闭连接\n";
        }
    }

    // WebSocket握手
    private function performHandshake($client) {
        // 读取握手请求
        $request = '';
        while (true) {
            $buffer = @socket_read($client, 1024);
            if ($buffer === false || $buffer === '') break;
            $request .= $buffer;
            if (strpos($request, "\r\n\r\n") !== false) break;
        }

        // 验证握手请求头
        if (!preg_match('#Sec-WebSocket-Key: (.*?)\r\n#i', $request, $keyMatch) ||
            !preg_match('#Sec-WebSocket-Version: (.*?)\r\n#i', $request, $versionMatch)) {
            error_log("握手失败:缺失Sec-WebSocket-Key或Version");
            return false;
        }

        $secKey = trim($keyMatch[1]);
        $secVersion = trim($versionMatch[1]);

        // 仅支持版本13
        if ($secVersion != 13) {
            error_log("握手失败:不支持的版本,客户端版本: {$secVersion}");
            return false;
        }

        // 生成响应密钥
        $secAccept = base64_encode(sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));

        // 构建响应
        $response = "HTTP/1.1 101 Switching Protocols\r\n";
        $response .= "Upgrade: websocket\r\n";
        $response .= "Connection: Upgrade\r\n";
        $response .= "Sec-WebSocket-Accept: {$secAccept}\r\n\r\n";

        // 发送响应
        if (!socket_write($client, $response, strlen($response))) {
            error_log("握手响应发送失败");
            return false;
        }

        return true;
    }

    // 处理客户端消息
    private function handleClientMessage($client) {
        // 读取消息帧
        $buffer = @socket_read($client, 4096);
        if ($buffer === false || $buffer === '') {
            // 连接断开
            $this->removeClient($client);
            return;
        }

        // 解析消息
        $message = $this->decodeWebSocketFrame($buffer, $client);
        if ($message === false) {
            return;
        }
        $clientId = spl_object_id($client);
        echo PHP_EOL;
        echo "📥 收到客户端{$clientId}消息: {$message}\n";
        // 广播消息(排除发送者)
        $this->broadcast($message, $client);
    }

    // 解析WebSocket帧
    private function decodeWebSocketFrame($frame, $client) {
        $firstByte = ord($frame[0]);
        $secondByte = ord($frame[1]);

        // 检查是否为最后一帧
        $isFinal = ($firstByte & 0x80) != 0;
        if (!$isFinal) {
            error_log("不支持分片消息");
            return false;
        }

        // 处理关闭帧
        $opcode = $firstByte & 0x0F;
        if ($opcode == 8) {
            echo "🔌 收到客户端关闭请求\n";
            $this->sendCloseFrame($client);
            $this->removeClient($client);
            return false;
        }

        // 只支持文本消息
        if ($opcode != 1) {
            error_log("不支持的消息类型,Opcode: {$opcode}");
            return false;
        }

        // 客户端消息必须带掩码
        $hasMask = ($secondByte & 0x80) != 0;
        if (!$hasMask) {
            error_log("客户端消息必须带掩码");
            return false;
        }

        // 解析 payload 长度(简化处理<=125字节)
        $payloadLen = $secondByte & 0x7F;
        if ($payloadLen > 125) {
            error_log("不支持超过125字节的消息");
            return false;
        }

        // 解掩码
        $maskOffset = 2;
        $dataOffset = $maskOffset + 4;
        $mask = substr($frame, $maskOffset, 4);
        $payload = substr($frame, $dataOffset, $payloadLen);
        $message = '';
        for ($i = 0; $i < $payloadLen; $i++) {
            $message .= chr(ord($payload[$i]) ^ ord($mask[$i % 4]));
        }

        return $message;
    }

    // 发送关闭帧
    private function sendCloseFrame($client) {
        if ($client instanceof Socket) {
            $closeFrame = chr(0x88) . chr(0x00); // 标准关闭帧
            socket_write($client, $closeFrame, strlen($closeFrame));
        }
    }

    // 广播消息
    private function broadcast($message, $excludeClient = null) {
        echo "\n=== 开始广播 ===";
        echo "\n广播内容: {$message}";
        echo "\n在线客户端数: {$this->clientCount}";

        // 编码消息
        $encodedMsg = $this->encodeWebSocketFrame($message);
        if (!$encodedMsg) {
            echo "\n广播失败:消息编码无效";
            echo "\n=== 广播结束 ===\n";
            return;
        }

        // 遍历所有客户端
        foreach ($this->clients as $client) {
            // 跳过发送者和无效客户端
            if ($client === $excludeClient || !($client instanceof Socket)) {
                continue;
            }

            // 发送消息
            $sendLen = @socket_write($client, $encodedMsg, strlen($encodedMsg));
            if ($sendLen === false) {
                $clientId = spl_object_id($client);
                echo "\n发送失败: 客户端ID {$clientId},错误: " . socket_strerror(socket_last_error($client));
                $this->removeClient($client);
            } else {
                $clientId = spl_object_id($client);
                echo "\n发送成功: 客户端ID {$clientId},发送字节: {$sendLen}";
            }
        }

        echo "\n=== 广播结束 ===\n";
    }

    // 编码WebSocket帧
    private function encodeWebSocketFrame($message) {
        $len = strlen($message);
        $frame = '';

        // 第一字节:FIN=1,Opcode=1(文本)
        $frame .= chr(0x81);

        // 第二字节:Mask=0,长度
        if ($len <= 125) {
            $frame .= chr($len);
        } elseif ($len <= 65535) {
            $frame .= chr(126) . pack('n', $len);
        } else {
            $frame .= chr(127) . pack('J', $len);
        }

        // 消息内容
        $frame .= $message;

        return $frame;
    }

    // 移除客户端
    private function removeClient($client) {
        if (!($client instanceof Socket)) {
            echo "\n忽略非Socket对象的移除请求";
            return;
        }

        $clientId = spl_object_id($client);
        echo "\n🔌 准备移除客户端,ID: {$clientId}";

        // 关闭socket
        socket_shutdown($client, 2);
        socket_close($client);
        echo "\n已关闭客户端socket";

        // 从列表中删除
        if (isset($this->clients[$clientId])) {
            unset($this->clients[$clientId]);
            $this->clientCount = count($this->clients);
            echo "\n客户端已从列表移除,当前在线: {$this->clientCount}";
            // 广播用户离开
            $this->broadcast("系统通知: 用户离开,当前在线{$this->clientCount}人");
        } else {
            echo "\n客户端不在列表中";
        }

        echo "\n================\n";
    }

    // 析构函数:清理资源
    public function __destruct() {
        // 关闭所有客户端
        foreach ($this->clients as $client) {
            if ($client instanceof Socket) {
                socket_close($client);
            }
        }
        // 关闭服务器socket
        if ($this->serverSocket instanceof Socket) {
            socket_close($this->serverSocket);
        }
    }
}

// 启动服务器
$server = new WebSocketServer('0.0.0.0', 9501);
$server->run();

3、Nginx配置文件
(1)宿主机创建test.conf 配置文件
cd ~/Desktop/code/docker/test
touch test.conf
server {
    listen 80;
    server_name test;

    # HTTP 重定向到 HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl;
    http2 on;
    server_name test;

    # SSL 证书配置
    ssl_certificate /usr/local/openresty/nginx/cert/test+3.pem;
    ssl_certificate_key /usr/local/openresty/nginx/cert/test+3-key.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    # 基础参数
    charset utf-8;
    root /data/test;
    index index.html index.php;
    error_log /usr/local/openresty/nginx/logs/error-test.log error;

    # 重定向控制
    server_name_in_redirect off;
    absolute_redirect off;

    # WebSocket配置(核心部分)
   location /wss {
       proxy_pass http://php:9501;  # 用容器名,比IP更稳定

       # 强制转发所有请求头(包括WebSocket握手必需的头)
       proxy_pass_request_headers on;

       # WebSocket协议升级必需
       proxy_http_version 1.1;
       proxy_set_header Upgrade $http_upgrade;
       proxy_set_header Connection "Upgrade";
       proxy_set_header Host $host;
       proxy_set_header X-Real-IP $remote_addr;
       proxy_set_header Sec-WebSocket-Key $http_sec_websocket_key;  # 关键:转发Sec-WebSocket-Key
       proxy_set_header Sec-WebSocket-Version $http_sec_websocket_version;  # 转发版本号

       # 3. 禁用缓存(避免连接被缓存中断)
       proxy_cache off;
       proxy_buffering off;
       proxy_set_header Cache-Control "no-cache";

       proxy_read_timeout 86400s;
       proxy_send_timeout 86400s;
   }

    # 主location规则
    location / {
        try_files $uri $uri/ @phpfallback;
    }

    # PHP处理(安全增强版)
    location ~ \.php$ {
        fastcgi_pass php:9000;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;

        # 安全限制
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_intercept_errors on;
        try_files $uri =404;  # 防止任意文件执行
    }

    location @phpfallback {
        rewrite ^ /index.php?$query_string last;
    }

    # 静态资源优化
    location ~* \.(js|css|png|ico)$ {
        expires 30d;
        access_log off;
        add_header Cache-Control "public";
    }

    # 安全防护
    location ~ /\.(?!well-known) {
        deny all;
    }
    location = /favicon.ico {
        log_not_found off;
        access_log off;
    }
}
(2)加载配置文件

进入nginx容器

docker exec -it openresty bash
cp /data/test/test.conf /etc/nginx/conf.d/
nginx -t
nginx -s reload

注:/usr/local/openresty/nginx/conf/nginx.conf文件里定义了include /etc/nginx/conf.d/*.conf;,你也可以修改nginx.conf添加其他配置文件目录。

(3)注意事项

还有需要注意的的是,可以看到,我在配置文件里添加了https的解析,这是我的个人偏好,如果你嫌麻烦,也可以将https的配置去掉,只对80端口进行监听。

如果你也想在本地配置https,可以参考文章 Macbook 本地生成 SSL 证书,根治谷歌浏览器搜索栏出现红色不安全感叹号

但要注意的是,有些浏览器是不支持本地https的,比如谷歌和edge浏览器,因此如果你配置了本地https,那么你可以用QQ浏览器或safari浏览器访问测试站点。

五、使用
1、PHP所在容器或所在环境运行脚本,进而开启websocket监听处理
php websocket_server.php
2、本地配置hosts
127.0.0.1       	test
3、浏览器访问
https://test
4、代码实现了什么

前后端代码结合实现了一个基于WebSocket的实时聊天系统,前端通过HTML/JavaScript构建聊天界面并与后端通信,后端通过PHP实现WebSocket服务器处理多客户端连接、消息转发等核心逻辑。在实际业务中,这类系统可用于即时通讯(如客服聊天、团队协作工具)、实时通知(如订单状态更新、消息推送)等场景。

实时消息交互:支持多用户同时在线聊天,消息发送后无需刷新页面即可实时接收。

连接状态管理:前端实时展示连接状态(已连接/未连接),断开后自动重试,提升用户体验。

系统通知同步:新用户加入、用户离开等系统事件会广播给所有在线客户端,保持全局状态一致。

多客户端支持:后端通过非阻塞Socket监听多个客户端连接,实现消息“广播”(将某用户消息转发给其他所有用户)。

六、前后端交互逻辑

前后端交互基于WebSocket协议,分为“连接建立(握手)- 消息收发 - 连接关闭”三个核心阶段,具体流程如下:

1、WebSocket连接建立(握手阶段)

WebSocket是基于TCP的应用层协议,建立连接前需先完成“HTTP握手”,确保客户端与服务器协议一致。

前端初始化连接

  • 页面加载完成后,前端通过initWebSocket()创建WebSocket实例,请求地址为wss://test/wss(通过Nginx反向代理指向后端PHP服务器的9501端口)。
  • 示例:websocket = new WebSocket(wsUri),触发后端的“新连接监听”。

后端处理握手

  • 后端PHP服务器通过socket_listen(9501端口)监听新连接,当收到前端请求时,通过handleNewConnection()接受连接。
  • 后端读取前端发送的HTTP握手请求头(含Sec-WebSocket-KeySec-WebSocket-Version等关键字段),验证版本(仅支持13版)并生成Sec-WebSocket-Accept响应密钥。
  • 后端返回HTTP 101响应(状态码“Switching Protocols”),告知客户端切换到WebSocket协议,握手成功。

连接成功回调

  • 前端websocket.onopen触发,更新界面状态为“已连接”,并添加“连接成功”的系统消息。
  • 后端将新客户端的Socket对象存入$clients数组,广播“新用户加入”通知给其他在线客户端。
2、实时消息收发(核心交互阶段)

连接建立后,前后端可双向发送二进制/文本消息,这里仅支持文本消息

前端发送逻辑

  • 用户输入消息后,点击“发送”按钮或按回车触发sendMessage()
  • 校验逻辑:若消息为空或连接未就绪(如websocket.readyState !== WebSocket.OPEN),则提示错误;否则通过websocket.send(message)将消息发送给后端。
  • 前端同时将发送的消息添加到界面(标为“已发送”样式message-sent)。

后端接收与广播

  • 后端通过socket_select()监听客户端Socket的“可读事件”,当收到消息时触发handleClientMessage()
  • 后端通过decodeWebSocketFrame()解析WebSocket消息帧(需解掩码,因为客户端消息必须带掩码),提取文本内容。
  • 后端调用broadcast()将消息编码为WebSocket帧(无需掩码,服务器消息不强制掩码),并转发给$clients数组中除发送者外的所有客户端。

前端接收逻辑

  • 当后端广播消息时,前端websocket.onmessage触发,接收evt.data(后端转发的文本消息)。
  • 前端通过addMessage('received', evt.data)将消息添加到界面(标为“已接收”样式message-received),并自动滚动到最新消息。

连接主动关闭

  • 客户端关闭页面时,浏览器自动发送WebSocket关闭帧(Opcode=8),后端decodeWebSocketFrame()检测到后,通过removeClient()关闭Socket、从$clients中删除该客户端,并广播“用户离开”通知。

连接异常断开

  • 若网络中断或服务器异常,前端websocket.onclose触发,更新界面状态为“未连接”,并通过setTimeout(initWebSocket, 3000)每3秒自动重试连接。
  • 后端通过socket_read()返回空值检测到客户端断开,同样执行removeClient()清理资源。

网站公告

今日签到

点亮在社区的每一天
去签到