PHP WebSockets — Complete Guide to Real-Time Communication
In this tutorial, you will learn about PHP WebSockets. We cover key concepts, practical examples, and best practices to help you master this topic.
PHP WebSockets enable real-time bidirectional communication between a server and clients, allowing live chat applications, push notifications, collaborative editing, and real-time dashboards without polling. This tutorial covers installing Ratchet, building a WebSocket server, handling connections and events, implementing a chat room, managing topic subscriptions, broadcasting messages, and deploying WebSocket servers with Supervisor.
What You'll Learn
- Understanding the WebSocket protocol and upgrade handshake
- Installing Ratchet for PHP WebSocket servers
- Building event handlers for onOpen, onMessage, onClose, and onError
- Implementing a multi-room chat server
- Broadcasting messages to connected clients
- Using ZeroMQ for cross-Process message publishing
- Handling connection heartbeats and stale connection cleanup
- Deploying WebSocket servers with Supervisor process manager
Why It Matters
Traditional PHP uses request-response cycles: the browser sends a request, PHP processes it, and returns HTML. This does not work for real-time features like live chat or notifications. WebSockets provide a persistent connection that the server can push data through at any time, enabling interactive applications that users expect today.
Real-World Use
DodaTech's live support chat system uses Ratchet WebSockets to connect customers with support agents in real time. When a customer types a message, it appears instantly on the agent's screen without page refreshes. The system pushes notifications when support tickets are updated, reducing average response time from minutes to seconds.
Learning Path
flowchart LR A[File Manager Project] --> B[WebSockets\nYou are here] B --> C[Guzzle HTTP] style B fill:#f90,color:#fff
Installing Ratchet
Ratchet is a PHP WebSocket server built on ReactPHP:
composer require cboden/ratchet
Simple WebSocket Server
Create chat-server.php:
<?php
require __DIR__ . '/vendor/autoload.php';
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
class ChatServer implements MessageComponentInterface
{
protected \SplObjectStorage $clients;
public function __construct()
{
$this->clients = new \SplObjectStorage();
echo "Chat server started.\n";
}
public function onOpen(ConnectionInterface $conn): void
{
$this->clients->attach($conn);
echo "New connection: {$conn->resourceId}\n";
}
public function onMessage(ConnectionInterface $from, $msg): void
{
$data = json_decode($msg, true);
if (!$data || !isset($data['type'])) return;
switch ($data['type']) {
case 'chat':
$message = json_encode([
'type' => 'chat',
'username' => $data['username'] ?? 'Anonymous',
'message' => $data['message'] ?? '',
'timestamp' => date('H:i:s'),
]);
foreach ($this->clients as $client) {
$client->send($message);
}
break;
case 'typing':
$message = json_encode([
'type' => 'typing',
'username' => $data['username'] ?? 'Anonymous',
'is_typing' => $data['is_typing'] ?? false,
]);
foreach ($this->clients as $client) {
if ($client !== $from) {
$client->send($message);
}
}
break;
}
}
public function onClose(ConnectionInterface $conn): void
{
$this->clients->detach($conn);
echo "Connection {$conn->resourceId} disconnected.\n";
}
public function onError(ConnectionInterface $conn, \Exception $e): void
{
echo "Error: {$e->getMessage()}\n";
$conn->close();
}
}
$server = IoServer::factory(
new HttpServer(new WsServer(new ChatServer())),
8080
);
echo "Listening on port 8080...\n";
$server->run();
Run the server:
php chat-server.php
Output:
Chat server started.
Listening on port 8080...
Client-Side HTML and JavaScript
Create chat-client.html:
<!DOCTYPE html>
<html>
<head>
<title>WebSocket Chat</title>
<style>
#messages { border: 1px solid #ccc; height: 300px; overflow-y: auto; padding: 10px; }
.message { margin-bottom: 5px; }
.username { font-weight: bold; color: #3498db; }
.timestamp { font-size: 11px; color: #999; }
.typing { font-style: italic; color: #888; }
</style>
</head>
<body>
<h1>Chat Room</h1>
<div id="messages"></div>
<input type="text" id="username" value="User" placeholder="Your name">
<input type="text" id="message-input" placeholder="Type message..." style="width:300px">
<button id="send-btn">Send</button>
<div id="typing-indicator" class="typing"></div>
<script>
const ws = new WebSocket('ws://localhost:8080');
const messages = document.getElementById('messages');
const input = document.getElementById('message-input');
const username = document.getElementById('username');
const typingDiv = document.getElementById('typing-indicator');
let typingTimer = null;
let isTyping = false;
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.type === 'chat') {
const div = document.createElement('div');
div.className = 'message';
div.innerHTML = '<span class="username">' + escapeHtml(data.username) + '</span>: '
+ escapeHtml(data.message)
+ ' <span class="timestamp">' + data.timestamp + '</span>';
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
} else if (data.type === 'typing') {
typingDiv.textContent = data.is_typing ? data.username + ' is typing...' : '';
}
};
function sendMessage() {
const msg = input.value.trim();
if (!msg) return;
ws.send(JSON.stringify({ type: 'chat', username: username.value, message: msg }));
input.value = '';
}
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') { sendMessage(); return; }
if (!isTyping) { isTyping = true; ws.send(JSON.stringify({ type: 'typing', username: username.value, is_typing: true })); }
clearTimeout(typingTimer);
typingTimer = setTimeout(function() {
isTyping = false;
ws.send(JSON.stringify({ type: 'typing', username: username.value, is_typing: false }));
}, 2000);
});
document.getElementById('send-btn').addEventListener('click', sendMessage);
function escapeHtml(text) {
var d = document.createElement('div');
d.textContent = text;
return d.innerHTML;
}
</script>
</body>
</html>
Open chat-client.html in two browser tabs. Messages typed in one tab appear instantly in the other.
Room-Based Chat
Extend the server with rooms. Clients can join and leave rooms, and messages go only to clients in the same room:
<?php
// room-chat-server.php (partial — room handling)
protected array $rooms = [];
public function onOpen(ConnectionInterface $conn): void
{
$this->clients->attach($conn, ['room' => 'lobby']);
$this->rooms['lobby'][] = $conn->resourceId;
}
public function onMessage(ConnectionInterface $from, $msg): void
{
$data = json_decode($msg, true);
if (!$data || !isset($data['type'])) return;
if ($data['type'] === 'join') {
$this->joinRoom($from, $data['room'] ?? 'lobby');
} elseif ($data['type'] === 'chat') {
$this->sendToRoom($from, $data);
}
}
private function joinRoom(ConnectionInterface $conn, string $room): void
{
$oldRoom = $this->clients->offsetGet($conn)['room'];
$this->removeFromRoom($conn, $oldRoom);
$this->clients->offsetSet($conn, ['room' => $room]);
$this->rooms[$room][] = $conn->resourceId;
$conn->send(json_encode(['type' => 'joined', 'room' => $room]));
}
private function sendToRoom(ConnectionInterface $from, array $data): void
{
$room = $this->clients->offsetGet($from)['room'];
$message = json_encode([
'type' => 'chat',
'username' => $data['username'] ?? 'Anonymous',
'message' => $data['message'] ?? '',
'timestamp' => date('H:i:s'),
]);
foreach ($this->clients as $client) {
if ($this->clients->offsetGet($client)['room'] === $room) {
$client->send($message);
}
}
}
Deploying with Supervisor
WebSocket servers run continuously. Use Supervisor to keep them alive:
Create /etc/supervisor/conf.d/chat-server.conf:
[program:chat-server]
command=php /var/www/chat-server.php
directory=/var/www
user=www-data
autostart=true
autorestart=true
startretries=3
stderr_logfile=/var/log/chat-server.err.log
stdout_logfile=/var/log/chat-server.out.log
Restart Supervisor:
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start chat-server
Common Mistakes
- Not handling connection limits: PHP's default max connections setting may be too low. Increase ulimit -n and configure the OS for higher connection counts.
- Blocking the event loop: Long-running PHP operations (database queries, file reads) block ReactPHP's event loop. Use asynchronous database drivers or offload work to a queue.
- Not cleaning up stale connections: Clients can disconnect without sending a close frame. Implement heartbeat pings every 30 seconds and close unresponsive connections.
- Sending too much data in a single message: WebSocket frames have size limits. For large payloads, split data into chunks.
- Not validating incoming messages: A malicious client can send arbitrary data. Validate JSON structure and sanitize content before broadcasting.
Practice Questions
How does the WebSocket handshake work?
- The client sends an HTTP Upgrade request. The server responds with 101 Switching Protocols and the connection upgrades to WebSocket.
What is the difference between Ratchet and traditional PHP?
- Ratchet runs a persistent event loop handling multiple connections. Traditional PHP terminates after sending a response.
How do you broadcast to all connected clients?
- Iterate over SplObjectStorage and call send() on each ConnectionInterface.
What is the purpose of a typing indicator?
- It sends typing events to the server when the user is typing and stops after inactivity. Other clients display "User is typing..."
Challenge: Build a WebSocket-based collaborative whiteboard where multiple users draw simultaneously, broadcasting draw events to all connected clients.
Mini Project
Build a real-time notification system:
- Create a WebSocket server subscribing to a Redis pub/sub channel.
- PHP scripts publish notifications to the channel when events occur.
- The WebSocket server pushes notifications to the appropriate client by user ID.
- Include notification types: success, warning, error, info.
- Implement notification history via REST API for reconnecting users.
FAQ
What's Next
Now learn how to make HTTP requests to external APIs with Guzzle HTTP.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro