我来为您设计一个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聊天室解决方案。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播