2

I have a very simple websocket using PHP and Ratchet libraray.

When a user opens a specific page it sends the users id to my socket and it should update the status for that user (at the moment I'm just logging it in the console), like this:

<input type="hidden" value="'.$account_id.'" id="account_id">
<input type="hidden" value="trial" id="request_type">
<script>
$(document).ready(function(){
    var conn = new WebSocket('ws://127.0.0.1:8080');

    conn.onopen = function(e){
        console.log("Connection Opened!");
        var account_id = $("#account_id").val();
        var request_type = $("#request_type").val();
        var data = {account_id: account_id, request_type: request_type};
        conn.send(JSON.stringify(data));
    }
    conn.onclose = function(e){
        console.log("Connection Closed!");
    }
    conn.onmessage = function(e) {
        var data = JSON.parse(e.data);
        console.log(data);
    };
    conn.onerror = function(e){
        var data = JSON.parse(e.data);
        console.log(data);
    }
})
</script>

Then my socket script is as follows:

set_time_limit(0);

use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
require dirname(__DIR__) . '../vendor/autoload.php';

class socket implements MessageComponentInterface{
    protected $clients;

    public function __construct(){
        $this->clients = new \SplObjectStorage;
        echo 'Server Started.'.PHP_EOL;
    }

    public function onOpen(ConnectionInterface $socket){
        $this->clients->attach($socket);
        echo 'New connection '.$socket->resourceId.'!'.PHP_EOL;
    }
    public function onClose(ConnectionInterface $socket) {
        $this->clients->detach($socket);
        echo 'Connection '.$socket->resourceId.' has disconnected'.PHP_EOL;
    }
    public function onError(ConnectionInterface $socket, \Exception $e) {
        echo 'An error has occurred: '.$e->getMessage().'!'.PHP_EOL;
        $socket->close();
    }
    public function onMessage(ConnectionInterface $from, $json){
        echo 'Connection '.$from->resourceId.' sent '.$json.PHP_EOL;
        $data = json_decode($json, true);
        $account_id = $data['account_id'];
        $request_type = $data['request_type'];

        try {
            $conn = new PDO("mysql:host=".$db_host.";port:".$db_port.";dbname=".$db_name."", $db_user, $db_pass);
            $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        }catch(PDOException $e){
            echo $e->getMessage();
        }
        
        foreach ($this->clients as $client) {
            if ($from->resourceId == $client->resourceId) {
                if($request_type == 'trial'){
                    // while(true){
                        $response_array= [];
                        $stmt = $conn->prepare("SELECT * FROM table WHERE account_id=:account_id AND last_status_change=now()");
                        $stmt->bindParam(':account_id', $account_id);
                        $stmt->execute();
                        $result = $stmt->setFetchMode(PDO::FETCH_ASSOC);
                        foreach($stmt->fetchAll() as $key=>$value) {
                            $response_array[$key] = $value;
                        }
                        if(!empty($response_array)){
                            foreach($response_array as $item){
                                $status = $item['status'];
                            }
                            $response = array(
                                'account_id' => $account_id,
                                'status' => $status
                            );
                            var_dump($response);
                            $client->send(json_encode($response));
                        }
                        // sleep(5);
                    // }
                }
            }
        }
    }
}

$server = IoServer::factory(
    new HttpServer(
        new WsServer(
            new socket()
        )
    ),
    8080
);
$server->run();

As it stands it works as expected, but only gives the current status if the status changed at the time when the page was loaded and I will see the status in the console, as soon as I un-comment the while() loop to actually keep checking the status for updates, my socket will do the var_dump() of the result in the command line when there is a status change but nothing gets logged in the client.

I'm new to websockets, I had been doing long polling by having an interval in JS that was sending a fetch() to a PHP script that got the latest DB results but it wasn't very efficient and was causing issues when a large number of clients were active and constantly making requests to the file which was in turn slowing down the DB. So I'm not sure why the while() loop is affecting it like this or if I am even going about this the right way.

halfer
  • 19,824
  • 17
  • 99
  • 186
Paddy Hallihan
  • 1,624
  • 3
  • 27
  • 76
  • 1
    Note that we prefer a technical style of writing here. We gently discourage greetings, hope-you-can-helps, thanks, advance thanks, notes of appreciation, regards, kind regards, signatures, please-can-you-helps, chatty material and abbreviated txtspk, pleading, how long you've been stuck, voting advice, meta commentary, etc. Just explain your problem, and show what you've tried, what you expected, and what actually happened. – halfer Jan 16 '21 at 12:14
  • where is the $request_type variable come from? – Karthick Jan 16 '21 at 15:03
  • 1
    @Karthick sorry my mistake, was trying to give the minimal example. It is another hidden input on the client side that is sent the exact same way as the account_id – Paddy Hallihan Jan 18 '21 at 09:15
  • Does ratchet even work this way? `while` loop sounds fishy and blocking. – Daniel W. Jan 18 '21 at 11:48
  • Hi Paddy. Please leave the question in its edited state - it is a good edit. Editors trim fluff and conversational material here - _Meta_ references are available on request. – halfer Jan 22 '21 at 21:04

3 Answers3

2

replace this line if ($from->resourceId == $client->resourceId) { with if ($from == $client) { this change may look simple but in the example Chat class provided by php ratchet in order avoid sending the message to the sender they have a condition to send messages to clients except the sender, they compared like this if ($from == $client) { only not only an resourceId the entire object itself!

Karthick
  • 281
  • 2
  • 7
  • Thanks for your suggestion I've made this change, however the main issue still appears to be with the while loop, they way it is commented out now works and the client logs the correct info, as soon as I try to put it in a loop, the client doesn't appear to be receiving anything – Paddy Hallihan Jan 18 '21 at 11:36
2

A while loop is not how it works. It will block stuff and infinitely and unnecessarily consume resources.

What you want is addPeriodicTimer().

Check periodically for clients that need updates.

Add to your bootstrapping something like this:

$reactEventLoop->addPeriodicTimer(5, function() use $messageHandler, $server {
    // Fetch all changed clients at once and update their status
    $clientsToUpdate = getUpdatedClients($server->app->clients);
    foreach ($clientsToUpdate as $client) {
        $client->send(json_encode($response));
    }
});

This is much more lightweight than any other method, as you can

  1. Fetch N clients status with a single prepared database query
  2. Update only changed clients periodically
  3. Not put your app in a blocking state

Other resources on Stackoverflow will help you to find the right spot:

How do I access the ratchet php periodic loop and client sending inside app?

Periodically sending messages to clients in Ratchet

Benjamin Loison
  • 3,782
  • 4
  • 16
  • 33
Daniel W.
  • 31,164
  • 13
  • 93
  • 151
0

you should be using addPeriodicTimer from Ratchet, although you have to make $clients public in order to place the timer. Maybe you can place it inside the class and still be private, but I am not sure if it could initiate a timer for every client.

Anyway as you can see, you can create another public function that will actually do the job in the periodic timer(just like while loop) and then call it once the client is connected and multiple times inside the timerloop, for that I created also a public account_ids to keep truck of the account ids

Give it a try and let me know

use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
require dirname(__DIR__) . '../vendor/autoload.php';

class socket implements MessageComponentInterface{
    public $clients;
    public $account_ids;

    public function __construct(){
        $this->clients = new \SplObjectStorage;
        echo 'Server Started.'.PHP_EOL;
    }

    public function onOpen(ConnectionInterface $socket){
        $this->clients->attach($socket);
        echo 'New connection '.$socket->resourceId.'!'.PHP_EOL;
    }
    public function onClose(ConnectionInterface $socket) {
        $this->clients->detach($socket);
        echo 'Connection '.$socket->resourceId.' has disconnected'.PHP_EOL;
    }
    public function onError(ConnectionInterface $socket, \Exception $e) {
        echo 'An error has occurred: '.$e->getMessage().'!'.PHP_EOL;
        $socket->close();
    }
    public function onMessage(ConnectionInterface $from, $json){
        echo 'Connection '.$from->resourceId.' sent '.$json.PHP_EOL;
        $data = json_decode($json, true);
        $account_id = $data['account_id'];
        $request_type = $data['request_type'];
        foreach ( $this->clients as $client ) {
            if ( $from->resourceId == $client->resourceId ) {
                if( $request_type == 'trial'){
                    $this->account_ids[$client->resourceId] = $account_id;
                    $this->checkStatus($client, $account_id);
                }
            }
        }
    }
    public function checkStatus($client, $account_id){
        try {
            $conn = new PDO("mysql:host=".$db_host.";port:".$db_port.";dbname=".$db_name."", $db_user, $db_pass);
            $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        }catch(PDOException $e){
            echo $e->getMessage();
        }
        $response_array= [];
        $stmt = $conn->prepare("SELECT * FROM table WHERE account_id=:account_id AND last_status_change=now()");
        $stmt->bindParam(':account_id', $account_id);
        $stmt->execute();
        $result = $stmt->setFetchMode(PDO::FETCH_ASSOC);
        foreach($stmt->fetchAll() as $key=>$value) {
            $response_array[$key] = $value;
        }
        if ( !empty($response_array) ) {
            foreach($response_array as $item){
                $status = $item['status'];
            }
            $response = array(
                'account_id' => $account_id,
                'status' => $status
            );
            var_dump($response);
            $client->send(json_encode($response));
        }
    }
}

$socket = new socket();
$server = IoServer::factory(
    new HttpServer(
        new WsServer(
            $socket
        )
    ),
    8080
);
$server->loop->addPeriodicTimer(5, function () use ($socket) {
    foreach($socket->clients as $client) {
        echo "Connection ".$client->resourceId." check\n";
        $socket->checkStatus($client, $socket->account_ids[$client->resourceId]);
    }
});


$server->run();
Tch
  • 1,055
  • 5
  • 11