Rabikant

Posted on November 24th

Build a WebSocket Server in PHP Without Any Library.

"Lets build a php websocket app without a library"

Socket Creation and Setup

$host = '127.0.0.1'; 
$port = 8080;        

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($socket, $host, $port);
socket_listen($socket);

This section involves setting of the WebSocket server. socket_create(): Creates a socket using IPv4 and TCP which is Transmission Control Protocol and using the SOL_TCP which is the TCP protocol. socket_bind(): Ties the socket to the IP address and port. socket_listen(): Brings the socket to a state where it waits for clients to connect to it through a server.

Handling Client Connections

$clients = []; 

echo "WebSocket server started on $host:$portn";

while (true) {
    $changedSockets = $clients;
    $changedSockets[] = $socket;

    $write = [];
    $except = [];

    socket_select($changedSockets, $write, $except, 0, 10);

    if (in_array($socket, $changedSockets)) {
        $newSocket = socket_accept($socket);
        $clients[] = $newSocket;
        $handshake = false;
        echo "New client connectedn";
        $socketKey = array_search($socket, $changedSockets);
        unset($changedSockets[$socketKey]);    }

This section deals with establishing new client link and manage the connected client accordingly.

$clients[]: A list where all active client will be stored. socket_select():It scans all sockets (including the server’s and the clients’ ones) for new events, like connection or messages. socket_accept() : Takes a connection request from a new client and includes the new client socket with the list of contacted clients.

Handling Incoming Data and Client Disconnections

 foreach ($changedSockets as $clientSocket) {
        $data = @socket_recv($clientSocket, $buffer, 1024, 0);
        if ($data === false || $data = 0) {
            echo "client disconnectedn";
            $clientKey = array_search($clientSocket, $clients);
            unset($clients[$clientKey]);
            socket_close($clientSocket);
            continue;
        }

        if (!$handshake) {
            performHandshake($clientSocket, $buffer);
            $handshake = true;
        } else {
            $message = unmask($buffer);
            if (!empty($message)) {
                echo "Received: $messagen";
                foreach ($clients as $client) {
                    if ($client != $clientSocket) {
                        sendMessage($client, $message);
                    }
                }
            }
        }
    }
  • socket_recv(): Data send by client is received by this function.@ Symbol is used to disable any warnings if the client disconnects.
  • Client disconnection: ($data === false) if data is not received,client gets disconnected and removed from the list and socket is closed.
  • performHandshake():Perform a WebSocket handshake if a client is connected. After the connection is established, communication can begin.
  • unmask():this function decodes the WebSocket frames to get the raw messages sent by the client.
  • Broadcasting : When a client sends a message it is sent to other users that are connected.

WebSocket Handshake

function performHandshake($clientSocket, $headers) {
    $headers = parseHeaders($headers);
    $secKey = $headers['Sec-WebSocket-Key'];
    $secAccept = base64_encode(pack('H*', sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
    $handshakeResponse = "HTTP/1.1 101 Switching Protocolsrn" .
        "Upgrade: websocketrn" .
        "Connection: Upgradern" .
        "Sec-WebSocket-Accept: $secAcceptrnrn";
    socket_write($clientSocket, $handshakeResponse, strlen($handshakeResponse));
}

Performs the WebSocket handshake to establish the connection.

  • parseHeaders(): When the client sends the initial request, this function extracts the WebSocket headers from the request.
  • Sec-WebSocket-Key: It is the key that is sent by the client to make a connection to W ebSocket
  • Sec-WebSocket-Accept: A calculated value that is returned to the client to coSec-WebSocket-Accept: A calculated value that is returned to the client to confirm the WebSocket upgrade.Sec-WebSocket-Accept: A calculated value that is returned to the client to confirm the WebSocket upgrade.Sec-WebSocket-Accept: A calculated value that is returned to the client to confirm the WebSocket upgrade.Sec-WebSocket-Accept: A calculated value that is returned to the client to confirm the WebSocket upgrade.Sec-WebSocket-Accept: A calculated value that is returned to the client to confirm the WebSocket upgrade.

Parsing HTTP Headers

function parseHeaders($headers) {
    $headers = explode("\r\n", $headers);
    $headerArray = [];
    foreach ($headers as $header) {
        $parts = explode(": ", $header);
        if (count($parts) === 2) {
            $headerArray[$parts[0]] = $parts[1];
        }
    }
    return $headerArray;
}

Parses the HTTP headers sent by the client during the WebSocket connection upgrade request.

Unmasking and Masking Messages

function unmask($payload)
{
    $length = ord($payload[1]) & 127;
    if ($length == 126) {
        $masks = substr($payload, 4, 4);
        $data = substr($payload, 8);
    } elseif ($length == 127) {
        $masks = substr($payload, 10, 4);
        $data = substr($payload, 14);
    } else {
        $masks = substr($payload, 2, 4);
        $data = substr($payload, 6);
    }
    $unmaskedtext = '';
    for ($i = 0; $i < strlen($data); ++$i) {
        $unmaskedtext .= $data[$i] ^ $masks[$i % 4];
    }
    return $unmaskedtext;
}

WebSocket messages are masked for security reasons, so this function "unmasks" the payload by applying the XOR operation on the data with the masking key.

function sendMessage($clientSocket, $message)
{
    $message = mask($message);
    socket_write($clientSocket, $message, strlen($message));
}

Sends a message to a specific client by first masking the message using the mask() function.

Masking Messages for Sending

function mask($message)
{
    $frame = [];
    $frame[0] = 129;

    $length = strlen($message);
    if ($length <= 125) {
        $frame[1] = $length;
    } elseif ($length <= 65535) {
        $frame[1] = 126;
        $frame[2] = ($length >> 8) & 255;
        $frame[3] = $length & 255;
    } else {
        $frame[1] = 127;
        $frame[2] = ($length >> 56) & 255;
        $frame[3] = ($length >> 48) & 255;
        $frame[4] = ($length >> 40) & 255;
        $frame[5] = ($length >> 32) & 255;
        $frame[6] = ($length >> 24) & 255;
        $frame[7] = ($length >> 16) & 255;
        $frame[8] = ($length >> 8) & 255;
        $frame[9] = $length & 255;
    }

    foreach (str_split($message) as $char) {
        $frame[] = ord($char);
    }

    return implode(array_map('chr', $frame));
}

Prepares a message to be sent to the client by framing and masking it as required by the WebSocket protocol.

HTML Structure

<! DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket Client</title>
    <!-- CSS Styles Here -->
</head>
<body>
    <div class="container">
        <h2>WebSocket Client</h2>
        <button onclick="connect()">Connect</button>
        <input type="text" id="messageInput" placeholder="Type your message here...">
        <button onclick="sendMessage()">Send Message</button>
        <div id="output"></div>
    </div>

    <script>
        // JavaScript code here
    </script>
</body>
</html>
  • HTML Tags: Defines the structure of the WebSocket client page.
  • : A text input for users to type their message.
  • Buttons:
    • The Connect button triggers the connect() function to establish a WebSocket connection with the server.
    • The Send Message button triggers the sendMessage() function to send a message to the WebSocket server.
  • : Displays messages (connection status and chat messages) received from the WebSocket server.

CSS Styling

      body {
            font-family: Arial, sans-serif;
            background-color: #f4f4f9;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
        }
        .container {
            background-color: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
            width: 300px;
            text-align: center;
        }
        h2 {
            color: #333;
        }
        button {
            background-color: #4CAF50;
            color: white;
            border: none;
            padding: 10px 20px;
            margin: 10px 0;
            border-radius: 5px;
            cursor: pointer;
        }
        button:hover {
            background-color: #45a049;
        }
        input {
            width: calc(100% - 22px);
            padding: 10px;
            margin-bottom: 10px;
            border-radius: 5px;
            border: 1px solid #ccc;
        }
        #output {
            margin-top: 20px;
            background-color: #f9f9f9;
            padding: 10px;
            border-radius: 5px;
            height: 150px;
            overflow-y: auto;
            border: 1px solid #ddd;
            color: #555;
            font-size: 14px;
        }
  • General styling: Makes the interface clean and user-friendly.
  • Container: Centers the WebSocket client interface in the middle of the screen and adds padding and a shadow for visual effect.
  • Buttons: Styled with green background color and hover effect for interaction.
  • Message Input: Provides space for typing messages with a border and padding.
  • Output Box: Displays messages with a scrollbar when the content exceeds the box size.

JavaScript for WebSocket Connection

let socket;

        function connect() {
            socket = new WebSocket("ws://127.0.0.1:8080");
            socket.onopen = function(event) {
                document.getElementById("output").innerHTML += "<span style='color: green;'>Connected to server</span><br>";
            };
            socket.onmessage = function(event) {
                document.getElementById("output").innerHTML += "<span style='color: blue;'>Message received:</span> " + event.data + "<br>";
            };
            socket.onclose = function(event) {
                document.getElementById("output").innerHTML += "<span style='color: red;'>Disconnected from server</span><br>";
            };
        }

        function sendMessage() {
            let message = document.getElementById("messageInput").value || "Hello, Server!";
            socket.send(message);
            document.getElementById("output").innerHTML += "<span style='color: purple;'>Message sent:</span> " + message + "<br>";
            document.getElementById("messageInput").value = ""; 
        }
  • WebSocket Connection:
    • The connect() function creates a WebSocket connection to the server at ws://127.0.0.1:8080.
    • onopen: Triggers when the connection is successfully opened. Displays a message in green saying "Connected to server".
    • onmessage: Triggers when a message is received from the server. Displays the message in blue under "Message received".
    • onclose: Triggers when the connection is closed. Displays a message in red saying "Disconnected from server".

Sending and Displaying Messages

 function sendMessage($clientSocket, $message)
{
    $message = mask($message);
    socket_write($clientSocket, $message, strlen($message));
}
  • Purpose: Sends a message to the WebSocket server.
  • Message Input: The message is retrieved from the input box. If no message is typed, it defaults to "Hello, Server!".
  • socket.send(message): Sends the message to the server through the WebSocket.
  • Display: Shows the sent message in purple and then clears the message input field.

Summary

  • Connect: Establishes a WebSocket connection.
  • Send Message: Sends typed messages to the server and displays both sent and received messages.
  • Live Feedback: Shows connection status and received messages dynamically in the output section.

Create a file name websocket.php and paste the following code:

<?php
$host = '127.0.0.1'; 
$port = 8080;        

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($socket, $host, $port);
socket_listen($socket);

$clients = []; 

echo "WebSocket server started on $host:$portn";

while (true) {
    $changedSockets = $clients;
    $changedSockets[] = $socket;

    $write = [];
    $except = [];

    socket_select($changedSockets, $write, $except, 0, 10);

    if (in_array($socket, $changedSockets)) {
        $newSocket = socket_accept($socket);
        $clients[] = $newSocket;
        $handshake = false;
        echo "New client connectedn";
        $socketKey = array_search($socket, $changedSockets);
        unset($changedSockets[$socketKey]);    }

    foreach ($changedSockets as $clientSocket) {
        $data = @socket_recv($clientSocket, $buffer, 1024, 0);
        if ($data === false || $data = 0) {
            echo "client disconnectedn";
            $clientKey = array_search($clientSocket, $clients);
            unset($clients[$clientKey]);
            socket_close($clientSocket);
            continue;
        }

        if (!$handshake) {
            performHandshake($clientSocket, $buffer);
            $handshake = true;
        } else {
            $message = unmask($buffer);
            if (!empty($message)) {        $handshake = true;
                echo "Received: $messagen";
                foreach ($clients as $client) {
                    if ($client != $clientSocket) {
                        sendMessage($client, $message);
                    }
                }
            }
        }
    }
}


function performHandshake($clientSocket, $headers) {
    $headers = parseHeaders($headers);
    $secKey = $headers['Sec-WebSocket-Key'];
    $secAccept = base64_encode(pack('H*', sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
    $handshakeResponse = "HTTP/1.1 101 Switching Protocolsrn" .
        "Upgrade: websocketrn" .
        "Connection: Upgradern" .
        "Sec-WebSocket-Accept: $secAcceptrnrn";
    socket_write($clientSocket, $handshakeResponse, strlen($handshakeResponse));
}

// Parse HTTP Headers
function parseHeaders($headers) {
    $headers = explode("\r\n", $headers);
    $headerArray = [];
    foreach ($headers as $header) {
        $parts = explode(": ", $header);
        if (count($parts) === 2) {
            $headerArray[$parts[0]] = $parts[1];
        }
    }
    return $headerArray;
}

function unmask($payload)
{
    $length = ord($payload[1]) & 127;
    if ($length == 126) {
        $masks = substr($payload, 4, 4);
        $data = substr($payload, 8);
    } elseif ($length == 127) {
        $masks = substr($payload, 10, 4);
        $data = substr($payload, 14);
    } else {
        $masks = substr($payload, 2, 4);
        $data = substr($payload, 6);
    }
    $unmaskedtext = '';
    for ($i = 0; $i < strlen($data); ++$i) {
        $unmaskedtext .= $data[$i] ^ $masks[$i % 4];
    }
    return $unmaskedtext;
}

function sendMessage($clientSocket, $message)
{
    $message = mask($message);
    socket_write($clientSocket, $message, strlen($message));
}

function mask($message)
{
    $frame = [];
    $frame[0] = 129;

    $length = strlen($message);
    if ($length <= 125) {
        $frame[1] = $length;
    } elseif ($length <= 65535) {
        $frame[1] = 126;
        $frame[2] = ($length >> 8) & 255;
        $frame[3] = $length & 255;
    } else {
        $frame[1] = 127;
        $frame[2] = ($length >> 56) & 255;
        $frame[3] = ($length >> 48) & 255;
        $frame[4] = ($length >> 40) & 255;
        $frame[5] = ($length >> 32) & 255;
        $frame[6] = ($length >> 24) & 255;
        $frame[7] = ($length >> 16) & 255;
        $frame[8] = ($length >> 8) & 255;
        $frame[9] = $length & 255;
    }

    foreach (str_split($message) as $char) {
        $frame[] = ord($char);
    }

    return implode(array_map('chr', $frame));
}

Create a file named index.html and paste the following code:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket Client</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f4f4f9;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
        }
        .container {
            background-color: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
            width: 300px;
            text-align: center;
        }
        h2 {
            color: #333;
        }
        button {
            background-color: #4CAF50;
            color: white;
            border: none;
            padding: 10px 20px;
            margin: 10px 0;
            border-radius: 5px;
            cursor: pointer;
        }
        button:hover {
            background-color: #45a049;
        }
        input {
            width: calc(100% - 22px);
            padding: 10px;
            margin-bottom: 10px;
            border-radius: 5px;
            border: 1px solid #ccc;
        }
        #output {
            margin-top: 20px;
            background-color: #f9f9f9;
            padding: 10px;
            border-radius: 5px;
            height: 150px;
            overflow-y: auto;
            border: 1px solid #ddd;
            color: #555;
            font-size: 14px;
        }
    </style>
</head>
<body>
    <div class="container">
        <h2>WebSocket Client</h2>
        <button onclick="connect()">Connect</button>
        <input type="text" id="messageInput" placeholder="Type your message here...">
        <button onclick="sendMessage()">Send Message</button>
        <div id="output"></div>
    </div>

    <script>
        let socket;

        function connect() {
            socket = new WebSocket("ws://127.0.0.1:8080");
            socket.onopen = function(event) {
                document.getElementById("output").innerHTML += "<span style='color: green;'>Connected to server</span><br>";
            };
            socket.onmessage = function(event) {
                document.getElementById("output").innerHTML += "<span style='color: blue;'>Message received:</span> " + event.data + "<br>";
            };
            socket.onclose = function(event) {
                document.getElementById("output").innerHTML += "<span style='color: red;'>Disconnected from server</span><br>";
            };
        }

        function sendMessage() {
            let message = document.getElementById("messageInput").value || "Hello, Server!";
            socket.send(message);
            document.getElementById("output").innerHTML += "<span style='color: purple;'>Message sent:</span> " + message + "<br>";
            document.getElementById("messageInput").value = ""; 
        }
    </script>
</body>
</html>

To start the websocket server run the following code:

php websocket.php

Next, open the index.html file and connect to the WebSocket server and starting chatting.

Complete Code

The project is available on our GitHub : https://github.com/piehostHQ/php-native-ws-server

Comments

sam commented at

Posted on November 18th

many thanks by your help with this code, PLEASE fix this line:

$headers = explode("rn", $headers);

the correct is:

$headers = explode("\r\n", $headers);

thanks one more time :-)

Anand Singh replied
Thanks for your feedback, we have made the change!

Leave a comment.

Share your thoughts or ask a question to be added in the loop.