本文目录导读:

我来详细介绍一下在PHP项目中使用Ratchet实现WebSocket。
Ratchet简介
Ratchet是一个PHP实现的WebSocket库,基于ReactPHP事件循环,支持:
- WebSocket服务器
- HTTP长轮询
- WebSocket客户端
安装
composer require cboden/ratchet
基础WebSocket服务器实现
1 创建消息处理器
<?php
// src/Chat.php
namespace MyApp;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
class Chat implements MessageComponentInterface {
protected $clients;
protected $users = [];
public function __construct() {
$this->clients = new \SplObjectStorage;
echo "Chat server initialized\n";
}
public function onOpen(ConnectionInterface $conn) {
// 存储新的连接
$this->clients->attach($conn);
echo "New connection: {$conn->resourceId}\n";
}
public function onMessage(ConnectionInterface $from, $msg) {
$data = json_decode($msg, true);
switch($data['type']) {
case 'login':
$this->handleLogin($from, $data);
break;
case 'message':
$this->handleMessage($from, $data);
break;
case 'private':
$this->handlePrivateMessage($from, $data);
break;
case 'typing':
$this->handleTyping($from, $data);
break;
}
}
public function onClose(ConnectionInterface $conn) {
$this->clients->detach($conn);
// 处理用户断开
if(isset($this->users[$conn->resourceId])) {
$username = $this->users[$conn->resourceId];
unset($this->users[$conn->resourceId]);
$this->broadcast([
'type' => 'user_offline',
'username' => $username
]);
}
echo "Connection {$conn->resourceId} has disconnected\n";
}
public function onError(ConnectionInterface $conn, \Exception $e) {
echo "Error: {$e->getMessage()}\n";
$conn->close();
}
private function handleLogin($conn, $data) {
$this->users[$conn->resourceId] = $data['username'];
// 广播用户上线
$this->broadcast([
'type' => 'user_online',
'username' => $data['username'],
'users' => array_values($this->users)
], $conn);
// 发送当前在线用户列表给新用户
$conn->send(json_encode([
'type' => 'login_success',
'username' => $data['username'],
'users' => array_values($this->users)
]));
}
private function handleMessage($from, $data) {
$response = [
'type' => 'message',
'from' => $this->users[$from->resourceId] ?? 'Unknown',
'message' => $data['message'],
'timestamp' => date('Y-m-d H:i:s')
];
$this->broadcast($response);
}
private function handlePrivateMessage($from, $data) {
$response = [
'type' => 'private',
'from' => $this->users[$from->resourceId],
'message' => $data['message'],
'timestamp' => date('Y-m-d H:i:s')
];
// 发送给指定用户
foreach ($this->clients as $client) {
if (isset($this->users[$client->resourceId]) &&
$this->users[$client->resourceId] === $data['to']) {
$client->send(json_encode($response));
break;
}
}
}
private function handleTyping($from, $data) {
// 广播正在输入状态
$this->broadcast([
'type' => 'typing',
'username' => $this->users[$from->resourceId],
'is_typing' => $data['is_typing']
], $from);
}
private function broadcast($data, $exclude = null) {
$json = json_encode($data);
foreach ($this->clients as $client) {
if ($client !== $exclude) {
$client->send($json);
}
}
}
}
2 创建启动脚本
<?php
// bin/chat-server.php
require __DIR__ . '/../vendor/autoload.php';
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use MyApp\Chat;
$port = $argv[1] ?? 8080;
$server = IoServer::factory(
new HttpServer(
new WsServer(
new Chat()
)
),
$port
);
echo "WebSocket server started on port {$port}\n";
$server->run();
高级功能实现
1 房间/频道系统
<?php
// src/RoomServer.php
namespace MyApp;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
class RoomServer implements MessageComponentInterface {
private $clients;
private $rooms = [];
private $users = [];
public function __construct() {
$this->clients = new \SplObjectStorage;
}
public function onOpen(ConnectionInterface $conn) {
$this->clients->attach($conn);
$this->users[$conn->resourceId] = [
'conn' => $conn,
'rooms' => []
];
}
public function onMessage(ConnectionInterface $from, $msg) {
$data = json_decode($msg, true);
switch($data['action']) {
case 'join_room':
$this->joinRoom($from, $data['room']);
break;
case 'leave_room':
$this->leaveRoom($from, $data['room']);
break;
case 'room_message':
$this->roomMessage($from, $data);
break;
case 'create_room':
$this->createRoom($from, $data['room_name']);
break;
case 'list_rooms':
$this->listRooms($from);
break;
}
}
private function joinRoom($conn, $roomName) {
if (!isset($this->rooms[$roomName])) {
$this->rooms[$roomName] = new \SplObjectStorage;
}
$this->rooms[$roomName]->attach($conn);
$this->users[$conn->resourceId]['rooms'][] = $roomName;
// 通知房间成员
$this->broadcastToRoom($roomName, [
'action' => 'user_joined',
'user_id' => $conn->resourceId,
'room' => $roomName,
'users_count' => $this->rooms[$roomName]->count()
], $conn);
$conn->send(json_encode([
'action' => 'joined_room',
'room' => $roomName,
'success' => true
]));
}
private function leaveRoom($conn, $roomName) {
if (isset($this->rooms[$roomName])) {
$this->rooms[$roomName]->detach($conn);
// 通知房间成员
$this->broadcastToRoom($roomName, [
'action' => 'user_left',
'user_id' => $conn->resourceId,
'room' => $roomName
]);
// 清理空房间
if ($this->rooms[$roomName]->count() === 0) {
unset($this->rooms[$roomName]);
}
}
}
private function roomMessage($from, $data) {
$response = [
'action' => 'room_message',
'room' => $data['room'],
'from' => $from->resourceId,
'message' => $data['message'],
'timestamp' => time()
];
$this->broadcastToRoom($data['room'], $response, $from);
}
private function createRoom($conn, $roomName) {
if (!isset($this->rooms[$roomName])) {
$this->rooms[$roomName] = new \SplObjectStorage;
$conn->send(json_encode([
'action' => 'room_created',
'room' => $roomName,
'success' => true
]));
} else {
$conn->send(json_encode([
'action' => 'room_created',
'room' => $roomName,
'success' => false,
'error' => 'Room already exists'
]));
}
}
private function listRooms($conn) {
$roomList = [];
foreach ($this->rooms as $name => $storage) {
$roomList[] = [
'name' => $name,
'users_count' => $storage->count()
];
}
$conn->send(json_encode([
'action' => 'room_list',
'rooms' => $roomList
]));
}
private function broadcastToRoom($roomName, $data, $exclude = null) {
if (isset($this->rooms[$roomName])) {
$json = json_encode($data);
foreach ($this->rooms[$roomName] as $client) {
if ($client !== $exclude) {
$client->send($json);
}
}
}
}
public function onClose(ConnectionInterface $conn) {
// 从所有房间移除
foreach ($this->rooms as $roomName => $storage) {
if ($storage->contains($conn)) {
$storage->detach($conn);
$this->broadcastToRoom($roomName, [
'action' => 'user_left',
'user_id' => $conn->resourceId,
'room' => $roomName
]);
}
}
$this->clients->detach($conn);
unset($this->users[$conn->resourceId]);
}
public function onError(ConnectionInterface $conn, \Exception $e) {
$conn->close();
}
}
2 认证中间件
<?php
// src/AuthProvider.php
namespace MyApp;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
class AuthProvider implements MessageComponentInterface {
private $decorated;
private $authTokens = [];
public function __construct(MessageComponentInterface $component) {
$this->decorated = $component;
}
public function onOpen(ConnectionInterface $conn) {
$queryString = $conn->httpRequest->getUri()->getQuery();
parse_str($queryString, $params);
$token = $params['token'] ?? '';
if ($this->validateToken($token)) {
$this->authTokens[$conn->resourceId] = $token;
$this->decorated->onOpen($conn);
} else {
$conn->send(json_encode([
'type' => 'error',
'message' => 'Authentication failed'
]));
$conn->close();
}
}
public function onMessage(ConnectionInterface $from, $msg) {
if (isset($this->authTokens[$from->resourceId])) {
$this->decorated->onMessage($from, $msg);
}
}
public function onClose(ConnectionInterface $conn) {
if (isset($this->authTokens[$conn->resourceId])) {
unset($this->authTokens[$conn->resourceId]);
}
$this->decorated->onClose($conn);
}
public function onError(ConnectionInterface $conn, \Exception $e) {
$this->decorated->onError($conn, $e);
}
private function validateToken($token) {
// 实现token验证逻辑
return !empty($token) && strlen($token) > 10;
}
}
JavaScript客户端
// public/chat.js
class WebSocketClient {
constructor(url) {
this.url = url;
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.listeners = {};
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('Connected to WebSocket server');
this.reconnectAttempts = 0;
this.emit('connected');
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.emit('message', data);
this.emit(data.type || data.action, data);
};
this.ws.onclose = () => {
console.log('Disconnected from WebSocket server');
this.emit('disconnected');
this.reconnect();
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
this.emit('error', error);
};
}
reconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
console.log(`Reconnecting in ${delay}ms...`);
setTimeout(() => this.connect(), delay);
} else {
console.error('Max reconnection attempts reached');
this.emit('reconnect_failed');
}
}
send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
} else {
console.error('WebSocket is not connected');
}
}
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
}
off(event, callback) {
if (this.listeners[event]) {
this.listeners[event] = this.listeners[event]
.filter(cb => cb !== callback);
}
}
emit(event, data) {
if (this.listeners[event]) {
this.listeners[event].forEach(callback => callback(data));
}
}
disconnect() {
if (this.ws) {
this.ws.close();
}
}
}
// 使用示例
const client = new WebSocketClient('ws://localhost:8080');
client.on('connected', () => {
console.log('Ready to send messages');
});
client.on('message', (data) => {
console.log('Received:', data);
});
client.on('error', (error) => {
console.error('Error:', error);
});
client.connect();
// 发送消息
client.send({
type: 'login',
username: 'User1'
});
性能优化建议
// 6.1 使用连接池
class ConnectionPool {
private $connections = [];
public function add($id, $connection) {
$this->connections[$id] = [
'connection' => $connection,
'last_active' => time()
];
}
public function remove($id) {
unset($this->connections[$id]);
}
// 清理超时连接
public function cleanup($timeout = 300) {
$now = time();
foreach ($this->connections as $id => $data) {
if ($now - $data['last_active'] > $timeout) {
$data['connection']->close();
$this->remove($id);
}
}
}
}
// 6.2 消息队列处理
class MessageQueue {
private $queue = [];
public function push($message) {
$this->queue[] = $message;
}
public function processBatch($callback) {
while (!empty($this->queue)) {
$batch = array_splice($this->queue, 0, 100);
$callback($batch);
}
}
}
// 6.3 使用内存缓存(Redis)
class RedisBackend {
private $redis;
public function __construct() {
$this->redis = new \Redis();
$this->redis->connect('127.0.0.1', 6379);
}
public function saveUserState($userId, $data) {
$this->redis->set("user:{$userId}", json_encode($data));
$this->redis->expire("user:{$userId}", 3600);
}
public function getUserState($userId) {
$data = $this->redis->get("user:{$userId}");
return $data ? json_decode($data, true) : null;
}
}
Nginx反向代理配置
# /etc/nginx/sites-available/chat.example.com
upstream websocket {
server 127.0.0.1:8080;
}
server {
listen 80;
server_name chat.example.com;
location /ws {
proxy_pass http://websocket;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
proxy_send_timeout 86400;
}
location / {
proxy_pass http://127.0.0.1:8000; # PHP应用
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Docker部署
# Dockerfile
FROM php:8.1-cli
RUN apt-get update && apt-get install -y \
git \
unzip \
libevent-dev \
&& pecl install event \
&& docker-php-ext-enable event
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-interaction --optimize-autoloader
COPY . .
EXPOSE 8080
CMD ["php", "bin/chat-server.php"]
# docker-compose.yml
version: '3.8'
services:
websocket:
build: .
ports:
- "8080:8080"
volumes:
- .:/app
environment:
- REDIS_HOST=redis
- DB_HOST=mysql
restart: unless-stopped
redis:
image: redis:alpine
ports:
- "6379:6379"
restart: unless-stopped
Ratchet提供了完整的WebSocket实现方案,关键点:
- 使用消息组件接口处理连接生命周期
- 实现心跳检测和重连机制
- 支持房间/频道系统
- 集成认证中间件
- 配合Nginx实现负载均衡
- 使用Docker简化部署
选择Ratchet的场景:
- 需要长连接实时通信
- 聊天应用、在线游戏
- 实时数据推送
- 协作编辑工具