Implementing a WebSocket Server with PHP

Author: Heinz Schweitzer, December 2020 ; still in draft mode, may move to other location

alt

Preface

This document is about a real implementation of a WebSocket server written in vanilla PHP. It describes the inner working and structure of the server as well as code for web clients and PHP clients to actually use the server.

Furthermore it will go a step beyond the simple coding examples of just opening a WebSocket connection and sending a message or receiving a message. There is way more analysis and design associated with using WebSocket as expected at first sight. Hopefully this document will help you understand the implemented infrastructure supporting web sockets on the client and server side.

The development started in 2016 to satisfy the need to inform users of a web application about activities of other users and to inform about progress made in PHP backend scripts triggered by regular asynchron XHTTPrequests.

Who is this document for ?

It is for developers, software architects, project managers, not sure yet if and how web sockets can enrich their work and how it improves user experience. I presume the reader has some knowledge of HTML, JavaScript and PHP. Coding in these languages and reading code should not be a problem. In other words I assume the reader had a look at the WebSocket API for JavaScript, likes it, but got stuck because a lack of a Server to talk to.

Requirements

Have access to https://github.com/napengam/phpWebSocketServer you will find all the sources there. This is it. There are no other dependencies.

Recommendations

Why using WebSocket ?

From https://en.wikipedia.org/wiki/WebSocket
WebSocket is a computer communications protocol, providing full-duplex communication channels over a single TCP connection. The WebSocket protocol was standardized by the IETF as RFC 6455 in 2011, and the WebSocket API in Web IDL is being standardized by the W3C.
The magic here is that with web sockets you can from a web page, at any time send messages to a server or receive at any time messages from that server.

Some real life examples

EMAS

A web application working since 2017 in the intranet of a library the WLB Würtembergische Landesbibliothek Stuttgart .

An extreme simplified explanation what this application is used for is, ordering new books for the inventory of the library and customers. One source of information about new books is a database regularly filled with information about new books published.

Chances are that more than one expert is viewing the same set of new books. One requirement of that project is to visually indicate to all viewers what books they can work with (buying) and where they should be aware that others might work with that book.

To satisfy this requirement, every active web application sends, as soon as a page of new book titles is rendered in the client, all book ids via WebSocket to the WebSocket server, telling the server to lock these books. The server does its book keeping and tells the client what books are already locked by others by sending these book ids back to the client. The book keeping on the server side is done entirely in memory, no database is used here.

If the client receives through WebSocket book ids that can't be locked it dims these book titles in the GUI.

This is just a fraction of what is going forth and back between the client and the server, but I think you get the idea.

Next, if the client triggers a complex query, send with an XHTTP-Request to the PHP backend, this PHP process connects also to the WebSocket server to report any progress back to the client that triggered the request.

As the queries and processing the result can run for several seconds this will keep the user informed about the real progress. This is more convenient then displaying only an animated gif.

RVK-Browser

This project, yet not online, allows a user to query the inventory of the WLB using the RVK notation.
As the inventory is about 5 million books, the query within the RVK notation, may run from seconds to several minutes. As long as the database is working you can only report in intervals, the time in seconds the query already worked in the database back to the user. If a certain threshold is crossed the user is offered to abort the query and start something new.

Here again web sockets and the server are at work to manage the flow of messages between PHP scripts, a sql monitor and the client.

The benefit for user is that he knows the query is still alive and working and that he can abort the query.

WLBRISERVA

A reservation system for workspaces is another project inside the WLB.
This one allows registered users of the WLB to make reservations for workspaces over the internet. Here web sockets are mainly used to give the user feedback about what is happening in the background like reading a calendar or looking for free slots and the like. Here and in the other examples mentioned, such feedback is given in pseudo modal information dialogs blocking the user interface until the XHTTP request comes back.

Conclusion

The given examples demonstrate the benefits you can expect by using web sockets. However web sockets and a WebSocket server just provide a channel you can talk and listen to.

The real power though unfolds with the messages you send and receive, meaning how the messages are structured and how they fit into the application.

High level overview

Before diving into all the gory details of the implementation here is an overview to give you more context for the basic components needed having web sockets to work for your needs.

Messages

Messages are the ingredients that brings the entire WebSocket realm to live.

As a message and data interchange format we use JSON. This format can transport a variety of data structures including arrays. Functions to encode and decode JSON are well known and documented for JavaScript and PHP.

Common to all messages and expected to exist, are two attributes:

{'opcode'= 'code', 'message'=messageValue }

A messageValue can range from a simple string up to any other JSON structure. This is highly dependent on what you need or want to achieve.

Opcodes are just strings.
Here are opcodes already in use.

Next we have messages that are just short strings not encoded as JSON.
These are used to turn on or off some server internal functionality. These messages are hidden inside the provided code for JavaScript clients and PHP clients.

As of now we have two such control strings:


NOTE:

During tests I have noticed that sending very long messages ( > 8KB ) in one step does not work reliably. Messages often arrived shorter or garbled. I have yet no clue why this is so. However, if the provided code for a client detects a message longer then 6KB it turns on buffering until the message is sent in chunks, then turns buffering off.
This is transparent, you don't have to deal with this.



Connection

Everything starts here.

alt

The flow of messages

What you see here is the interplay between the web client using the provided JavaScript code and the server. The main point is that after the handshake according RFC6455 is done, a defined dialog takes place between the client and the server to bring the connection to a steady state.

NOTE:

This dialog is not covered in any RFC, it is just invented by me. Feel free to change it in any way.

The client having received the next message from the server is now enabled to send another message.

This is the way I control the flow of messages from any client to the server.

After every message clients have transmitted, they need the NEXT message from the server , as an acknowledgement, before they send another message. The server can send a message at any time.

Why?

As mentioned, the connection is now in a steady state and the server knows about the client via the uuid and the associated socket and knows the application class where to route all client messages to.

