PHP项目聊天室与消息持久化

wen PHP项目 3

我来为您设计一个PHP聊天室系统,包含消息持久化功能,这个系统将使用WebSocket实现实时通信,并使用数据库存储聊天记录。

PHP项目聊天室与消息持久化

系统架构

技术选型

  • WebSocket服务器: Ratchet (PHP WebSocket库)
  • 消息持久化: MySQL/PostgreSQL
  • 前端: WebSocket API + JavaScript
  • 实时通信: WebSocket协议

项目结构

chat-room/
├── server/
│   ├── ChatServer.php
│   ├── MessageHandler.php
│   └── database/
│       └── Database.php
├── public/
│   ├── index.html
│   ├── css/
│   │   └── style.css
│   └── js/
│       └── chat.js
├── config/
│   └── database.php
├── composer.json
└── start-server.php

核心代码实现

composer.json

{
    "require": {
        "cboden/ratchet": "^0.4",
        "react/socket": "^1.12"
    }
}

数据库表结构 (MySQL)

CREATE TABLE messages (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    message TEXT NOT NULL,
    room VARCHAR(50) DEFAULT 'general',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_room_created (room, created_at DESC)
);

config/database.php - 数据库配置

<?php
return [
    'host' => 'localhost',
    'dbname' => 'chat_room',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8mb4'
];

server/database/Database.php

<?php
namespace App\Database;
use PDO;
use PDOException;
class Database {
    private static $instance = null;
    private $pdo;
    private function __construct() {
        $config = require __DIR__ . '/../../config/database.php';
        try {
            $dsn = "mysql:host={$config['host']};dbname={$config['dbname']};charset={$config['charset']}";
            $this->pdo = new PDO($dsn, $config['username'], $config['password'], [
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                PDO::ATTR_EMULATE_PREPARES => false
            ]);
        } catch (PDOException $e) {
            throw new \RuntimeException("Database connection failed: " . $e->getMessage());
        }
    }
    public static function getInstance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    public function getPdo() {
        return $this->pdo;
    }
    public function saveMessage($username, $message, $room = 'general') {
        $stmt = $this->pdo->prepare(
            "INSERT INTO messages (username, message, room) VALUES (:username, :message, :room)"
        );
        return $stmt->execute([
            ':username' => $username,
            ':message' => $message,
            ':room' => $room
        ]);
    }
    public function getRecentMessages($room = 'general', $limit = 50) {
        $stmt = $this->pdo->prepare(
            "SELECT username, message, created_at FROM messages 
             WHERE room = :room 
             ORDER BY created_at DESC 
             LIMIT :limit"
        );
        $stmt->bindParam(':room', $room, PDO::PARAM_STR);
        $stmt->bindParam(':limit', $limit, PDO::PARAM_INT);
        $stmt->execute();
        return array_reverse($stmt->fetchAll());
    }
}

server/ChatServer.php

