本文目录导读:

我来为你创建一个完整的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协议
- 日志记录:记录所有操作日志
这个实现包含完整的聊天室功能,你可以根据实际需求进行扩展,比如添加表情、图片上传、房间管理等。
版权声明: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直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播