3
votes

I'm designing a chatroom in ratchet websockets to be as responsive as possible. It knows when a user leaves the page, and everything like that. But if a user/client for example loses its connection with the server, the issue is the client cant let the server know it has disconnected, because it has already disconnected and cant send the server a message. So how do I track when a chat client has lost their internet connection and is no longer online?

Two possible solutions I can think of:

  • server polls the clients once every 15 mins to half hour to check to see who is online. Clients who do not respond get disconnected. Is this possible to do without interrupting everything else going on in php? if so how? and where do I put the code? I saw something about addPeriodicTimer() from LoopInterface but im not sure if that would do the job or where the function would fit into my code. Also does it call sleep() function because that would not be good. I still want other tasks happening in the background while this function is on a timer (if possible in php)

  • onClose() method in php Can this detect when a user has really disconnected in every circumstance? If so, when this event fires off, how can I find out which user was disconnected? it only passes a ConnectionInterface and no message.

Sorry, im still new to the ratchet library and still trying to work out how to achieve this task.

my code for server.php:

<?php


require($_SERVER['DOCUMENT_ROOT'].'/var/www/html/vendor/autoload.php');

use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;

$server = IoServer::factory(new HttpServer(new WsServer(new Chat)), 8080);




$server->run();
?>

code for app.js

// JavaScript Document

var chat = document.getElementById("chatwindow");
var msg = document.getElementById("messagebox");
var refInterval = 0;
var timeup = false;
var awaytimer;
var socket = new WebSocket("ws://52.39.48.172:8080");
var openn = false;

function addMessage(msg){
        "use strict";
        chat.innerHTML += "<p>" + msg + "</p>";


}

msg.addEventListener('keypress', function(evt){
    "use strict";
    if(evt.charCode != 13)
        return;

    evt.preventDefault();

    if(msg.value == "" || !openn)
        return;

    socket.send(JSON.stringify({
            msg: msg.value,
            uname: nme,
            uid: id,
            tag: "[msgsend]"


    }));


    msg.value = "";



});

socket.onopen = function(evt) {
    openn = true;


    socket.send(JSON.stringify({
        uname: nme,
        uid: id,
        tag: "[connected]"



    }));




};

socket.onmessage = function(evt) {
    var data = JSON.parse(evt.data);

    if(data.tag == "[connected]")
    {
        addMessage(data.uname + " has connected...");

    }
    else if(data.tag == "[bye]")
    {
        addMessage(data.uname + " has left the room...");

        if(data.uname == nme)
            socket.close(); 
    }
    else if(data.tag == "[msgsend]")
    {
        addMessage(data.uname + ": " + data.msg);
    }
};



  window.onfocus = refPage;
  function refPage()
  {

      if(timeup == true)
      {


        if(refInterval == 1)
        {
            refInterval = 0;
            location.reload();
        }

      }
      else
      {
            clearTimeout(awaytimer);  
      }

      timeup = false;

  }

  window.onblur = timelyExit;  
  function timelyExit()
  {
        refInterval = 1;

        // change this to trigger some kind of inactivity timer
        awaytimer = setTimeout(function(){socket.send(JSON.stringify({
        uname: nme,
        uid: id,
        tag: "[bye]"                    
        })); timeup=true; }, 900000);


  }


  window.onoffline = window.onunload = window.onbeforeunload = confirmExit;
  function confirmExit()
  {
    socket.send(JSON.stringify({
        uname: nme,
        uid: id,
        tag: "[bye]"

    }));

    socket.close();
  }


socket.onclose = function() {

    openn = false;


           //cant send server this message because already closed.
    /*
    socket.send(JSON.stringify({
        uname: nme,
        uid: id,
        tag: "[bye]"

    }));

    */

    socket.close();


};

Code for chat.php

<?php

error_reporting(E_ALL ^ E_NOTICE);
session_id($_GET['sessid']);
    if(!session_id)
        session_start();


$userid = $_SESSION["userid"];
$username = $_SESSION["username"];
$isadmin = $_SESSION["isadmin"];


use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;

class Chat implements MessageComponentInterface
{
    protected $clients;

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




    }

    public function onOpen(ConnectionInterface $conn)
    {
        $this->clients->attach($conn);  

    }

    public function onClose(ConnectionInterface $conn)
    {

        $this->clients->detach($conn);  
    }

    public function onMessage(ConnectionInterface $conn, $msg)
    {
         $msgjson = json_decode($msg);
         $tag = $msgjson->tag;

         if($tag == "[msgsend]")
         {

            foreach($this->clients as $client)
            {
                  $client->send($msg);    
            }
         }
         else if($tag == "[bye]")
         {

             foreach($this->clients as $client)
             {
                  $client->send($msg);    
             }

             onClose($conn);
         }
         else if($tag == "[connected]")
         {
             //store client information


             //send out messages
              foreach($this->clients as $client)
             {
                  $client->send($msg);    
             }



         }







    }

    public function onError(ConnectionInterface $conn, Exception $e)
    {
        echo "Error: " . $e->getMessage(); 
        $conn -> close();   
    }

}

?>

Edit: just tested and confirmed onClose() method doesnt fire when internet connection is terminated.

is there a way i can still go about the first solution?

1

1 Answers

0
votes

This best solution for detecting disconnected clients would be event based and does not poll the clients at all. This approach would be your second solution, and also models itself nicely around the asynchronous nature of WebSocket message passing.

It is however possible as you state that some naughty clients in certain cases may not notify the socket server of their disconnection and leave it 'hanging', so to speak. In this case, I would suggest to not try to implement the polling trigger within the Socket Server itself and instead initiate the polling via a separate, server-side client that is triggered via cron or other task scheduler and instructs the socket server to initiate a request to poll all connected clients.

For more information on constructing server-side clients, see this question of mine for which I was also able to find some solutions.


In order to determine who sent the disconnect message, I would suggest going away from using just SplObjectStorage inside of the Ratchet\MessageComponentInterface implementation you have and instead wrap a simple array inside of another class, so something like this:

class MyClientList
{
    protected $clients = [];

    public function addClient(Connection $conn)
    {
        $this->clients[$conn->resourceId] = [
            'connection' => $conn,
        ];

        return $this;
    }

    public function removeClient(Connection $conn)
    {
        if(isset($this->clients[$conn->resourceId])) {
            unset($this->clients[$conn->resourceId]);
        }

        return $this;
    }

    public function registerClient(ConnectionInterface $conn, array $userData)
    {
        if(isset($this->clients[$conn->resourceId])) {
            $this->clients[$conn->resourceId] = array_merge(
                $this->clients[$conn->resourceId],
                $userData
            );
        }

        return $this;
    }

    public function getClientData(ConnectionInterface $conn)
    {
        return isset($this->clients[$conn->resourceId]) ?
            $this->clients[$conn->resourceId] :
            null
        ;
    }
}

At some point shortly after a user first connects, your client should send a socket message to the server and instruct the server to now register the identity of the client with additional information (in your case you are attempting to identify a uname and uid properties). By indexing against the connection id, you should be able to use this implementation to divine the identity of all messages originating from clients after they have sent in the initial registration message.