Skip to Content

How to Create A WebSocket Server with PHP

In this tutorial, you'll learn how to create a WebSocket server with PHP. This example ties directly into the front-end JavaScript solution so you can create a fully-functioning chat application.

Advertising Disclosure: I am compensated for purchases made through affiliate links. Click here for details.

Prerequisites

For the PHP code to function properly, you'll need to set up Apache websockets. The tutorial provides a comprehensive, step-by-step guide to set up your environment properly.

It's also recommended that you have PHP 7.4 or later.

The Code

Arguments

First, we'll accept two arguments that will be passed from the command line when we execute the script and assign them to local variables. These are passed in as arguments so you can add the code to any project seamlessly without needing to manually change hostnames and port numbers.

  • $argv[1] - the first argument accepted, which is the hostname, or domain name for where the WebSocket code will be executed.
  • $argv[2] - the second argument accepted, which is the port number the WebSockets will be listening.
$host = $argv[1];
$port = $argv[2];

Create the Socket & Client Connections

Here, we'll create a new variable named $socket used to identify our WebSocket in the script. We'll then initialize our new WebSocket and bind it to whatever port number we assign through the command line. Once the connection is initialized, we'll create another $clients array that holds all available client connections to the server.

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($socket, 0, $port);
socket_listen($socket);

$clients = array($socket);

Create An Endless Loop

Now, we'll create an endless loop so our WebSocket listener stays alive indefinitely:

while (true) {
...
}

Check Connection State

Within our new loop, we'll create a temporary variable named $changed to store our client array. Then, we'll check to see if there are any connection state changes within the array:

$changed = $clients;
$null = NULL;

socket_select($changed, $null, $null, 0, 10);

A $null variable assigned NULL is required here due to limitations within the current Zend Engine. The variable must be passed as a reference instead of passing in directly to avoid conflicts.

Check for New Sockets

Next, we'll check the connection against the socket to see if a new connection exists. If so, we'll perform a handshake, send a masked response indicating a new socket connection has been made, and then remove the user from the $changed array to prevent duplicate checks:

if (in_array($socket, $changed)) {
$socket_new = socket_accept($socket);
$clients[] = $socket_new;

$header = socket_read($socket_new, 1024);
perform_handshaking($header, $socket_new, $host, $port);

socket_getpeername($socket_new, $ip);
$response = mask(json_encode(array("type" => "system", "message" => $ip . " connected")));
send_message($response);

$found_socket = array_search($socket, $changed);
unset($changed[$found_socket]);
}

Loop Through All Socket Connections

Now that we've refined our socket connection array, we'll loop through each existing connection:

foreach($changed as $changed_socket) {
...
}

Format Incoming Message Data

Within the foreach loop, we'll filter through any incoming data and send it back to each connected user masked and in JSON format:

while(socket_recv($changed_socket, $buf, 1024, 0) >= 1) {
if (substr($buf, 0, 1) == "{") {
$received_text = $buf;
} else {
$received_text = unmask($buf);
}
$data = json_decode($received_text);

if ($data) {
$response_text = mask(json_encode($data));
send_message($response_text);
}
break 2;
}

Remove Expired Socket Connections

Now, we'll check to see if the connection has expired. If so, we'll remove them from the client list and send a notification confirming that the user has been disconnected:

$buf = @socket_read($changed_socket, 1024, PHP_NORMAL_READ);

if ($buf === false) {
$found_socket = array_search($changed_socket, $clients);
socket_getpeername($changed_socket, $ip);
unset($clients[$found_socket]);

$response = mask(json_encode(array("type" => "system", "message" => $ip . " disconnected")));
}

Close the Socket Instance

Finally, outside of our while loop, we'll close out the instance:

socket_close($socket);

Custom Socket Handling Functions

Send Messages

This function writes any incoming messages to any active socket connections:

function send_message($message) {
global $clients;

foreach($clients as $changed_socket) {
@socket_write($changed_socket, $message, strlen($message));
}

return true;
}

Mask Incoming Messages

The WebSocket protocol states that all framed messages sent from the client to the server must be masked, or encrypted, to prevent possible attacks. Masking is done with the following custom function:

function mask($message) {
$b1 = 0x80 | (0x1 & 0x0f);
$length = strlen($message);

if ($length <= 125) {
$header = pack("CC", $b1, $length);
} elseif ($length > 125 && $length < 65536) {
$header = pack("CCn", $b1, 126, $length);
} elseif ($length >= 65536) {
$header = pack("CCNN", $b1, 127, $length);
}

return $header.$message;
}

Unmash Incoming Messages

Likewise, we can unmask incoming messages with the following custom function:

function unmask($message) {
$length = ord($message[1]) & 127;

if ($length == 126) {
$masks = substr($message, 4, 4);
$data = substr($message, 8);
}
elseif ($length == 127) {
$masks = substr($message, 10, 4);
$data = substr($message, 14);
}
else {
$masks = substr($message, 2, 4);
$data = substr($message, 6);
}

$message = "";

for ($i = 0; $i < strlen($data); $i++) {
$message .= $data[$i] ^ $masks[$i % 4];
}

return $message;
}

Perform Handshake with New Clients

The final custom function determines whether or not the user can connect to the server and the response is written to the socket protocol:

function perform_handshaking($received_header, $client_conn, $host, $port) {
$headers = array();
$protocol = (stripos($host, "local.") !== false) ? "ws" : "wss";
$lines = preg_split("/\r\n/", $received_header);

foreach ($lines as $line) {
$line = chop($line);

if (preg_match("/\A(\S+): (.*)\z/", $line, $matches)) {
$headers[$matches[1]] = $matches[2];
}
}

$secKey = $headers["Sec-WebSocket-Key"];
$secAccept = base64_encode(pack("H*", sha1($secKey . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")));

$upgrade =
"HTTP/1.1 101 Web Socket Protocol Handshake\r\n" .
"Upgrade: WebSocket\r\n" .
"Connection: Upgrade\r\n" .
"WebSocket-Origin: $host\r\n" .
"WebSocket-Location: $protocol://$host:$port/websocket.php\r\n" .
"Sec-WebSocket-Version: 13\r\n" .
"Sec-WebSocket-Accept:$secAccept\r\n\r\n";

socket_write($client_conn, $upgrade, strlen($upgrade));
}

Run the WebSocket Server

Open a terminal window, navigate to the project folder containing your PHP script file, and execute the following command:

# php -q websocket.php [hostname] [port_number]

Replace the arguments [hostname] and [port_number] with the values you would like to use. For example:

# php -q websocket.php domain.com 9600

To run the script in the background on a Ubuntu server, you could execute the following command:

# nohup php websocket.php [hostname] 9600 &

Conclusion

Here, you learned how to create a simple WebSocket server with PHP that sends and receives messages from users in real-time.

For the server-side WebSocket functionality, visit our PHP WebSocket GitHub repository.

Posted by: Josh Rowe
Last Updated: November 30, 2023
Created: October 17, 2023

Comments

There are no comments yet. Start the conversation!

Add A Comment

Comment Etiquette: Wrap code in a <code> and </code>. Please keep comments on-topic, do not post spam, keep the conversation constructive, and be nice to each other.