Managing connections

For managing client connections, the stores allApps, clients and sockets are used.
If these would be implemented as tables in a sql database a crude entity relationship diagram would look like the one below.

alt

The relation between these is as follows.
From a socket resource returned by stream_socket_* functions and ID is derived
using intval(socket).

This ID is used as an index into the table socket and client.
In the table client we store all other needed info about the client like the appName transmitted with the handshake, web and the UUID is transmitted within the dialog after the handshake.

The store allApps is prepared before the server is started, by some startup code.
In fact it is an array of pointers to instances of classes representing the applications by implementing required methods to handle messages from client. Read more about this in section booting the Server

NOTE:

It is nowhere written that client management and message routing, has to be done like this.
Again there is no RFC or similar, that covers or recommends an architecture like the one here. Feel free to change this in any way.

Feedback

Now that the client is connected it can start to work by triggering a PHP back end script.

alt

This is a common scenario for web a application. Triggering some backend script because of some event fired by a user through the GUI, or nowadays called the UX.

The point here is that the web application is passing its UUID, among other parameters, to the backend script. The PHP backend script connects with the server, sending the UUID, message and opcode FEEDBACK to the server.

With UUDI the server finds the client, the associated socket and the application responsible for this client. With this the message can be transmitted back to the web application.

NOTE:

Connecting a PHP script to the server does not require a handshake or encoding or decoding messages because this is just a simple socket connection. The server handles this connection the same as a connection from a web client.

Broadcast

Here we see the web client using a broadcast

alt

In my applications broadcast messages are used to announce events or state changes to all other web applications, mainly to keep all players in sync. What the recipients do with the message and the data coming with it, is dictated by the application.

Be carefull

Sending messages from a web-client to a websocket server is not without risk. As any mischievous user can open the developer tools of the browser, locate the related code, and send, or manipulate, whatever messages, using the debugger and console. This might have detrimental effects on the application.

To my knowledge there is as yet no way to detect this. If I am wrong let me know !

NOTE:

Therefore I recommend to just read from a websocket, unless you run the web applciation in an intranet where you, hopefully, can trust your users.

Search also the web for 'websocket vulnerabilities' , this will give you more infromation

Implementation

Sources on github https://github.com/napengam/phpWebSocketServer

We will walk through the include files that keep the overall configuration, then we will look into the directory where the server sources are located.

Once there, I will explain how the store allApps is filled before starting the server.

Next we will look at the code for the webClient and PHPclient.

NOTE:

The sources you will see here are as close to those you will find on github. As of now I will not synchronize the sources unless there a drastic changes in the real implementation.

Whenever you see this arrow you can fade in more details. It is similar to stepping into a function when debugging.

Project tree

The core source tree looks like this

PHPWebsocketServer

include
PHPclient
server
webClient

Includes

phpwebsocket/include/; Globals to configure server and clients

Instead if hard coding configuration data into the sources I keep this information in include files.

What has to be configured?

First of all we need to tell the server on what host it is living and at what port it should listen for connections and what protocol to use, tcp or ssl/tls.

This information is kept in
phpwebsocket/include/addressPort.inc.php

phpWebSocketServer/include/addressPort.inc.php

    /*
    * **************************
    * include address and port like:
    * $Address=[ssl:// | tcp://]your.server.net
    * Use ssl:// if your server supports secure protocol
    * Otherwise use tcp://
    * 
    * $Port=number
    * **************************
    */
    //$Address = 'ssl://your.server.net';
    //$Address = 'tcp://your.server.net';
    //$Port = 10654;
    
On the very first diagram above, you see that the server is running on the same system as the web server. If this web server uses secure communication (HTTPS) with its clients, you have to specify ssl:// as the prefix, otherwise tcp://.

In case you have to communicate using ssl/tls you have to give the server a file containing the certificate and the private key, used for enabling HTTPS. The information on how this file is created and where to find it, is kept in
phpwebsocket/include/certPath.inc.php

phpWebSocketServer/include/certPath.inc.php

/*
  #
  # script for linux shell to merge key and certificate into one file
  # as this is requiered by PHP in case you use https:// to communicate with 
  # your web server
  #
  cd /etc/letsencrypt/live/your.server.net/
  openssl pkcs12 -export -in cert.pem -inkey privkey.pem -out tmp.p12
  openssl pkcs12 -in tmp.p12 -nodes -out certKey.pem
  rm tmp.p12

 */
$keyAndCertFile = '/etc/letsencrypt/live/your.server.net/certKey.pem';
$pathToCert = '/etc/letsencrypt/live/your.server.net/';
    
If you do not know how to deal with this or you do not have the privileges to access this information ask your webmaster, security master, sysadmin to help you with this. If the certificate has to be renewed, repeat the above.

Last you must specify where the server log files should be placed.
This information is kept in
phpwebsocket/include/logToFile.inc.php

phpWebSocketServer/include/logToFile.inc.php

/*
************************************************
* Directory for logfiles  
************************************************
*/
//$logDir = "/var/log/";
$logDir = "d:/temp";

    

Server

Directory phpwebsocket/server/

Prepare

The start of our journey, is the source file runSocketServer.php.
This is the source file that will boot and start the server.

    #> php runSocketServer.php co=1

    Wed, 13 Jan 2021 11:31:48 +0100; Server initialized on WINNT  localhost:8091
    Wed, 13 Jan 2021 11:31:48 +0100; Starting server...
    Wed, 13 Jan 2021 11:31:48 +0100; Registered resource : /
    Wed, 13 Jan 2021 11:31:48 +0100; Registered resource : /web
    Wed, 13 Jan 2021 11:31:48 +0100; Registered resource : /php

phpWebSocketServer/server/runSocketServer.php

/*
 * ***********************************************
 *  the runtime
 * ***********************************************
 */
