PHP WebSocket聊天室

wen PHP项目 2

本文目录导读:

PHP WebSocket聊天室

  1. 项目结构
  2. 安装依赖
  3. 服务器端代码
  4. 前端代码 public/index.html
  5. 使用方法
  6. 功能特性
  7. 安全建议

我来为你创建一个完整的PHP WebSocket聊天室实现,这个方案使用Ratchet库,包含完整的服务器端和客户端代码。

项目结构

chat-room/
├── server.php          # WebSocket服务器
├── chat-server.php     # 聊天服务器主逻辑
├── public/
│   └── index.html     # 前端聊天界面
├── composer.json      # Composer依赖配置
└── src/
    ├── Chat.php       # 聊天逻辑处理
    └── User.php       # 用户管理类

安装依赖

首先创建 composer.json

{
    "require": {
        "cboden/ratchet": "^0.4.4"
    },
    "autoload": {
        "psr-4": {
            "ChatRoom\\": "src/"
        }
    }
}

运行安装命令:

composer install

服务器端代码

server.php(入口文件)

<?php
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use ChatRoom\Chat;
require_once __DIR__ . '/vendor/autoload.php';
$server = IoServer::factory(
    new HttpServer(
        new WsServer(
            new Chat()
        )
    ),
    8080
);
echo "聊天服务器启动在端口 8080...\n";
$server->run();

src/Chat.php(聊天核心逻辑)

