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.
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.
Created: October 17, 2023