<?php
namespace App;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use App\Database\Database;
class ChatServer implements MessageComponentInterface {
    protected $clients;
    protected $rooms;
    public function __construct() {
        $this->clients = new \SplObjectStorage;
        $this->rooms = [];
    }
    public function onOpen(ConnectionInterface $conn) {
        $this->clients->attach($conn);
        $conn->room = 'general';
        $conn->username = 'Anonymous';
        if (!isset($this->rooms['general'])) {
            $this->rooms['general'] = new \SplObjectStorage;
        }
        $this->rooms['general']->attach($conn);
        echo "New connection: {$conn->resourceId}\n";
    }
    public function onMessage(ConnectionInterface $from, $msg) {
        $data = json_decode($msg, true);
        switch ($data['type']) {
            case 'message':
                $this->handleMessage($from, $data);
                break;
            case 'join':
                $this->handleJoin($from, $data);
                break;
            case 'history':
                $this->sendHistory($from, $data['room'] ?? 'general');
                break;
        }
    }
    private function handleMessage($from, $data) {
        $message = [
            'type' => 'message',
            'username' => $from->username,
            'message' => strip_tags($data['message']),
            'time' => date('H:i:s'),
            'room' => $from->room
        ];
        // 持久化消息到数据库
        try {
            $db = Database::getInstance();
            $db->saveMessage(
                $message['username'],
                $message['message'],
                $message['room']
            );
        } catch (\Exception $e) {
            echo "Error saving message: " . $e->getMessage() . "\n";
        }
        // 广播消息到同房间的所有客户端
        $this->broadcastToRoom($from->room, $message);
    }
    private function handleJoin($from, $data) {
        // 离开原来房间
        if (isset($this->rooms[$from->room])) {
            $this->rooms[$from->room]->detach($from);
        }
        $newRoom = $data['room'] ?? 'general';
        $from->room = $newRoom;
        $from->username = strip_tags($data['username'] ?? 'Anonymous');
        if (!isset($this->rooms[$newRoom])) {
            $this->rooms[$newRoom] = new \SplObjectStorage;
        }
        $this->rooms[$newRoom]->attach($from);
        // 通知房间内成员
        $notification = [
            'type' => 'system',
            'message' => "{$from->username} 加入了房间",
            'time' => date('H:i:s')
        ];
        $this->broadcastToRoom($newRoom, $notification);
        // 发送历史消息
        $this->sendHistory($from, $newRoom);
    }
    private function sendHistory($conn, $room) {
        try {
            $db = Database::getInstance();
            $history = $db->getRecentMessages($room);
            $conn->send(json_encode([
                'type' => 'history',
                'messages' => $history
            ]));
        } catch (\Exception $e) {
            echo "Error fetching history: " . $e->getMessage() . "\n";
        }
    }
    private function broadcastToRoom($room, $message) {
        if (isset($this->rooms[$room])) {
            foreach ($this->rooms[$room] as $client) {
                $client->send(json_encode($message));
            }
        }
    }
    public function onClose(ConnectionInterface $conn) {
        // 从房间移除
        if (isset($this->rooms[$conn->room])) {
            $this->rooms[$conn->room]->detach($conn);
            $notification = [
                'type' => 'system',
                'message' => "{$conn->username} 离开了房间",
                'time' => date('H:i:s')
            ];
            $this->broadcastToRoom($conn->room, $notification);
        }
        $this->clients->detach($conn);
        echo "Connection {$conn->resourceId} has disconnected\n";
    }
    public function onError(ConnectionInterface $conn, \Exception $e) {
        echo "An error has occurred: {$e->getMessage()}\n";
        $conn->close();
    }
}

start-server.php

<?php
require __DIR__ . '/vendor/autoload.php';
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use App\ChatServer;
$server = IoServer::factory(
    new HttpServer(
        new WsServer(
            new ChatServer()
        )
    ),
    8080
);
echo "Chat server started on port 8080\n";
$server->run();

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">PHP聊天室</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <div class="chat-container">
        <div class="chat-header">
            <h2>PHP聊天室</h2>
            <div class="room-selector">
                <select id="roomSelect">
                    <option value="general">公共聊天室</option>
                    <option value="tech">技术交流</option>
                    <option value="random">闲聊区</option>
                </select>
                <input type="text" id="usernameInput" placeholder="输入昵称" value="用户" + Math.floor(Math.random() * 1000)>
                <button id="joinBtn">加入房间</button>
            </div>
        </div>
        <div id="messages" class="messages"></div>
        <div class="chat-input">
            <input type="text" id="messageInput" placeholder="输入消息..." autocomplete="off">
            <button id="sendBtn">发送</button>
        </div>
    </div>
    <script src="js/chat.js"></script>
</body>
</html>

public/js/chat.js

class ChatClient {
    constructor() {
        this.ws = null;
        this.username = '用户' + Math.floor(Math.random() * 1000);
        this.room = 'general';
        this.connect();
        this.initElements();
        this.bindEvents();
    }
    connect() {
        this.ws = new WebSocket('ws://localhost:8080');
        this.ws.onopen = () => {
            console.log('Connected to chat server');
            this.joinRoom();
        };
        this.ws.onmessage = (event) => {
            const data = JSON.parse(event.data);
            this.handleMessage(data);
        };
        this.ws.onerror = (error) => {
            console.error('WebSocket error:', error);
        };
        this.ws.onclose = () => {
            console.log('Disconnected from server');
            setTimeout(() => this.connect(), 3000);
        };
    }
    initElements() {
        this.messagesContainer = document.getElementById('messages');
        this.messageInput = document.getElementById('messageInput');
        this.sendBtn = document.getElementById('sendBtn');
        this.usernameInput = document.getElementById('usernameInput');
        this.roomSelect = document.getElementById('roomSelect');
        this.joinBtn = document.getElementById('joinBtn');
    }
    bindEvents() {
        this.sendBtn.addEventListener('click', () => this.sendMessage());
        this.messageInput.addEventListener('keypress', (e) => {
            if (e.key === 'Enter') this.sendMessage();
        });
        this.joinBtn.addEventListener('click', () => this.joinRoom());
    }
    joinRoom() {
        this.username = this.usernameInput.value.trim() || 'Anonymous';
        this.room = this.roomSelect.value;
        this.ws.send(JSON.stringify({
            type: 'join',
            username: this.username,
            room: this.room
        }));
    }
    sendMessage() {
        const message = this.messageInput.value.trim();
        if (!message) return;
        this.ws.send(JSON.stringify({
            type: 'message',
            message: message
        }));
        this.messageInput.value = '';
    }
    handleMessage(data) {
        switch (data.type) {
            case 'message':
                this.displayMessage(data);
                break;
            case 'system':
                this.displaySystemMessage(data);
                break;
            case 'history':
                this.displayHistory(data.messages);
                break;
        }
    }
    displayMessage(data) {
        const msgElement = document.createElement('div');
        msgElement.className = 'message';
        msgElement.innerHTML = `
            <span class="username">${data.username}</span>
            <span class="time">${data.time}</span>
            <p>${data.message}</p>
        `;
        this.messagesContainer.appendChild(msgElement);
        this.scrollToBottom();
    }
    displaySystemMessage(data) {
        const msgElement = document.createElement('div');
        msgElement.className = 'system-message';
        msgElement.innerHTML = `
            <span>${data.message}</span>
            <span class="time">${data.time}</span>
        `;
        this.messagesContainer.appendChild(msgElement);
        this.scrollToBottom();
    }
    displayHistory(messages) {
        this.messagesContainer.innerHTML = '';
        messages.forEach(msg => {
            this.displayMessage({
                username: msg.username,
                message: msg.message,
                time: new Date(msg.created_at).toLocaleTimeString()
            });
        });
    }
    scrollToBottom() {
        this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight;
    }
}
// 初始化聊天客户端
document.addEventListener('DOMContentLoaded', () => {
    new ChatClient();
});