include __DIR__ . '/../include/certPath.inc.php';
include __DIR__ . '/../include/adressPort.inc.php';
include __DIR__ . '/../include/logToFile.inc.php';
include __DIR__ . '/../include/errorHandler.php';
include __DIR__ . '/logToFile.php';
/*
 * ***********************************************
 *  inlcude the core server
 * ***********************************************
 */
include __DIR__ . "/webSocketServer.php";
/*
 * **********************************************
 *   your backend applications
 * **********************************************
 */
include __DIR__ . '/resource.php';
include __DIR__ . '/resourceDefault.php';
include __DIR__ . '/resourceWeb.php';
include __DIR__ . '/resourcePHP.php';

function check_set($n, $v = '') {
    if (isset($_GET[$n])) {
        return ($_GET[$n]);
    }
    return($v);
}

/*
 * ***********************************************
 *  check for parameters 
 * ***********************************************
 */
$logdir = '';
$console = false;
if ($argc > 1) {
    parse_str(implode('&', array_slice($argv, 1)), $_GET);
    $logdir = check_set('ld', $logDir);
    $console = check_set('co', false);
}
/*
 * ***********************************************
 *  create a logger
 * set directory for logfiles and 
 * log to console
 * ***********************************************
 */
$logger = new logToFile($logDir, $console);
if ($logger->error === '') {
    $logger->logOpen('webSockLog');
} else {
    $logger = '';
}
/*
 * *****************************************
 *  create server 
 * *****************************************
 */
$server = new WebsocketServer($Address, $Port, $logger, $keyAndCertFile, $pathToCert);
    

phpWebSocketServer/server/webSocketServer.php

function __construct($Address, $Port, $logger, $keyAndCertFile = '', $pathToCert = '') {

    $errno = 0;
    $errstr = '';
    $this->logging = $logger;

    $usingSSL = '';
    $context = stream_context_create();
    if ($this->isSecure($Address)) {
        stream_context_set_option($context, 'ssl', 'local_cert', $keyAndCertFile);
        stream_context_set_option($context, 'ssl', 'capth', $pathToCert);
        stream_context_set_option($context, 'ssl', 'allow_self_signed', true);
        stream_context_set_option($context, 'ssl', 'verify_peer', false);
        $usingSSL = "using SSL";
    }
    $socket = stream_socket_server("$Address:$Port", $errno, $errstr, 
                                   STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context);

    $this->Log("Server initialized on " . PHP_OS . "  $Address:$Port $usingSSL");
    if (!$socket) {
        $this->Log("Error $errno creating stream: $errstr", true);
        exit;
    }
    $this->serveros = PHP_OS;
    $this->Sockets[ intval($socket)] = $socket;
    $this->socketMaster = $socket;

    error_reporting($this->errorReport);
    set_time_limit($this->timeLimit);
    if ($this->implicitFlush) {
        ob_implicit_flush();
    }
}
        
/*
 * ***********************************************
 *  instantiate backend 'applications'
 * ***********************************************
 */
$resDefault = new resourceDefault();
$resWeb = new resourceWeb();
$resPHP = new resourcePHP();
/*
 * ***********************************************
 *  register backend 'applications' with server
 * ***********************************************
 */
$server->registerResource('/', $resDefault);
$server->registerResource('/web', $resWeb);
$server->registerResource('/php', $resPHP);

phpWebSocketServer/server/webSocketServer.php

public function registerResource($name, $app) {
    $this->allApps[$name] = $app;
    foreach (['registerServer', 'onOpen', 'onData', 'onClose', 'onError'] as $method) {
        if (!method_exists($app, $method)) {
            $this->allApps[$name] = NULL;
            return false;
        }
    }
    $app->registerServer($this);
    return true;
}
        

If everything is ok with the class, we save the class instance along with the name in array allApps. In case a method is missing we save NULL along with the name.

With finaly calling the method $app->registerServer($this) we link the server ($this) back into the resoure respectively into the class given in $app. This method is inherited from our base class resource.php

Lets just look at this method inside resource.php

final public function registerServer($server) {
        $this->server = $server;
}
        

This way we give the application classes access to common used server methods like

  • broadcast
  • feedback
  • write
  • close
  • log
/*
 * ***********************************************
 *  now start server.
 * Handle requests from clients
 * ***********************************************
 */

$server->Start();

    

All include files are consumed here and the webSocketServer will be booted. We have now an instance of webSocketServer and access to all its public methods we will need later on.

We registered the application classes, each under the name the clients will refer to in the GET /name clause during handshake.

We have prepared the server now with the information to route messages to the application requested by the client and the server is now able to listen to incoming client connections.

Now we can start the server and have it listening for connections.

Start

$server->Start();

phpWebSocketServer/server/webSocketServer.php

The interesting parts are highlighted

public function Start() {

    $this->Log("Starting server...");
    foreach ($this->allApps as $appName => $class) {
        $this->Log("Registered resource : $appName");
    }
    $a = true;
    $nulll = NULL;
    while ($a) {
        $socketArrayRead = $this->Sockets;
        $socketArrayWrite = $socketArrayExceptions = NULL;
        stream_select($socketArrayRead, $socketArrayWrite, $socketArrayExceptions, $nulll);
        foreach ($socketArrayRead as $Socket) { 
            $SocketID = intval($Socket);
            ///////////////
            // A socket has received some data
            ///////////////
            if ($Socket === $this->socketMaster) {
                $clientSocket = stream_socket_accept($Socket);
                if (!is_resource($clientSocket)) {
                    $this->Log("$SocketID, Connection could not be established");
                    continue;
                } else {
                    //////////////////
                    // We have a new client. Store the default data for this new client
                    //////////////////    
                    $SocketID = intval($clientSocket);
                    $this->Clients[$SocketID] = (object) [
                                'ID' => $SocketID,
                                'uuid' => '',
                                'Headers' => null,
                                'Handshake' => null,
                                'timeCreated' => null,
                                'bufferON' => false,
                                'buffer' => [],
                                'app' => NULL
                    ];
                    $this->Sockets[$SocketID] = $clientSocket;
                    $this->Log("New client connecting on socket #$SocketID");
                }
                continue;
            } 
            ///////////////////
            // Read data from client socket into buffer and check
            ///////////////////

            $dataBuffer = fread($Socket, $this->bufferLength);
            if ($dataBuffer === false || strlen($dataBuffer) == 0) {
                $this->onError($SocketID, "Client disconnected - TCP connection lost");
                $this->Close($Socket);
                continue;
            }

            $Client = $this->Clients[$SocketID];
            if ($Client->Handshake == false) {
                /*
                 * ***********************************************
                 * Handshake according to RFC6455
                 * ***********************************************
                 */
                if ($this->Handshake($Socket, $dataBuffer)) { 
    

phpWebSocketServer/server/RFC6455.php

protected function Handshake($Socket, $Buffer) {

        $errorResponds = [];
        $SocketID = intval($Socket);
        $Headers = [];
        $reqResource = [];
        $Lines = explode("\n", $Buffer);

        if ($Lines[0] == "php process") {
            $this->Log('Handshake:' . $Buffer);
            $this->Clients[$SocketID]->Headers = 'tcp';
            $this->Clients[$SocketID]->Handshake = true;
            preg_match("/GET (.*) HTTP/i", $Buffer, $reqResource);
            $Headers['get'] = trim($reqResource[1]);
            if (isset($this->allApps[$Headers['get']])) {
                $this->Clients[$SocketID]->app = $this->allApps[$Headers['get']];
            }
            return true;
        }
        $this->Log('Handshake: webClient');
        foreach ($Lines as $Line) {
            if (strpos($Line, ":") !== false) {
                $Header = explode(":", $Line, 2);
                $Headers[strtolower(trim($Header[0]))] = trim($Header[1]);
            } else if (stripos($Line, "get ") !== false) {
                preg_match("/GET (.*) HTTP/i", $Buffer, $reqResource);
                $Headers['get'] = trim($reqResource[1]);
            }
        }
  
        if (!isset($Headers['host']) || !isset($Headers['origin']) ||
                !isset($Headers['sec-websocket-key']) ||
                (!isset($Headers['upgrade']) || strtolower($Headers['upgrade']) != 'websocket') ||
                (!isset($Headers['connection']) || strpos(strtolower($Headers['connection']), 'upgrade') === FALSE)) {
            $errorResponds[] = "HTTP/1.1 400 Bad Request";
        }
        if (!isset($Headers['sec-websocket-version']) || strtolower($Headers['sec-websocket-version']) != 13) {
            $errorResponds[] = "HTTP/1.1 426 Upgrade Required\r\nSec-WebSocketVersion: 13";
        }
        if (!isset($Headers['get'])) {
            $errorResponds[] = "HTTP/1.1 405 Method Not Allowed\r\n\r\n";
        }
        if (count($errorResponds) > 0) {
            $message = implode("\r\n", $errorResponds);
            fwrite($Socket, $message, strlen($message));
            $this->onError($SocketID, "Handshake aborted - [" . trim($message) . "]");
            $this->Close($Socket);
            return false;
        }
        $Token = "";
        $sah1 = sha1($Headers['sec-websocket-key'] . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
        for ($i = 0; $i < 20; $i++) {
            $Token .= chr(hexdec(substr($sah1, $i * 2, 2)));
        }
        $Token = base64_encode($Token);
        $statusLine = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: $Token\r\n\r\n";
        fwrite($Socket, $statusLine, strlen($statusLine));

        $this->Clients[$SocketID]->Headers = 'websocket';
        $this->Clients[$SocketID]->Handshake = true;
        if (isset($this->allApps[$Headers['get']])) {
            $this->Clients[$SocketID]->app = $this->allApps[$Headers['get']];
        }
        return true;
    }
        
  
                    if ($Client->app === NULL) {
                        $this->Log("Application incomplete or does not exist);"
                                . " Telling Client to disconnect on  #$SocketID");
                        $msg = (object) Array('opcode' => 'close', 'os' => $this->serveros);
                        $this->Write($SocketID, json_encode($msg));
                        $this->Close($Socket);
                    } else {
                        $this->Log("Telling Client to start on  #$SocketID");
                        $msg = (object) Array('opcode' => 'ready', 'os' => $this->serveros);
                        $this->Write($SocketID, json_encode($msg));
                        $Client->app->onOpen($SocketID);
                    }
                }
                continue;
            }           
            /*
            * ***********************************************
            * message from client
            * ***********************************************
            */
           $message = $this->Read($SocketID, $dataBuffer);
    

phpWebSocketServer/server/webSocketServer.php

Let's have a closer look at this method.

In case the message is coming from a websocket we have to decode it and have a look at the opcode encoded, else we can take it as is.

Next we will 'release' the client by sending the 'next' message. If the message will come in chunks we have to collect them.

Finally we return the message

public function Read($SocketID, $message) {
    $client = $this->Clients[$SocketID];
    if ($client->Headers === 'websocket') {
        $message = $this->Decode($message);// Decode according to RFC6455
        

phpWebSocketServer/server/RFC6455.php

public function Decode($payload) {
    // detect ping or pong frame
    $this->opcode = ord($payload[0]) & 15;
    $length = ord($payload[1]) & 127;
    if ($length == 126) {
        $masks = substr($payload, 4, 4);
        $data = substr($payload, 8);
    } else if ($length == 127) {
        $masks = substr($payload, 10, 4);
        $data = substr($payload, 14);
    } else {
        $masks = substr($payload, 2, 4);
        $data = substr($payload, 6, $length); // hgs 30.09.2016
    }
    $text = '';
    $l = strlen($data);
    $m0 = $masks[0];
    $m1 = $masks[1];
    $m2 = $masks[2];
    $m3 = $masks[3];
    for ($i = 0; $i < $l;) {
        $text .= $data[$i++] ^ $m0;
        if ($i < $l) {
            $text .= $data[$i++] ^ $m1;
            if ($i < $l) {
                $text .= $data[$i++] ^ $m2;
                if ($i < $l) {
                    $text .= $data[$i++] ^ $m3;
                }
            }
        }
    }
    return $text;
}
            
        if ($this->opcode == 10) { //pong
            $this->log("Unsolicited Pong frame received from socket #$SocketID"); //ignore
            return '';
        }
        if ($this->opcode == 8) { //Connection Close Frame 
            $this->log("Connection Close frame received from socket #$SocketID");
            $this->Close($SocketID);
            return '';
        }
    }
    //
    // Client can send next message
    //
    $this->Write($SocketID, json_encode((object) ['opcode' => 'next']));
        

phpWebSocketServer/server/webSocketServer.php

public function Write($SocketID, $message) {
    if ($this->Clients[$SocketID]->Headers === 'websocket') {
        $message = $this->Encode($message);
            

phpWebSocketServer/server/RFC6455.php

public function Encode($M) {   
    $L = strlen($M);
    $bHead = [];
    if ($this->opcode == 10) { // POng
        $bHead[0] = 137;
    } else {
        $bHead[0] = 129; // 0x1 text frame (FIN + opcode)
    }
    if ($L <= 125) {
        $bHead[1] = $L;
    } else if ($L >= 126 && $L <= 65535) {
        $bHead[1] = 126;
        $bHead[2] = ( $L >> 8 ) & 255;
        $bHead[3] = ( $L ) & 255;
    } else {
        $bHead[1] = 127;
        $bHead[2] = ( $L >> 56 ) & 255;
        $bHead[3] = ( $L >> 48 ) & 255;
        $bHead[4] = ( $L >> 40 ) & 255;
        $bHead[5] = ( $L >> 32 ) & 255;
        $bHead[6] = ( $L >> 24 ) & 255;
        $bHead[7] = ( $L >> 16 ) & 255;
        $bHead[8] = ( $L >> 8 ) & 255;
        $bHead[9] = ( $L ) & 255;
    }
    return (implode(array_map("chr", $bHead)) . $M);
}
                
    }
    return fwrite($this->Sockets[$SocketID], $message, strlen($message));
}
            
    if ($this->serverCommand($client, $message)) {
        

phpWebSocketServer/server/webScocketServer.php

private function serverCommand($client, &$message) {
    if ($message === 'bufferON') {
        $client->bufferON = true;
        $client->buffer = [];
        $this->Log('Buffering ON');
        return true;
    }
    if ($message === 'bufferOFF') {
        $client->bufferON = false;
        $message = implode('', $client->buffer);
        $client->buffer = [];
        $this->Log('Buffering OFF');
    }

    return false;
}
            
        return ''; // found bufferON; message will come in chunks;
    }
    if ($client->bufferON) {
        if (count($client->buffer) <= $this->maxChunks) {
            $client->buffer[] = $message;
        } else {
            $this->log("Too many chunks from socket #$SocketID");
            $this->onCLose($SocketID);
        }
        return '';
    }
   return $message;
    
}
        
  
           if ($message != '') {
               /*
                * ***********************************************
                * pass message to application class 
                * ***********************************************
                */
               $Client->app->onData($SocketID, $message);
           }           
        }
    }
}

The above section shows the sequence

In the very last step above we hand over the message to one of the registered application classes. These classes extend the base class resource.php and overwrite those methods needed. Have a look into the most generic application class: / .

phpWebSocketServer/server/resourceDefault.php

/*
 * **********************************************
 * default resource for websockets and sockets
 * **********************************************
 */

class resourceDefault extends resource {
    

phpWebSocketServer/server/resource.php

class resource {
    public $server;
    /*
     * ***********************************************
     * Overwrite these functions, when needed, in an 
     * application class then register with the  server
     * ***********************************************
     */

    function onOpen($SocketID) {       
    }

    function onData($SocketID, $M) { //... a message from client        
    }

    function onClose($SocketID) { // ...socket has been closed AND deleted        
    }

    function onError($SocketID, $M) { // ...any connection-releated error   
    }
  
    final public function registerServer($server) {
        $this->server = $server;
    }
    final function getPacket($M) {
        $packet = json_decode($M);
        $err = json_last_error();
        if ($err) {
            $packet = (object) ['opcode' => 'jsonerror', 'message' => $err];
        }
        return $packet;
    }
}

        
    private $packet; //, $server;

    function onData($SocketID, $M) {
        /*
         * *****************************************
         * $M is JSON like
         * {'opcode':task,  <followed by whatever is expected based on the value of opcode>}
         * This is just an example used here, you can send whatever you want.
         * *****************************************
         */

        $packet = $this->getPacket($M);
        if ($packet->opcode === 'jsonerror') {
            $this->server->Log("jsonerror closing #$SocketID");
            $this->server->Close($SocketID);
            return;
        }

        $this->packet = $packet;
        if ($packet->opcode === 'quit') {
            /*
             * *****************************************
             * client quits
             * *****************************************
             */
            $this->server->Log("QUIT; Connection closed to socket #$SocketID");
            $this->server->Close($SocketID);
            return;
        }
        if ($packet->opcode === 'uuid') {
            /*
             * *****************************************
             * web client registers
             * *****************************************
             */
            $this->server->Clients[$SocketID]->uuid = $packet->message;
            $this->server->log("Broadcast $M");
            return;
        }

        if ($packet->opcode === 'feedback') {
            /*
             * *****************************************
             * send feedback to client with uuid found
             * in $packet
             * *****************************************
             */
            $this->server->feedback($packet);
            return;
        }

        if ($packet->opcode === 'broadcast') {
            $this->server->broadCast($SocketID, $M);
            return;
        }
        /*
         * *****************************************
         * unknown opcode-> do nothing
         * *****************************************
         */
    }

}
    

Web client

Here is how I test connection and working with the SocketServer through a web client using my websocket module.

Create GUI

The first part is some HTML GUI/UX to allow you to interact with the server and other clients.

As you can push buttons here you will need some callback to catch these event and of course some callbacks to react to messages coming through websket from the server.

phpWebSocketServer/webClient/testWithWebSocket.php

<html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>WebClient</title>
    </head>
    <body>
        Status:<b><span id='connect'>not connected</span></b><br>
        <button id="ajax" >CALL Backend via AJAX</button><br>
        Here you will see feedback from backend : <b><span id='feedback'></span> </b>
        <hr>
        <button id="ready" >Talk to others; my UUID=<b><span id='uuid'></span></b> </button>
        <div id="broadcast">
            <b>Messages from other web clients:</b><br>
        </div>
        <?php
        include '../include/adressPort.inc.php';
        /*
         * ***********************************************
         * globals for the client module socketWebClient.js
         * ***********************************************
         */
        echo "<script>"
        . "server='$Address';"
        . "port='$Port';"
        . "</script>";
        ?>
        <script src="socketWebClient.js"></script>
        <script src="startWebClient.js"></script>
    </body>
</html>
    

The HTML above will produce this output:

Start GUI

We have to instrumented the buttons and provide areas for feedback and or broadcasts coming in via websocket from other clients.

The code to do this follows here in file startWebClient.js

phpWebSocketServer/webClient/startWebClient.js

/* global server, port */

window.addEventListener('load', startGUI, false);

function startGUI() {
    'use strict';
    var sock, uuid, i, longString = '';

    //********************************************
    //   Prepare the socket ecosystem :-) 
    //*******************************************

    sock = socketWebClient(server, port, '/web');
 // overwrite dummy functions
    sock.setCallbackReady( ready);
    sock.setCallbackReadMessage( readMessage);
    sock.setCallbackStatus( sockStatus);
    

phpWebSocketServer/webClient/socketWebClient.php

Here are the functions that overwrite the internal dummy functions

 
function setCallbackStatus(func) {
    //  overwrite dummy call back with your own func
    callbackStatus = func;
}
function setCallbackReady(func) {
    //  overwrite dummy call back with your own func
    callbackReady = func;
}
function setCallbackReadMessage(func) {
    //  overwrite dummy call back with your own func
    callbackReadMessage = func;
}
        


        //********************************************
        //   Connect 
        //*******************************************

        sock.init();
        uuid = sock.uuid;

        //********************************************
        //   create a long message 
        //*******************************************

        for (i = 0; i < 16 * 1024; i++) {
            longString += 'X';
        }

        function sockStatus(m) {
            //*******************************
            //  report connection status 
            //*******************************
           document.getElementById('connect').innerHTML = m;
        }

        function readMessage(packet) {
            //*******************************
            //  respond to messages from server 
            //*******************************
            var obj;
            if (packet.opcode === 'broadcast') {
                obj = document.getElementById('broadcast');
                obj.innerHTML += packet.message + '<br>';
            } else if (packet.opcode === 'feedback') {
                obj = document.getElementById('feedback');
                obj.innerHTML = packet.message;
            }
        }
        function ready() {
            //***********************************************
            //  test if messages appear in same order as send 
            //  no message is lost and very long message is buffered 
            //***********************************************
            sock.sendMsg({'opcode': 'broadcast', 'message': 'hallo11 from :' + uuid});
            sock.sendMsg({'opcode': 'broadcast', 'message': 'hallo22 from :' + uuid});
            sock.sendMsg({'opcode': 'broadcast', 'message': 'hallo33 from :' + uuid});
            sock.sendMsg({'opcode': 'broadcast', 'message': 'hallo44 from :' + uuid});
            sock.sendMsg({'opcode': 'broadcast', 'message': longString + uuid});
        }

        function triggerAJAX() {
            //****************************************
            //  start dummy backend script 
            //****************************************
            var req;
            req = new XMLHttpRequest();
            req.open("POST", '../phpClient/simulateBackend.php');
            req.setRequestHeader("Content-Type", "application/json");
            req.send(JSON.stringify({'uuid': uuid}));
        }
        //********************************************
        //   instrument the buttons 
        //*******************************************

        document.getElementById('ready').onclick = ready;
        document.getElementById('ajax').onclick = triggerAJAX;
        document.getElementById('uuid').innerHTML = uuid;

        }

    

Websocket handler

The core module handling socket traffic.

In here we listen to these events associated with the websocket

NOTE:

As we can not and will not block the javascript engine to wait for a next message from the server all messages are queued and sent only if the queue length transitions from 0 to 1 (kick start) or we receive the 'next' message from the server to send the next message from the queue.
See function sendMsg.

phpWebSocketServer/webClient/socketWebClient.js

function socketWebClient(server, port, app) {
    'use strict';
    var
            tmp = [], queue = [], uuid, socket = {}, serveros, proto,
            chunkSize = 6 * 1024, socketOpen = false, socketSend = false;
    
    //********************************************
    //  figure out what  protokoll to use 
    //*******************************************
    tmp = server.split('://');
    if (tmp[0] === 'ssl') {
        proto = 'wss://';
    } else {
        proto = 'ws://';
    }
    if (tmp.length > 1) {
        server = tmp[1];
    }

    uuid = generateUUID();
    function init() {

     callbackStatus('Try to connect ...');
    

phpWebSocketServer/webclient/startWebClient.js

Function callbackStatus is now overwritten with this one here.

function sockStatus(m) {
    //*******************************
    //  report connection status 
    //*******************************
    document.getElementById('connect').innerHTML = m;
    }
        
        //********************************************
        //    connect to server at port  
        //*******************************************
        socket = new WebSocket('' + proto + server + ':' + port + app);
        
        socket.onopen = function () {
            queue = [];
            callbackStatus('Connected');
        };
        socket.onerror = function () {
            if (socketSend === false) {
                callbackStatus('Can not connect to specified server');
            }
            socketSend = false;
            socketOpen = false;
            queue = [];
        };
        //********************************************
        //    look at message from server  
        //*******************************************
        socket.onmessage = function (msg) {
            var packet;
            if (msg.data.length === 0 || msg.data.indexOf('pong') >= 0) {
                return;
            }
            packet = JSON.parse(msg.data);
            if (packet.opcode === 'next') {
                //******************
                //* server is ready for next message
                //******************/
                queue.shift();
                if (queue.length > 0) {
                    //********************************************
                    //   next in line to send 
                    //*******************************************
                    msg = queue[0];
                    socket.send(msg);
                } else {
                    //********************************************
                    //   ready for next message; via kick start 
                    //*******************************************
                    queue = [];
                }
                return;
            }
            if (packet.opcode === 'ready') {
                //********************************************
                //   server is ready expecting UUID 
                //*******************************************
                socketOpen = true;
                socketSend = true;
                serveros = packet.os;
                msg = {'opcode': 'uuid', 'message': uuid};
                sendMsg(msg);
                callbackReady(packet);
    

phpWebSocketServer/webclient/startWebClient.js

Function callbackReady is now overwritten with this one here.

function ready() {
    //*******************************************************************
    // test if messages appear in same order as send 
    // no message is lost and very long message is buffered 
    //********************************************************************* 
    sock.sendMsg({'opcode': 'broadcast', 'message': 'hallo11 from :' + uuid});
    sock.sendMsg({'opcode': 'broadcast', 'message': 'hallo22 from :' + uuid});
    sock.sendMsg({'opcode': 'broadcast', 'message': 'hallo33 from :' + uuid});
    sock.sendMsg({'opcode': 'broadcast', 'message': 'hallo44 from :' + uuid});
    sock.sendMsg({'opcode': 'broadcast', 'message': longString + uuid});
}
        
                
                return;
            }
            if (packet.opcode === 'close') {
                //********************************************
                //   Server has closed connections 
                //*******************************************
                socketOpen = false;
                socketSend = false;
                callbackStatus('Server closed connection');
                return;
            }
            //********************************************
            //   have external fucntion look at message 
            //*******************************************
            callbackReadMessage(packet);
    

phpWebSocketServer/webclient/startWebClient.js

Function callbackReady is now overwritten with this one here.

function readMessage(packet) {
//*******************************
//  respond to messages from server 
//*******************************
    var obj;
    if (packet.opcode === 'broadcast') {
        obj = document.getElementById('broadcast');
        obj.innerHTML += packet.message + '<br>';
    } else if (packet.opcode === 'feedback') {
        obj = document.getElementById('feedback');
        obj.innerHTML = packet.message;
    }
}
        
 
        };
        //********************************************
        //   server has gone  
        //*******************************************
        socket.onclose = function () {
            queue = [];
            socketOpen = false;
            socketSend = false;
        };
    } /////////////////  End of init()  ///////////////////////////////

    //********************************************
    //   messages are queued 
    //*******************************************
    function sendMsg(msgObj) {
        var i, j, nChunks, msg, sendNow = false;
        if (!socketSend || !socketOpen) {
            return;
        }
        msg = JSON.stringify(msgObj);
        if (msg.length < chunkSize) {
            //********************************************
            //   normal short message 
            //*******************************************
            queue.push(msg);
        } else {
            //********************************************
            //   sending long messages in chunks 
            //*******************************************
            if (queue.length === 0) {
                sendNow = true;
            }
            queue.push('bufferON'); //command for the server
            nChunks = Math.floor(msg.length / chunkSize);
            for (i = 0, j = 0; i < nChunks; i++, j += chunkSize) {
                queue.push(msg.slice(j, j + chunkSize));
            }
            if (msg.length % chunkSize > 0) {
                queue.push(msg.slice(j, j + msg.length % chunkSize));
            }
            queue.push('bufferOFF'); //command for the server
        }

        if ((queue.length === 1 || sendNow) && socketOpen) {
            //********************************************
            //   kick start sending messages 
            //*******************************************
            msg = queue[0];
            socket.send(msg);
            sendNow = false;
        }
    }
    //********************************************
    //   dummy functions; should be set from outside 
    //*******************************************

    function callbackStatus(p) { //  dummy callback 
        return p;
    }
    function callbackReady(p) { //  dummy callback 
        return p;
    }
    function callbackReadMessage(p) { //  dummy callback 
        return p;
    }
    //********************************************
    //   functions to overwrite dummy functions 
    //*******************************************

    function setCallbackStatus(func) {
        //   overwrite dummy call back with your own func 
         callbackStatus = func;
    }
    function setCallbackReady(func) {
        //   overwrite dummy call back with your own func 
         callbackReady = func;
    }
    function setCallbackReadMessage(func) {
        //   overwrite dummy call back with your own func 
        callbackReadMessage = func;
    }
    //********************************************
    //    
    //*******************************************

    function generateUUID() { //  Public Domain/MIT 
        var d = new Date().getTime();
        if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
            d += performance.now(); //use high-precision timer if available
        }
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
            var r = (d + Math.random() * 16) % 16 | 0;
            d = Math.floor(d / 16);
            return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
        });
    }

    
    function quit() {
        sendMsg({'opcode': 'quit'});
        socket.close();
        socketOpen = false;
        socketSend = false;
    }
    function isOpen() {
        return socketOpen;
    }
    //********************************************
    //   reveal these function to the caller 
    //*******************************************

    return {
        'init': init,
        'sendMsg': sendMsg,
        'uuid': function () {
            return uuid;
        }(),
        'quit': quit,
        'isOpen': isOpen,
        'setCallbackReady': setCallbackReady,
        'setCallbackReadMessage': setCallbackReadMessage,
        'setCallbackStatus': setCallbackStatus
    };
}
    

