Building WebSockets in PHP from Scratch
Some time ago, I was looking for a library to work with WebSockets in PHP. During my research, I came across multiple articles discussing Node.js integration with Yii, while most WebSocket-related articles limited themselves to instructions on using phpdaemon.
I explored libraries like phpdaemon and Ratchet. They seemed overly complex, especially since Ratchet recommends using WAMP for sending messages to specific users. I couldn't understand why such heavyweight solutions were necessary, particularly when they required installing additional dependencies. After reviewing the source code of these and other libraries, I figured out how everything works and decided to write a simple WebSocket server in PHP myself. This helped me reinforce my understanding and discover some hidden pitfalls I hadn't considered before.
Thus, I set out to build the required functionality from scratch.
At the end of this article, you will find the complete code and a link to a demo chat.
Goals
- Understand server-side sockets in PHP.
- Learn the WebSocket protocol.
- Write a simple WebSocket server from scratch.
1) Server-side Sockets in PHP
Before this, I had only a vague understanding of server-side sockets. After reviewing several WebSocket library implementations, I encountered two common approaches:
Using the PHP socket
extension:
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); // Create socket
socket_bind($socket, '127.0.0.1', 8000); // Bind to IP and port
socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1); // Allow multiple connections on the same port
socket_listen($socket); // Start listening
Using the PHP stream
extension:
$socket = stream_socket_server("tcp://127.0.0.1:8000", $errno, $errstr);
I preferred the second option for its simplicity.
Now that we have created a server socket, we need to handle incoming connections. There are two main approaches:
Basic while-loop:
while ($connect = stream_socket_accept($socket, -1)) { // Wait for new connection (no timeout)
... // Handle $connect
}
Using stream_select
for multiple connections:
$connects = array();
while (true) {
$read = $connects;
$read[] = $socket;
$write = $except = null;
if (!stream_select($read, $write, $except, null)) { // Wait for readable sockets (no timeout)
break;
}
if (in_array($socket, $read)) { // New connection detected
$connect = stream_socket_accept($socket, -1);
$connects[] = $connect;
unset($read[array_search($socket, $read)]);
}
foreach ($read as $connect) { // Process all active connections
... // Handle $connect
unset($connects[array_search($connect, $connects)]);
}
}
Since we need to handle both new connections and existing ones for incoming messages, we will use the stream_select
approach.
2) WebSocket Protocol
A great explanation of the WebSocket protocol can be found in this article. Here, we focus on two key aspects:
WebSocket Handshake
To establish a WebSocket connection, we need to read the Sec-WebSocket-Key
header from the client request, compute the Sec-WebSocket-Accept
value, and send a proper response:
$SecWebSocketAccept = base64_encode(pack('H*', sha1($SecWebSocketKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
$response = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" .
"Upgrade: websocket\r\n" .
"Connection: Upgrade\r\n" .
"Sec-WebSocket-Accept: $SecWebSocketAccept\r\n\r\n";
Message Encoding and Decoding
When receiving data from a WebSocket, we need to decode it, and when sending data, we must encode it. The encoding process is well-documented in WebSocket specifications, but in practice, we only need two functions: decode()
and encode()
.
3) Simple WebSocket Server
Now that we have all the necessary components, we can combine the HTTP server logic with handshake, decoding, and encoding functions to create a basic WebSocket server.
Example of a simple WebSocket server:
// Implementation of a basic WebSocket server
This example allows customization of event handlers like onOpen
, onClose
, and onMessage
for custom functionality.
Goals Achieved
With this implementation, we have successfully:
- Understood PHP server sockets.
- Implemented the WebSocket protocol.
- Built a simple WebSocket server from scratch.
If you found this material useful, in the next article, I will describe how to run multiple processes for handling connections (one master and several workers), inter-process communication, and integration with frameworks like Yii.
Demo Chat with the Above Functionality
[Demo Chat Code]
Update (Best Comments from Readers):
- Each connection consumes about 9KB of memory.
- Using
fgets()
with open sockets can cause "hanging" because WebSocket messages do not end with a newline. Usefread()
instead. - When writing a response to a socket using
fwrite()
, always check if all bytes were successfully written. - Before sending data from the server, check if the client is ready to receive using
stream_socket_accept()
. - Sending non-UTF-8 characters to the socket will cause the client to disconnect with an error:
WebSocket connection to 'ws://sharoid.ru:8000/' failed: Could not decode a text frame as UTF-8.
- To check if no data was received and the socket should be closed, use
!strlen($data)
, not!$data
. - You can place an Nginx server in front of the WebSocket server for better performance.