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

系统架构
技术选型
- 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
性能优化建议
- 连接池: 使用数据库连接池减少连接开销
- 缓存: 对热点消息使用Redis缓存
- 分页: 历史消息支持分页加载
- 消息队列: 使用RabbitMQ处理高并发消息写入
- 压缩: 启用WebSocket消息压缩
安全性考虑
- 输入过滤和XSS防护
- WebSocket连接验证
- 消息速率限制
- SQL注入防护
- 会话管理
这个聊天室系统支持多房间、消息持久化、实时通信和历史消息加载,是一个功能完整的PHP聊天室解决方案。