PHP client

Commandline

This is the php script to test connection to and working with the WebSocketServer.

php testWithPHPSocket.php

This will connect to the server and send a bunch of messages. These messages are supposed to show up in all the connectet WebClients, because they are send using broadcast.

phpWebSocketServer/phpClient/testWithPHPSocket.php



if ($argc > 1) {
    parse_str(implode('&', array_slice($argv, 1)), $_GET);
}

require 'socketPhpClient.php';
include '../include/adressPort.inc.php';// remember this one ?

$talk = new socketTalk($Address, $Port, '/php');
if (!isset($_GET['m'])) {
    $_GET['m'] = '';
}


$message = trim($_GET['m']);
if ($message == '') {
    //return;
    $message = 'hallo from PHP';
} else {
    $talk->broadcast($message);
    $talk->silent();
    exit;
}
$longString = '';
for ($i = 0; $i < 9 * 1024; $i++) {
    $longString .= 'P';
}

/*
 * ***********************************************
 * test if messages apear in same order as send
 * no message is lost and very long message is buffered
 * ***********************************************
 */

$talk->broadcast("$message 1");
$talk->broadcast("$message 2");
$talk->broadcast("$message 3");
$talk->broadcast("$message 4");
$talk->broadcast("$message 5");
$talk->broadcast("äüöÄÜÖß$longString 6~6~6~6 ÄÜÖßäüö");