<?php
namespace ChatRoom;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use SplObjectStorage;
class Chat implements MessageComponentInterface {
    protected $clients;
    protected $users;
    public function __construct() {
        // 存储所有连接的客户端
        $this->clients = new SplObjectStorage;
        // 存储用户信息
        $this->users = [];
    }
    // 新连接建立时
    public function onOpen(ConnectionInterface $conn) {
        // 添加新连接
        $this->clients->attach($conn);
        // 生成用户ID
        $userId = uniqid('user_');
        $conn->resourceId = $userId;
        // 发送欢迎消息
        $conn->send(json_encode([
            'type' => 'system',
            'message' => '欢迎来到聊天室!请设置你的昵称。',
            'userId' => $userId
        ]));
        echo "新连接: {$conn->resourceId}\n";
    }
    // 接收消息时
    public function onMessage(ConnectionInterface $from, $msg) {
        $data = json_decode($msg, true);
        if (!$data) {
            return;
        }
        // 根据消息类型处理
        switch ($data['type']) {
            case 'join':
                $this->handleJoin($from, $data);
                break;
            case 'chat':
                $this->handleChat($from, $data);
                break;
            case 'private':
                $this->handlePrivateMessage($from, $data);
                break;
            case 'userlist':
                $this->sendUserList($from);
                break;
            case 'typing':
                $this->handleTyping($from, $data);
                break;
            case 'disconnect':
                $this->handleDisconnect($from);
                break;
        }
    }
    // 处理用户加入
    private function handleJoin(ConnectionInterface $conn, $data) {
        if (!isset($data['username']) || empty($data['username'])) {
            $conn->send(json_encode([
                'type' => 'error',
                'message' => '昵称不能为空'
            ]));
            return;
        }
        $username = htmlspecialchars($data['username']);
        // 检查昵称是否唯一
        foreach ($this->users as $user) {
            if ($user['username'] === $username) {
                $conn->send(json_encode([
                    'type' => 'error',
                    'message' => '该昵称已被使用'
                ]));
                return;
            }
        }
        // 保存用户信息
        $this->users[$conn->resourceId] = [
            'connection' => $conn,
            'username' => $username
        ];
        // 向该用户发送加入成功消息
        $conn->send(json_encode([
            'type' => 'system',
            'message' => "你好,{$username}!欢迎加入聊天室。"
        ]));
        // 向其他用户广播新用户加入
        $this->broadcast([
            'type' => 'system',
            'message' => "{$username} 加入了聊天室"
        ], $conn->resourceId);
        // 更新用户列表
        $this->broadcastUserList();
        echo "用户加入: {$username}\n";
    }
    // 处理聊天消息
    private function handleChat(ConnectionInterface $from, $data) {
        if (!isset($this->users[$from->resourceId])) {
            $from->send(json_encode([
                'type' => 'error',
                'message' => '请先设置昵称'
            ]));
            return;
        }
        $message = isset($data['message']) ? htmlspecialchars($data['message']) : '';
        if (empty($message) || strlen($message) > 500) {
            $from->send(json_encode([
                'type' => 'error',
                'message' => '消息长度必须在1-500字符之间'
            ]));
            return;
        }
        $user = $this->users[$from->resourceId];
        // 广播聊天消息
        $this->broadcast([
            'type' => 'chat',
            'username' => $user['username'],
            'message' => $message,
            'time' => date('H:i:s'),
            'userId' => $from->resourceId
        ]);
        echo "聊天消息: {$user['username']}: {$message}\n";
    }
    // 处理私聊消息
    private function handlePrivateMessage(ConnectionInterface $from, $data) {
        if (!isset($this->users[$from->resourceId])) {
            return;
        }
        $targetId = $data['targetId'] ?? '';
        $message = isset($data['message']) ? htmlspecialchars($data['message']) : '';
        if (empty($message) || empty($targetId)) {
            return;
        }
        $fromUser = $this->users[$from->resourceId];
        // 发送给指定用户
        if (isset($this->users[$targetId])) {
            $target = $this->users[$targetId];
            $target['connection']->send(json_encode([
                'type' => 'private',
                'from' => $fromUser['username'],
                'message' => $message,
                'time' => date('H:i:s')
            ]));
            // 发送回执给发送者
            $from->send(json_encode([
                'type' => 'private',
                'to' => $this->users[$targetId]['username'],
                'message' => $message,
                'time' => date('H:i:s'),
                'sent' => true
            ]));
        } else {
            $from->send(json_encode([
                'type' => 'error',
                'message' => '用户不在线'
            ]));
        }
    }
    // 处理打字状态
    private function handleTyping(ConnectionInterface $from, $data) {
        if (isset($this->users[$from->resourceId])) {
            $user = $this->users[$from->resourceId];
            $isTyping = $data['isTyping'] ?? false;
            $this->broadcast([
                'type' => 'typing',
                'username' => $user['username'],
                'isTyping' => $isTyping,
                'userId' => $from->resourceId
            ], $from->resourceId);
        }
    }
    // 处理断开连接
    private function handleDisconnect(ConnectionInterface $conn) {
        if (isset($this->users[$conn->resourceId])) {
            $username = $this->users[$conn->resourceId]['username'];
            unset($this->users[$conn->resourceId]);
            // 广播用户离开
            $this->broadcast([
                'type' => 'system',
                'message' => "{$username} 离开了聊天室"
            ]);
            // 更新用户列表
            $this->broadcastUserList();
            echo "用户离开: {$username}\n";
        }
    }
    // 连接关闭时
    public function onClose(ConnectionInterface $conn) {
        $this->handleDisconnect($conn);
        $this->clients->detach($conn);
    }
    // 错误处理
    public function onError(ConnectionInterface $conn, \Exception $e) {
        echo "错误: {$e->getMessage()}\n";
        $conn->close();
    }
    // 广播消息给所有客户端
    private function broadcast($data, $excludeId = null) {
        $message = json_encode($data);
        foreach ($this->clients as $client) {
            if ($excludeId === null || $client->resourceId !== $excludeId) {
                $client->send($message);
            }
        }
    }
    // 广播用户列表
    private function broadcastUserList() {
        $userList = [];
        foreach ($this->users as $id => $user) {
            $userList[] = [
                'id' => $id,
                'username' => $user['username']
            ];
        }
        $this->broadcast([
            'type' => 'userlist',
            'users' => $userList
        ]);
    }
    // 发送用户列表给指定连接
    private function sendUserList(ConnectionInterface $conn) {
        $userList = [];
        foreach ($this->users as $id => $user) {
            if ($id !== $conn->resourceId) {
                $userList[] = [
                    'id' => $id,
                    'username' => $user['username']
                ];
            }
        }
        $conn->send(json_encode([
            'type' => 'userlist',
            'users' => $userList
        ]));
    }
}