public/css/style.css

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
}
.chat-container {
    width: 800px;
    max-width: 90%;
    height: 600px;
    background: white;
    border-radius: 10px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
    display: flex;
    flex-direction: column;
}
.chat-header {
    background: #4a90d9;
    color: white;
    padding: 20px;
    border-radius: 10px 10px 0 0;
}
.chat-header h2 {
    margin-bottom: 15px;
}
.room-selector {
    display: flex;
    gap: 10px;
}
.room-selector select,
.room-selector input {
    padding: 8px;
    border: 1px solid #ccc;
    border-radius: 5px;
    font-size: 14px;
}
.room-selector button {
    padding: 8px 15px;
    background: #2c3e50;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
}
.messages {
    flex: 1;
    overflow-y: auto;
    padding: 20px;
    background: #f5f5f5;
}
.message {
    margin-bottom: 15px;
    padding: 10px;
    background: white;
    border-radius: 8px;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.message .username {
    font-weight: bold;
    color: #4a90d9;
}
.message .time {
    float: right;
    color: #999;
    font-size: 0.8em;
}
.message p {
    margin-top: 5px;
    line-height: 1.4;
}
.system-message {
    text-align: center;
    color: #666;
    font-style: italic;
    margin: 10px 0;
}
.system-message .time {
    margin-left: 10px;
    color: #999;
    font-size: 0.8em;
}
.chat-input {
    display: flex;
    padding: 20px;
    background: #f9f9f9;
    border-top: 1px solid #ddd;
    border-radius: 0 0 10px 10px;
    gap: 10px;
}
.chat-input input {
    flex: 1;
    padding: 12px;
    border: 1px solid #ddd;
    border-radius: 5px;
    font-size: 16px;
    outline: none;
}
.chat-input input:focus {
    border-color: #4a90d9;
}
.chat-input button {
    padding: 12px 25px;
    background: #4a90d9;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    font-size: 16px;
    transition: background 0.3s;
}
.chat-input button:hover {
    background: #357abd;
}

启动与运行

# 1. 安装依赖
composer install
# 2. 创建数据库
mysql -u root -p
CREATE DATABASE chat_room;
USE chat_room;
CREATE TABLE messages (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    message TEXT NOT NULL,
    room VARCHAR(50) DEFAULT 'general',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_room_created (room, created_at DESC)
);
# 3. 启动WebSocket服务器
php start-server.php
# 4. 在浏览器中打开
http://localhost:8000/public/index.html

性能优化建议

  1. 连接池: 使用数据库连接池减少连接开销
  2. 缓存: 对热点消息使用Redis缓存
  3. 分页: 历史消息支持分页加载
  4. 消息队列: 使用RabbitMQ处理高并发消息写入
  5. 压缩: 启用WebSocket消息压缩

安全性考虑

  1. 输入过滤和XSS防护
  2. WebSocket连接验证
  3. 消息速率限制
  4. SQL注入防护
  5. 会话管理

这个聊天室系统支持多房间、消息持久化、实时通信和历史消息加载,是一个功能完整的PHP聊天室解决方案。

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