Jump to content

Building WebSockets in PHP from Scratch

From WikiJournal
Revision as of 12:45, 12 March 2025 by Philip (talk | contribs) (Created page with "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 soluti...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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

  1. Understand server-side sockets in PHP.
  2. Learn the WebSocket protocol.
  3. 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. Use fread() 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.