Ratchet 是一個基于 ReactPHP 的 PHP WebSocket 庫,無需依賴 Swoole 擴展。以下是實現步驟:
首先安裝 Ratchet:
composer require cboden/ratchet
創建 WebSocket 處理類:
<?php
/*** websocket處理類* @DateTime 2025/7/28 10:38*/namespace app\api\controller;use Ratchet\ConnectionInterface;
use Ratchet\MessageComponentInterface;class WebSocketSever implements MessageComponentInterface
{protected $clients;public function __construct(){$this->clients = new \SplObjectStorage;echo "Chat server initialized\n";}// 實現接口要求的四個方法public function onOpen(ConnectionInterface $conn){$this->clients->attach($conn);$clientId = $conn->resourceId;echo "新客戶端連接: ({$clientId})\n";$conn->send("連接成功!你的客戶端ID是: {$clientId}");}public function onMessage(ConnectionInterface $from, $msg){$clientId = $from->resourceId;echo "客戶端 {$clientId} 發送消息: {$msg}\n";// 向發送者確認$from->send("服務器已收到: {$msg}");// 廣播給其他客戶端foreach ($this->clients as $client) {if ($from !== $client) {$client->send("用戶 {$clientId} 說: {$msg}");}}}public function onClose(ConnectionInterface $conn){$this->clients->detach($conn);echo "客戶端 {$conn->resourceId} 斷開連接\n";}public function onError(ConnectionInterface $conn, \Exception $e){echo "發生錯誤: {$e->getMessage()}\n";$conn->close();}
}
創建 WebSocket 服務啟動文件start_server.php:
注意:這里不能使用控制器,不要問為什么,問就是我已經嘗試過了,當然你也可以嘗試一下。
<?php
/*** websocket啟動文件 start_server.php* @DateTime 2025/7/28 16:14*/
require 'vendor/autoload.php';use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use app\api\controller\WebSocketSever;$server =IoServer::factory(new HttpServer(new WsServer(new WebSocketSever())),9501 // 端口號
);echo "Ratchet WebSocket服務器已啟動: ws://127.0.0.1:9501\n";
echo "按 Ctrl+C 停止服務\n";
$server->run();
?前端測試頁面:
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>WebSocket 測試工具</title><script src="https://cdn.tailwindcss.com"></script><link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet"><!-- 配置Tailwind自定義顏色和動畫 --><script>tailwind.config = {theme: {extend: {colors: {primary: '#3B82F6',secondary: '#10B981',danger: '#EF4444',dark: '#1E293B',light: '#F8FAFC'},fontFamily: {sans: ['Inter', 'system-ui', 'sans-serif'],},animation: {'fade-in': 'fadeIn 0.3s ease-in-out',},keyframes: {fadeIn: {'0%': { opacity: '0' },'100%': { opacity: '1' },}}}}}</script><style type="text/tailwindcss">@layer utilities {.content-auto {content-visibility: auto;}.scrollbar-hide {scrollbar-width: none;-ms-overflow-style: none;}.scrollbar-hide::-webkit-scrollbar {display: none;}.message-box {@apply rounded-lg p-4 mb-3 max-w-[80%] animate-fade-in;}.sent-message {@apply bg-primary text-white ml-auto;}.received-message {@apply bg-gray-200 text-dark mr-auto;}.system-message {@apply bg-gray-100 text-gray-500 text-sm mx-auto;}}</style>
</head>
<body class="bg-gray-50 min-h-screen font-sans">
<div class="container mx-auto px-4 py-8 max-w-4xl"><header class="mb-8 text-center"><h1 class="text-[clamp(1.8rem,4vw,2.5rem)] font-bold text-dark mb-2"><i class="fa fa-comments text-primary mr-2"></i>WebSocket 測試工具</h1><p class="text-gray-600">與 Ratchet WebSocket 服務器的實時通信測試</p></header><!-- 連接狀態區域 --><div class="bg-white rounded-xl shadow-md p-4 mb-6 transition-all duration-300"><div class="flex items-center justify-between"><div class="flex items-center"><div id="connection-status" class="w-3 h-3 rounded-full bg-danger mr-2"></div><span id="status-text" class="text-danger font-medium">未連接</span></div><div class="flex space-x-2"><input type="text" id="server-url"value="ws://127.0.0.1:9501"class="px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50 text-sm w-64"><button id="connect-btn" class="bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg transition-all duration-200 flex items-center"><i class="fa fa-plug mr-1"></i> 連接</button></div></div><div class="mt-3 text-sm text-gray-500 flex items-center"><i class="fa fa-info-circle mr-1"></i><span>服務器狀態: <span id="server-info">等待連接...</span></span></div></div><!-- 消息顯示區域 --><div class="bg-white rounded-xl shadow-md p-4 mb-6 h-[500px] flex flex-col"><div class="text-sm font-medium text-gray-500 mb-3 border-b pb-2"><i class="fa fa-history mr-1"></i> 消息歷史</div><div id="messages" class="flex-1 overflow-y-auto scrollbar-hide p-2 space-y-2"><!-- 消息會動態添加到這里 --><div class="system-message message-box"><i class="fa fa-info-circle mr-1"></i>請點擊"連接"按鈕與服務器建立連接</div></div></div><!-- 消息輸入區域 --><div class="bg-white rounded-xl shadow-md p-4"><div class="flex space-x-3"><textareaid="messageInput"placeholder="輸入消息內容...(按Enter發送,Shift+Enter換行)"class="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50 resize-none"rows="3"></textarea><buttonid="send-btn"class="bg-secondary hover:bg-secondary/90 text-white px-6 py-3 rounded-lg transition-all duration-200 flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed"disabled><i class="fa fa-paper-plane mr-1"></i> 發送</button></div><div class="mt-3 text-xs text-gray-400 flex justify-between"><div><i class="fa fa-keyboard-o mr-1"></i> 快捷鍵: Enter發送消息</div><div id="client-id" class="hidden"><i class="fa fa-user mr-1"></i> 客戶端ID: <span></span></div></div></div><footer class="mt-8 text-center text-gray-500 text-sm"><p>WebSocket 測試工具 © 2023</p></footer>
</div><script>// DOM元素const connectBtn = document.getElementById('connect-btn');const sendBtn = document.getElementById('send-btn');const messageInput = document.getElementById('messageInput');const messagesDiv = document.getElementById('messages');const connectionStatus = document.getElementById('connection-status');const statusText = document.getElementById('status-text');const serverInfo = document.getElementById('server-info');const serverUrlInput = document.getElementById('server-url');const clientIdElement = document.getElementById('client-id').querySelector('span');const clientIdContainer = document.getElementById('client-id');// WebSocket實例let ws = null;let isConnected = false;// 連接狀態樣式更新function updateConnectionStatus(connected) {isConnected = connected;if (connected) {connectionStatus.classList.remove('bg-danger');connectionStatus.classList.add('bg-secondary');statusText.classList.remove('text-danger');statusText.classList.add('text-secondary');statusText.textContent = '已連接';connectBtn.innerHTML = '<i class="fa fa-unplug mr-1"></i> 斷開';connectBtn.classList.remove('bg-primary');connectBtn.classList.add('bg-danger');sendBtn.disabled = false;messageInput.focus();serverInfo.textContent = `已連接到 ${serverUrlInput.value}`;clientIdContainer.classList.remove('hidden');} else {connectionStatus.classList.remove('bg-secondary');connectionStatus.classList.add('bg-danger');statusText.classList.remove('text-secondary');statusText.classList.add('text-danger');statusText.textContent = '未連接';connectBtn.innerHTML = '<i class="fa fa-plug mr-1"></i> 連接';connectBtn.classList.remove('bg-danger');connectBtn.classList.add('bg-primary');sendBtn.disabled = true;serverInfo.textContent = '未連接到服務器';clientIdContainer.classList.add('hidden');}}// 顯示消息function showMessage(text, type = 'received') {const messageElement = document.createElement('div');// 設置消息類型樣式if (type === 'sent') {messageElement.classList.add('sent-message', 'message-box');} else if (type === 'received') {messageElement.classList.add('received-message', 'message-box');} else if (type === 'system') {messageElement.classList.add('system-message', 'message-box');}// 添加消息內容messageElement.textContent = text;messagesDiv.appendChild(messageElement);// 滾動到底部messagesDiv.scrollTop = messagesDiv.scrollHeight;}// 連接/斷開WebSocketfunction toggleConnection() {if (isConnected) {// 斷開連接if (ws) {ws.close();}} else {// 建立連接const serverUrl = serverUrlInput.value.trim();if (!serverUrl) {showMessage('請輸入服務器地址', 'system');return;}try {ws = new WebSocket(serverUrl);// 連接成功ws.onopen = function() {updateConnectionStatus(true);showMessage('成功連接到服務器', 'system');};// 接收消息ws.onmessage = function(evt) {console.log('收到消息: ' + evt.data);// 嘗試提取客戶端ID(如果服務器返回)if (evt.data.startsWith('你的客戶端ID是:')) {const clientId = evt.data.split(':')[1].trim();clientIdElement.textContent = clientId;}showMessage(evt.data, 'received');};// 連接關閉ws.onclose = function(event) {updateConnectionStatus(false);let message = '連接已關閉';if (event.code !== 1000) {message += ` (錯誤代碼: ${event.code})`;}showMessage(message, 'system');};// 連接錯誤ws.onerror = function(error) {showMessage(`連接錯誤: ${error.message || '未知錯誤'}`, 'system');console.error('WebSocket錯誤:', error);};} catch (error) {showMessage(`連接失敗: ${error.message}`, 'system');console.error('連接異常:', error);}}}// 發送消息function sendMessage() {if (!isConnected || !ws) {showMessage('請先連接到服務器', 'system');return;}const message = messageInput.value.trim();if (!message) return;try {ws.send(message);showMessage(`我: ${message}`, 'sent');messageInput.value = '';} catch (error) {showMessage(`發送失敗: ${error.message}`, 'system');console.error('發送消息錯誤:', error);}}// 事件監聽connectBtn.addEventListener('click', toggleConnection);sendBtn.addEventListener('click', sendMessage);// 處理文本框回車事件messageInput.addEventListener('keydown', function(e) {// Enter鍵且沒有按Shiftif (e.key === 'Enter' && !e.shiftKey) {e.preventDefault(); // 阻止默認換行sendMessage();}});// 服務器地址輸入框事件serverUrlInput.addEventListener('keydown', function(e) {if (e.key === 'Enter') {e.preventDefault();if (!isConnected) {toggleConnection();}}});
</script>
</body>
</html>
啟動服務:
php start_server.php
啟動結果:
訪問測試頁面即可進行 WebSocket 通信。
點擊【連接】后的頁面:
與 Swoole 相比,Ratchet 的優勢是不需要安裝 PHP 擴展,純 PHP 實現,兼容性更好;缺點是在高并發場景下性能可能不如 Swoole。
下一篇文章嘗試使用Swoole來搭建websocket服務。