$talk->silent()
    

Socket handler

This is the class you should use when talking to the server.
Here we connect to the server and fake some handshake.

NOTE:

When writing something to the server we wait on a blocking fread for the ACK/Next from the server. Because of this it is probably not a good idea to send messages to the server in a loop while fiercly processing data.

phpWebSocketServer/phpClient/socketPhpClient.php



class socketTalk {

    public $uuid, $connected = false, $serveros = 'linux', $chunkSize = 6 * 1024;
    private $socketMaster;

    function __construct($Address, $Port, $application = '/', $uu = '') {
        $context = stream_context_create();
        $arr = explode('://', $Address, 2);
        if (count($arr) > 1) {
            if (strncasecmp($arr[0], 'ssl', 3) == 0) {
                stream_context_set_option($context, 'ssl', 'allow_self_signed', true);
                stream_context_set_option($context, 'ssl', 'verify_peer', false);
                stream_context_set_option($context, 'ssl', 'verify_peer_name', false);
            } else {
                $Address = $arr[1]; // just the host
            }
        }
        $errno = 0;
        $errstr = '';
        $this->socketMaster = stream_socket_client("$Address:$Port", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context);

        if (!$this->socketMaster) {
            $this->connected = false;
            return;
        }
        $this->connected = true;
        fwrite($this->socketMaster, "php process\\nGET $application HTTP/1.1\\n");

        $buff = fread($this->socketMaster, 256); // wait for ACK 
        $json = json_decode($buff);
        if ($json->opcode != 'ready') {
            $this->connected = false;
        }
        if ($uu != '') {
            $this->uuid = $uu;
        }
    }