前端代码 public/index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">WebSocket 聊天室</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
        }
        .container {
            width: 100%;
            max-width: 1200px;
            height: 80vh;
            background: white;
            border-radius: 20px;
            box-shadow: 0 20px 60px rgba(0,0,0,0.3);
            display: flex;
            overflow: hidden;
        }
        .sidebar {
            width: 280px;
            background: #f8f9fe;
            padding: 20px;
            border-right: 1px solid #e9ecef;
        }
        .main-chat {
            flex: 1;
            display: flex;
            flex-direction: column;
        }
        .chat-header {
            padding: 20px;
            background: white;
            border-bottom: 2px solid #f1f3f5;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .chat-header h2 {
            font-size: 1.5rem;
            color: #333;
        }
        .chat-header .status {
            background: #89b4fa;
            color: white;
            padding: 5px 10px;
            border-radius: 20px;
            font-size: 12px;
            font-weight: 500;
        }
        .messages {
            flex: 1;
            overflow-y: auto;
            padding: 20px;
            background: #fafbfc;
        }
        .message {
            margin-bottom: 15px;
            padding: 15px;
            background: white;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.05);
            max-width: 80%;
        }
        .message.system {
            background: #f1f3f5;
            text-align: center;
            color: #495057;
            font-style: italic;
            margin: 10px auto;
            padding: 8px;
            max-width: 100%;
        }
        .message.chat {
            margin-left: auto;
            background: #be4bdb;
            color: white;
        }
        .message.private {
            background: #ff922b;
            color: white;
        }
        .message .username {
            font-weight: bold;
            margin-bottom: 5px;
        }
        .message .time {
            font-size: 12px;
            color: #aaa;
            float: right;
            margin-top: -20px;
        }
        .message .time.white {
            color: rgba(255,255,255,0.8);
        }
        .input-area {
            display: flex;
            padding: 20px;
            background: white;
        }
        #messageInput {
            flex: 1;
            padding: 12px 15px;
            border: 2px solid #e9ecef;
            border-radius: 10px;
            font-size: 14px;
            outline: none;
            transition: border-color 0.3s;
        }
        #messageInput:focus {
            border-color: #be4bdb;
        }
        #sendBtn {
            margin-left: 10px;
            padding: 12px 25px;
            background: #be4bdb;
            color: white;
            border: none;
            border-radius: 10px;
            cursor: pointer;
            font-weight: bold;
            transition: all 0.3s;
        }
        #sendBtn:hover {
            background: #9c36b5;
            transform: scale(1.05);
        }
        .user-list {
            margin-top: 20px;
        }
        .user-list h3 {
            margin-bottom: 10px;
            color: #495057;
        }
        .user-item {
            display: flex;
            align-items: center;
            padding: 8px 10px;
            margin-bottom: 5px;
            background: white;
            border-radius: 8px;
            cursor: pointer;
            transition: background-color 0.2s;
        }
        .user-item:hover {
            background: #e9ecef;
        }
        .user-item .avatar {
            width: 30px;
            height: 30px;
            border-radius: 50%;
            background: linear-gradient(135deg, #667eea, #764ba2);
            color: white;
            display: flex;
            align-items: center;
            justify-content: center;
            margin-right: 10px;
            font-weight: bold;
            font-size: 12px;
        }
        .user-item.active {
            background: #be4bdb;
            color: white;
        }
        .typing-indicator {
            position: absolute;
            bottom: 80px;
            left: 20px;
            color: #666;
            font-style: italic;
        }
        .login-modal {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0,0,0,0.7);
            display: flex;
            align-items: center;
            justify-content: center;
            z-index: 100;
        }
        .login-content {
            background: white;
            padding: 30px;
            border-radius: 15px;
            text-align: center;
        }
        .login-content h2 {
            margin-bottom: 20px;
        }
        .login-content input {
            padding: 10px;
            margin: 10px 0;
            width: 200px;
            border: 2px solid #e9ecef;
            border-radius: 5px;
            font-size: 16px;
        }
        .login-content button {
            padding: 10px 20px;
            background: #be4bdb;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            margin-top: 10px;
        }
        .private-indicator {
            background: #ff922b;
            color: white;
            padding: 2px 8px;
            border-radius: 10px;
            font-size: 12px;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="sidebar">
            <h2 style="margin-bottom: 20px;">聊天室用户</h2>
            <div class="user-list" id="userList"></div>
        </div>
        <div class="main-chat">
            <div class="chat-header">
                <h2>💬 WebSocket 聊天室</h2>
                <span class="status" id="connectionStatus">已连接</span>
            </div>
            <div class="messages" id="messages"></div>
            <div class="typing-indicator" id="typingIndicator"></div>
            <div class="input-area">
                <input type="text" id="messageInput" placeholder="输入消息...">
                <button id="sendBtn">发送</button>
            </div>
        </div>
    </div>
    <!-- 登录弹窗 -->
    <div class="login-modal" id="loginModal">
        <div class="login-content">
            <h2>👋 欢迎来到聊天室</h2>
            <p style="color: #666; margin-bottom: 15px;">请输入你的昵称</p>
            <input type="text" id="usernameInput" placeholder="你的昵称" autocomplete="off">
            <br>
            <button onclick="login()">进入聊天室</button>
        </div>
    </div>
    <script>
        class ChatClient {
            constructor() {
                this.ws = null;
                this.userId = null;
                this.username = null;
                this.selectedUser = null; // 当前私聊对象
                this.typingTimeout = null;
                this.initialize();
            }
            initialize() {
                this.bindEvents();
                this.connect();
            }
            connect() {
                // WebSocket连接
                this.ws = new WebSocket('ws://localhost:8080');
                this.ws.onopen = () => {
                    console.log('WebSocket连接成功');
                    this.updateConnectionStatus(true);
                };
                this.ws.onmessage = (event) => {
                    const data = JSON.parse(event.data);
                    this.handleMessage(data);
                };
                this.ws.onclose = () => {
                    console.log('WebSocket连接断开');
                    this.updateConnectionStatus(false);
                    // 尝试重新连接
                    setTimeout(() => this.connect(), 3000);
                };
                this.ws.onerror = (error) => {
                    console.error('WebSocket错误:', error);
                };
            }
            bindEvents() {
                document.getElementById('sendBtn').addEventListener('click', () => this.sendMessage());
                document.getElementById('messageInput').addEventListener('keypress', (e) => {
                    if (e.key === 'Enter') this.sendMessage();
                });
                // 输入框事件 - 处理打字状态
                document.getElementById('messageInput').addEventListener('input', () => {
                    this.sendTypingStatus(true);
                    clearTimeout(this.typingTimeout);
                    this.typingTimeout = setTimeout(() => this.sendTypingStatus(false), 1000);
                });
                // 点击用户进行私聊
                document.getElementById('userList').addEventListener('click', (e) => {
                    const userItem = e.target.closest('.user-item');
                    if (userItem) {
                        this.selectedUser = {
                            id: userItem.dataset.userId,
                            username: userItem.dataset.username
                        };
                        this.deselectAllUsers();
                        userItem.classList.add('active');
                        this.showMessage(`正在私聊 ${this.selectedUser.username},再次点击取消`, 'system');
                    }
                });
            }
            handleMessage(data) {
                switch (data.type) {
                    case 'system':
                        this.showMessage(data.message, 'system');
                        break;
                    case 'chat':
                        this.showChatMessage(data);
                        break;
                    case 'private':
                        this.showPrivateMessage(data);
                        break;
                    case 'userlist':
                        this.updateUserList(data.users);
                        break;
                    case 'typing':
                        this.handleTyping(data);
                        break;
                    case 'error':
                        alert(data.message);
                        break;
                    case 'welcome':
                        this.userId = data.userId;
                        break;
                    default:
                        console.log('未知消息类型:', data);
                }
            }
            showMessage(message, type, username = '', time = '') {
                const messagesDiv = document.getElementById('messages');
                const messageElement = document.createElement('div');
                messageElement.className = `message ${type}`;
                if (type === 'chat' || type === 'private') {
                    messageElement.innerHTML = `
                        <span class="username">${username}:</span>
                        ${message}
                        <span class="time ${type === 'chat' || type === 'private' ? 'white' : ''}">${time}</span>
                    `;
                } else {
                    messageElement.textContent = message;
                }
                messagesDiv.appendChild(messageElement);
                messagesDiv.scrollTop = messagesDiv.scrollHeight;
            }
            showChatMessage(data) {
                this.showMessage(data.message, 'chat', data.username, data.time);
            }
            showPrivateMessage(data) {
                if (data.sent) {
                    this.showMessage(`私聊 ${data.to}: ${data.message}`, 'private', '你', data.time);
                } else {
                    this.showMessage(`来自 ${data.from}: ${data.message}`, 'private', '私聊', data.time);
                }
            }
            updateUserList(users) {
                const userListDiv = document.getElementById('userList');
                userListDiv.innerHTML = '<h3>在线用户 (<span id="userCount">0</span>)</h3>';
                const count = users.length + 1; // +1 for current user
                document.getElementById('userCount').textContent = count;
                // 添加当前用户
                if (this.username) {
                    const currentUserDiv = document.createElement('div');
                    currentUserDiv.className = 'user-item';
                    currentUserDiv.innerHTML = `
                        <div class="avatar">${this.username[0].toUpperCase()}</div>
                        ${this.username} (我)
                    `;
                    userListDiv.appendChild(currentUserDiv);
                }
                // 添加其他用户
                users.forEach(user => {
                    const userDiv = document.createElement('div');
                    userDiv.className = 'user-item';
                    userDiv.dataset.userId = user.id;
                    userDiv.dataset.username = user.username;
                    userDiv.innerHTML = `
                        <div class="avatar">${user.username[0].toUpperCase()}</div>
                        ${user.username}
                    `;
                    userListDiv.appendChild(userDiv);
                });
            }
            deselectAllUsers() {
                document.querySelectorAll('.user-item').forEach(item => {
                    item.classList.remove('active');
                });
            }
            sendMessage() {
                const input = document.getElementById('messageInput');
                const message = input.value.trim();
                if (!message || !this.username) return;
                if (this.selectedUser) {
                    // 发送私聊消息
                    this.ws.send(JSON.stringify({
                        type: 'private',
                        targetId: this.selectedUser.id,
                        message: message
                    }));
                } else {
                    // 发送公开消息
                    this.ws.send(JSON.stringify({
                        type: 'chat',
                        message: message
                    }));
                }
                // 清空输入框
                input.value = '';
                // 清除打字状态
                this.sendTypingStatus(false);
            }
            sendTypingStatus(isTyping) {
                if (this.ws && this.ws.readyState === WebSocket.OPEN) {
                    this.ws.send(JSON.stringify({
                        type: 'typing',
                        isTyping: isTyping
                    }));
                }
            }
            handleTyping(data) {
                const typingDiv = document.getElementById('typingIndicator');
                if (data.isTyping && data.userId !== this.userId) {
                    typingDiv.textContent = `${data.username} 正在输入...`;
                } else {
                    typingDiv.textContent = '';
                }
            }
            updateConnectionStatus(connected) {
                const statusEl = document.getElementById('connectionStatus');
                statusEl.textContent = connected ? '已连接' : '连接断开';
                statusEl.style.background = connected ? '#89b4fa' : '#ff6b6b';
            }
        }
        // 登录函数
        function login() {
            const username = document.getElementById('usernameInput').value.trim();
            if (username.length === 0) {
                alert('请输入昵称');
                return;
            }
            if (username.length > 20) {
                alert('昵称不能超过20个字符');
                return;
            }
            // 隐藏登录弹窗
            document.getElementById('loginModal').style.display = 'none';
            // 初始化聊天客户端
            const client = new ChatClient();
            // 设置用户名
            client.username = username;
            client.connectedUser = client.userId;
            // 连接后发送加入消息
            setTimeout(() => {
                if (client.ws.readyState === WebSocket.OPEN) {
                    client.ws.send(JSON.stringify({
                        type: 'join',
                        username: username
                    }));
                }
            }, 100);
        }
        // 回车键登录
        document.getElementById('usernameInput').addEventListener('keypress', (e) => {
            if (e.key === 'Enter') login();
        });
    </script>
</body>
</html>

使用方法

启动服务器

php server.php

访问聊天室

打开浏览器,访问:

http://localhost:8080

如果端口冲突,可以修改 server.php 中的端口号。

功能特性

  1. 多用户支持:实时显示在线用户
  2. 公开聊天:所有用户可见的群聊
  3. 私聊功能:点击用户列表中的用户进行私聊
  4. 打字状态提示:显示谁正在输入
  5. 消息历史:实时滚动显示消息
  6. 系统通知:用户加入/离开提示
  7. 消息验证:防止注入和过长消息

安全建议

  1. 输入验证:对用户输入进行过滤和长度限制
  2. 连接限制:限制同时连接数量
  3. 速率限制:限制消息发送频率
  4. 数据加密:生产环境使用WSS协议
  5. 日志记录:记录所有操作日志

这个实现包含完整的聊天室功能,你可以根据实际需求进行扩展,比如添加表情、图片上传、房间管理等。

抱歉,评论功能暂时关闭!