本文目录导读:

我来为你创建一个完整的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 中的端口号。
功能特性
- 多用户支持:实时显示在线用户
- 公开聊天:所有用户可见的群聊
- 私聊功能:点击用户列表中的用户进行私聊
- 打字状态提示:显示谁正在输入
- 消息历史:实时滚动显示消息
- 系统通知:用户加入/离开提示
- 消息验证:防止注入和过长消息
安全建议
- 输入验证:对用户输入进行过滤和长度限制
- 连接限制:限制同时连接数量
- 速率限制:限制消息发送频率
- 数据加密:生产环境使用WSS协议
- 日志记录:记录所有操作日志
这个实现包含完整的聊天室功能,你可以根据实际需求进行扩展,比如添加表情、图片上传、房间管理等。