Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Find object in SplObjectStorage by attached info

Tags:

php

I build a chat app using PHP Ratchet.

I store all my connection in SplObjectStorage.

Each connection will have user id that I will attach him by this:

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

   public function onOpen(ConnectionInterface $conn)
    {
        // Store the new connection to send messages to later
        $querystring = $conn->WebSocket->request->getQuery();

        foreach ($querystring as $value)
        {
            if($key == "senderId")
                $senderId = $value;
        } 

        $this->clients->attach($conn, $senderId);

        echo "New connection! ({$conn->resourceId}) senderId({$senderId})\n";
    }

When a message arrive I want the fastest way to get the $conn object related to the specific user id. I can use the trivial foreach like this:

public function onMessage(ConnectionInterface $from, $msg) {
        $numRecv = count($this->clients) - 1;
        echo sprintf('Connection %d sending message "%s" to %d other connection%s' . "\n"
            , $from->resourceId, $msg, $numRecv, $numRecv == 1 ? '' : 's');

     foreach ($this->clients as $client)
     {
                if ($from->getInfo() !== $client->getInfo()) {

                   // do stuff
                }
    }

I wonder if there is a faster method. Maybe using some function like this:

$conn = $this->clients->getClientWithInfo("WANTED-INFO");

The wanted approach is to void the loop over all my connection in order the send a message to a specific user. I want to get the connection that is associated with a the user id.

like image 733
dasdasd Avatar asked Mar 01 '15 09:03

dasdasd


2 Answers

In my opinion there is only one solution to make it work, like you expected => extending the SplObjectStorage class. But then you have two options.

First you can be lazy and add a getClientWithInfo method to the class which find the object for you:

class ConnectionStorageSimple extends SplObjectStorage
{
    public function getClientWithInfo($info)
    {
        $this->rewind();
        while ($this->valid()) {
            $object = $this->current(); // similar to current($s)
            $data = $this->getInfo();
            if ($info === $data) {
                $this->rewind();

                return $object;
            }
            $this->next();
        }

        return null;
    }
}

$conStorage = new ConnectionStorageSimple();

$con1 = new \stdClass();
$con1->id = 1;
$con2 = new \stdClass();
$con2->id = 2;


$conStorage->attach($con1, 1);
$conStorage->attach($con2, 2);

var_dump($conStorage->getClientWithInfo(1));

var_dump($conStorage->getClientWithInfo(2));

/**
 This will output something like that:
 class stdClass#2 (1) {
   public $id =>
   int(1)
 }
 class stdClass#3 (1) {
   public $id =>
   int(2)
 }
*/

The other option is, that you build your one info-object mapping based on the parent function. This is a little more complex:

<?php

class ConnectionStorage extends SplObjectStorage
{

    private $objInfoMapping = array();

    public function attach($object, $data = null)
    {
        if (null !== $data) {
            $this->objInfoMapping[$data] = $object;
        }
        parent::attach($object, $data);
    }

    public function detach($object)
    {
        $this->detach($object);
        parent::detach($object);
    }

    public function addAll($storage)
    {
        $this->addStorage($storage);

        parent::addAll($storage);
    }

    public function removeAll($storage)
    {
        $this->objInfoMapping = array();
        parent::removeAll($storage);
    }

    public function removeAllExcept($storage)
    {
        $this->objInfoMapping = array();
        $this->addStorage($storage);
        parent::removeAllExcept($storage);
    }

    public function unserialize($serialized)
    {
        parent::unserialize($serialized);
        $this->addStorage($this);
    }

    public function offsetUnset($object)
    {
        $this->detach($object);
        parent::offsetUnset($object);
    }

    protected function detachObject($obj)
    {
        $info = $this[$obj];
        if (array_key_exists($info, $this->objInfoMapping)) {
            unset($this->objInfoMapping[$info]);
        }
    }

    protected function addStorage(SplObjectStorage $storage)
    {
        $storage->rewind();
        while ($storage->valid()) {
            $object = $storage->current(); // similar to current($s)
            $data = $storage->getInfo();
            $this->objInfoMapping[$data] = $object;
            $storage->next();
        }
    }

    public function getClientWithInfo($info)
    {
        if (array_key_exists($info, $this->objInfoMapping)) {
            return $this->objInfoMapping[$info];
        }
    }

}

$conStorage = new ConnectionStorage();

$con1 = new \stdClass();
$con1->id = 1;
$con2 = new \stdClass();
$con2->id = 2;


$conStorage->attach($con1, 1);
$conStorage->attach($con2, 2);

var_dump($conStorage->getClientWithInfo(1));

var_dump($conStorage->getClientWithInfo(2));

/**
 This will also output something like that:
 class stdClass#2 (1) {
   public $id =>
   int(1)
 }
 class stdClass#3 (1) {
   public $id =>
   int(2)
 }
*/

The main difference between the two classes is, that the second example will perform better on big datasets, because you do not have to iterate over all objects of the storage. And because you store just object references to the own array, the extra memory consumption should not be so big.

Disclaimer: The classes are just to illustrate the possibilities. The first one should be save to use, but the second one should be tested more

Hope this helps.

like image 130
skroczek Avatar answered Nov 14 '22 20:11

skroczek


this is what I did, see that it's simpler and fast.

namespace mine;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;

class ws implements MessageComponentInterface {
    protected $clients;
    protected $clientids;

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

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

    public function send_to($to,$msg) {
        if (array_key_exists($to, $this->clientids)) $this->clientids[$to]->send($msg);
    }

    public function onOpen(ConnectionInterface $conn) {
        $socket_name = "{$conn->resourceId}@{$conn->WebSocket->request->getHeader('X-Forwarded-For')}";
        $this->clients->attach($conn,$socket_name);
        $this->clientids[$socket_name] = $conn;
    }

    public function onMessage(ConnectionInterface $from, $msg) {

    }

    public function onClose(ConnectionInterface $conn) {
        unset($this->clientids[$this->clients[$conn]]);
        $this->clients->detach($conn);
    }

    public function onError(ConnectionInterface $conn, \Exception $e) {
        $conn->close();
    }
}

this adds 2 functions, one for multicast, and another to message a client by the socket_name which is a string ID (I chose a combination of socket id and ip to stop possible collisions).

so to send to a client:

$ws->send_to(socket_name,message);

obviously $ws is the websocket created at initisation:

$ws = new mine\ws();
$ws_server = new Ratchet\Server\IoServer( new Ratchet\Http\HttpServer( new Ratchet\WebSocket\WsServer( $ws ) ), $socket );
like image 32
Smellymoo Avatar answered Nov 14 '22 20:11

Smellymoo