0

There is a symfony application that uses php websockets with Ratchet (http://socketo.me/docs/sessions). It seems to be very common to create a websocket application that can broadcast a received messages to all connected clients (web browsers). But I have big issues to send a message only to a specific client (e.g. load user with getUser() and find its belonging websocket client object).

This is my setup:

// WebsocketServer.php
$server = IoServer::factory(
    new HttpServer(
        new SessionProvider(
            new WsServer(
                new WebSocketCommunicator()
            ),
            new MySessionHandler())

    ),
    $this->_websocketPort
);

$server->run();
// Handler for communication with clients
class WebSocketCommunicator implements HttpServerInterface
{

    protected $clients;

    public function __construct()
    {
        $this->clients = new \SplObjectStorage();
    }

    public function onOpen(ConnectionInterface $conn, RequestInterface $request = null)
    {
        echo "URI: " . $conn->httpRequest->getUri()->getHost();
        echo "KEY: " . $conn->Session->get('key');
        
        // ==> I need somethink like:
        $user = $conn->Session->get('user_from_session')->getId();

        // Attach new client
        $this->clients->attach($conn);
    }

    public function onMessage(ConnectionInterface $pClient, $msg)
    {
        $msgObj = json_decode($msg);
        echo "WebSocket-Request msg: " . $msg->msg;
    }
}

When using websockets there is no regular session or security object where I can load the logged in user entity (->getUser()). As described in the link above there is the possibility to use the Symfony2 Session object or the httpRequest object that contains header and cookie data. Is there a way to retrieve the user object or even the user id in on of there instances?

All I could find are examples and workarounds like these:

Send user ID from browser to websocket server while opening connection

How do you send data to the server onload using ratchet websockets?

https://medium.com/@nihon_rafy/create-a-websocket-server-with-symfony-and-ratchet-973a59e2df94

Is there a modern solution to this now? Or any kind of a tutorial or reference where I could read about?

igi
  • 125
  • 1
  • 15

1 Answers1

0

I am developing a Laravel 8.0 application, PHP 7.4 and cboden/Ratchet package for the websocket server. I needed to send notifications to a user/group of users, or update the UI, when an event is fired in the backend.

What I did is:

1) Installed amphp/websocket-client package using composer.

2) Created a separated class in order to instantiate an object that could get connected to the websocket server, send the desired message and disconnect:

namespace App;
use Amp\Websocket\Client;

class wsClient {

   public function __construct() {
      //
   }


   // Creates a temporary connection to the WebSocket Server
   // The parameter $to is the user name the server should reply to.
   public function connect($msg) {
      global $x;
      $x = $msg;
      \Amp\Loop::run(
         function() {
            global $x;
            $connection = yield Client\connect('ws://ssa:8090');
            yield $connection->send(json_encode($x));
            yield $connection->close();
            \Amp\Loop::stop();
         }
      );
   }
}

3) The onMessage() event, in the handler class, looks like this:

   /**
    * @method onMessage
    * @param  ConnectionInterface $conn
    * @param  string              $msg
    */   
   public function onMessage(ConnectionInterface $from, $msg) {
      $data = json_decode($msg);
      // The following line is for debugging purposes only
      echo "   Incoming message: " . $msg . PHP_EOL;
      if (isset($data->username)) {
         // Register the name of the just connected user.
         if ($data->username != '') {
            $this->names[$from->resourceId] = $data->username;
         }
      }
      else {
         if (isset($data->to)) {
            // The "to" field contains the name of the users the message should be sent to.
            if (str_contains($data->to, ',')) {
               // It is a comma separated list of names.
               $arrayUsers = explode(",", $data->to);
               foreach($arrayUsers as $name) {
                  $key = array_search($name, $this->names);
                  if ($key !== false) {
                     $this->clients[$key]->send($data->message);
                  }
               }
            }
            else {
               // Find a single user name in the $names array to get the key.
               $key = array_search($data->to, $this->names);
               if ($key !== false) {
                  $this->clients[$key]->send($data->message);
               }
               else {
                  echo "   User: " . $data->to . " not found";
               }
            }
         } 
      }

      echo "  Connected users:\n";
      foreach($this->names as $key => $data) {
         echo "   " . $key . '->' . $data . PHP_EOL;
      }
   }

As you can see the user(s), you want the websocket server to send the message to, are specified as a string ($data->to) in the $msg parameter along with the message itself ($data->message). Those two things are JSON encoded so that the parameter $msg can be treated as an object.

4) On the client side (javascript in a layout blade file) I send the user name to the websocket server when the client connects (just like your first link suggests to)

    var currentUser = "{{ Auth::user()->name }}";
    socket = new WebSocket("ws://ssa:8090");
    
    socket.onopen = function(e) {
       console.log(currentUser + " has connected to websocket server");
       socket.send(JSON.stringify({ username: currentUser }));
    };
    
    socket.onmessage = function(event) {
       console.log('Data received from server: ' + event.data);
    };

So, the user name and its connection number are saved in the websocket server.

5) The onOpen() method in the handler class looks like this:

   public function onOpen(ConnectionInterface $conn) {
      // Store the new connection to send messages to later
      $this->clients[$conn->resourceId] = $conn;
      echo " \n";
      echo "  New connection ({$conn->resourceId}) " . date('Y/m/d h:i:sa') . "\n";
   }

Every time a client gets connected to the websocket server, its connection number or resourceId is stored in a array. So that, the user names are stored in an array ($names), and the keys are stored in another array ($clients).

6) Finally, I can create an instance of the PHP websocket client anywhere in my project:

public function handle(NotificationSent $event) {
    $clientSocket = new wsClient();
    $clientSocket->connect(array('to'=>'Anatoly,Joachim,Caralampio', 'message'=>$event->notification->data));
}

In this case I am using the handle() method of a notification event Listener.

Alright, this is for anyone wondering how to send messages from the PHP websocket server (AKA echo server) to one specific client or to a group of clients.

jgarcias
  • 337
  • 3
  • 17
  • I am now dealing with how to keep the websocket client connection persistent even between web requests or different tabs in the browser window. – jgarcias Jun 03 '21 at 07:45