    final function broadcast($message) {
        $this->talk(['opcode' => 'broadcast', 'message' => $message]);
    }

    final function feedback($message) {
        if ($this->uuid) {
            $this->talk(['opcode' => 'feedback', 'uuid' => $this->uuid, 'message' => $message]);
        }
    }

    final function talk($msg) {
        if ($this->connected === false) {
            return;
        }
        $json = json_encode((object) $msg, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK);
        $len = mb_strlen($json);
        if ($len > $this->chunkSize) {
            $nChunks = floor($len / $this->chunkSize);
            if ($this->writeWait('bufferON')) {
                for ($i = 0, $j = 0; $i < $nChunks; $i++, $j += $this->chunkSize) {
                    if ($this->writeWait(mb_substr($json, $j, $j + $this->chunkSize)) === false) {
                        break;
                    }
                }
            }
            if ($len % $this->chunkSize > 0) {
                $this->writeWait(mb_substr($json, $j, $j + $len % $this->chunkSize));
            }
            $this->writeWait('bufferOFF');
        } else {
            $this->writeWait($json);
        }
    }

    final function silent() {
        if ($this->connected) {
            $this->connected = false;
            fclose($this->socketMaster);
        }
    }

    final function writeWait($m) {
        if ($this->connected === false) {
            return false;
        }
        fwrite($this->socketMaster, $m);
        $buff = fread($this->socketMaster, 256); // wait for ACK
        $ack = json_decode($buff);
        if ($ack->opcode != 'next') {
            $this->silent();
            return false;
        }
        return true;
    }

}
    

A demo backend script

This script is executed when you press in the webclient giving you feedback about what is going on.

phpWebSocketServer/phpClient/socketPhpClient.php


require 'socketPhpClient.php';
include '../include/adressPort.inc.php';
/*
 * ***********************************************
 * get parameters from client
 * ***********************************************
 */
$json = file_get_contents('php://input');
$payload = (object) json_decode($json, true);
/*
 * ***********************************************
 * connect to the server
 * ***********************************************
 */
$talk = new socketTalk($Address, $Port, '/php', $payload->uuid);
/*
 * ***********************************************
 * send feedback to client
 * ***********************************************
 */

$talk->feedback("doing some work sleep(1)");
sleep(1); // work
$talk->feedback("very importand work  sleep(2)");
sleep(2); // work
for ($i = 0; $i < 1000000; $i++) {
    if ($i % 1000 == 0) {
        $talk->feedback("loop $i");
    }
}
$talk->feedback( "done");
/*
 * ***********************************************
 * end of AJAX call
 * ***********************************************
 */
echo json_encode($payload,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_NUMERIC_CHECK);


    

Credits

Without the work of many other people, this server would probably not exist. Thank you !