How to reconnect a client automatically on ratchetphp? - php

I'm using rachetphp to create a client for an api server.
But i have a problem, when my connection close, whatever the reason, i can't reconnect automatically.
here the lib i use : https://github.com/ratchetphp/Pawl
<?php
require __DIR__ . '/vendor/autoload.php';
$loop = React\EventLoop\Factory::create();
$connector = new Ratchet\Client\Connector($loop);
$connector('ws://127.0.0.1:9000', ['protocol1', 'subprotocol2'], ['Origin' => 'http://localhost'])
->then(function(Ratchet\Client\WebSocket $conn) {
$conn->on('message', function(\Ratchet\RFC6455\Messaging\MessageInterface $msg) use ($conn) {
echo "Received: {$msg}\n";
$conn->close();
});
$conn->on('close', function($code = null, $reason = null) {
echo "Connection closed ({$code} - {$reason})\n";
});
$conn->send('Hello World!');
}, function(\Exception $e) use ($loop) {
echo "Could not connect: {$e->getMessage()}\n";
$loop->stop();
});
$loop->run();
I would like to try a reconnect every Seconds after a connection close.
Any ideas?

The idea is simple but it needs some refactoring. We must put the reconnect code in the handler that is executed when the connection is closed. In order to do that we pass the $app function inside self.
require __DIR__ . '/vendor/autoload.php';
$loop = React\EventLoop\Factory::create();
$connector = new Ratchet\Client\Connector($loop);
$app = function (Ratchet\Client\WebSocket $conn) use ($connector, $loop, &$app) {
$conn->on('message', function (\Ratchet\RFC6455\Messaging\MessageInterface $msg) use ($conn) {
echo "Received: {$msg}\n";
$conn->close();
});
$conn->on('close', function ($code = null, $reason = null) use ($connector, $loop, $app) {
echo "Connection closed ({$code} - {$reason})\n";
//in 3 seconds the app will reconnect
$loop->addTimer(3, function () use ($connector, $loop, $app) {
connectToServer($connector, $loop, $app);
});
});
$conn->send('Hello World!');
};
function connectToServer($connector, $loop, $app)
{
$connector('ws://127.0.0.1:9000', ['protocol1', 'subprotocol2'], ['Origin' => 'http://localhost'])
->then($app, function (\Exception $e) use ($loop) {
echo "Could not connect: {$e->getMessage()}\n";
$loop->stop();
});
}
connectToServer($connector, $loop, $app);
$loop->run();
The idea is that when the connection receives the close event we do a reconnect using the connectToServer function:
$conn->on('close', function ($code = null, $reason = null) use ($connector, $loop, $app) {
echo "Connection closed ({$code} - {$reason})\n";
//in 3 seconds the app will reconnect
$loop->addTimer(3, function () use ($connector, $loop, $app) {
connectToServer($connector, $loop, $app);
});
});

If you have pnctl (http://php.net/manual/en/book.pcntl.php) installed, you can just change the 'close' handler to this:
$conn->on('close', function($code = null, $reason = null) {
echo "Connection closed ({$code} - {$reason})\n";
// restart myself
global $argv;
pcntl_exec($_SERVER['_'], $argv);
exit();
});
For me this was simpler because I didn't already have the $loop and $connector extracted, I am just using
\Ratchet\Client\connect($ws_url, [], $headers)
->then(function ($conn) {
echo "connected\n";
...
To handle it in multiple places, I created a little helper function that I call on those events (based on the case, I add a delay).
function restartMyself() {
global $argv;
pcntl_exec($_SERVER['_'], $argv);
exit();
}

Related

send TCP message in loop

I am trying to send a tcp message in a script with reading from stdin in a loop. The problem is that the server only receives the connection, not the message. Everything works if I remove the loop.
#!/bin/php
<?php
use React\Socket\ConnectionInterface;
use React\Socket\TcpConnector;
require dirname(__DIR__) . '/vendor/autoload.php';
while (true) {
$line = trim(fgets(STDIN));
echo "try to send $line\n";
try {
$client = new TcpConnector();
$client
->connect('tcp://127.0.0.1:8080')
->then(
function (ConnectionInterface $connection) use ($line) {
echo "fulfilled\n";
$connection->write($line);
$connection->end();
},
function () {
echo "rejected\n";
},
function () {
echo "onProgress\n";
}
);
} catch (Throwable $e) {
echo $e->getMessage();
}
// break; // work with this break
}
with this code, when I send
./console.php
foo
try to send foo
bar
try to send bar
On the server side :
new connection: tcp://127.0.0.1:40936
new connection: tcp://127.0.0.1:40944
If I remove the loop :
./console.php
foo
try to send foo
fulfilled
Note: work great with telnet
Ok, some question about ratchet in a loop tel me the answer.
The loop wait the end of script before run... It never happen in this case.
We need to run $loop->run() at the right time.
The corrected code :
#!/bin/php
<?php
use React\EventLoop\Loop;
use React\Socket\ConnectionInterface;
use React\Socket\TcpConnector;
require dirname(__DIR__) . '/vendor/autoload.php';
while (true) {
$line = trim(fgets(STDIN));
echo "try to send $line\n";
try {
$loop = Loop::get();
$client = new TcpConnector($loop);
$client
->connect('tcp://127.0.0.1:8080')
->then(
function (ConnectionInterface $connection) use ($line) {
echo "fulfilled\n";
$connection->write($line);
$connection->end();
},
function () {
echo "rejected\n";
},
function () {
echo "onProgress\n";
}
);
$loop->run();
} catch (Throwable $e) {
echo $e->getMessage();
}
}
I think your solution fits your use case, just beware that $line = trim(fgets(STDIN)); will always block your code, because it waits on your input.
To simplify your example a bit more:
#!/bin/php
<?php
use React\EventLoop\Loop;
use React\Socket\ConnectionInterface;
use React\Socket\TcpConnector;
require dirname(__DIR__) . '/vendor/autoload.php';
while (true) {
$line = trim(fgets(STDIN));
echo "try to send $line\n";
$loop = Loop::get();
$client = new TcpConnector($loop);
$client
->connect('tcp://127.0.0.1:8080')
->then(
function (ConnectionInterface $connection) use ($line) {
echo "fulfilled\n";
$connection->write($line);
$connection->end();
},
function () {
echo "rejected\n";
}
);
$loop->run();
}
Your try-catch does actually nothing here, so you won't need it. This is because of the promise behavior in ReactPHP. If an exception is thrown inside a promise, you can only see it if you explicitly catch it or define a behavior if the promise gets rejected, e.g. like this:
// your reject
function () {
echo "rejected\n";
}
// my suggested reject
function (Exception $error) {
echo 'Error: ' . $error->getMessage() . PHP_EOL;
}
You can also take a look at https://github.com/clue/reactphp-stdio as an alternative approach for your use case.

PHP/Ratchet websocket - issues with while loop

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.
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
Fetch N clients status with a single prepared database query
Update only changed clients periodically
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
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!
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();

"This server only speaks WebSocket subprotocols wamp.2"

I am trying to subscribe to a channel of an api. I tried using 'ratchetphp/Pawl' and Laravel and this is my code:
$loop = Factory::create();
$connector = new Connector($loop);
$connector('wss://api.poloniex.com')
->then(function(WebSocket $conn) {
$conn->send('{"event": "subscribe","channel":"ticker","pair": "BTC_ETH"}');
$conn->on('message', function(MessageInterface $msg) use ($conn) {
var_dump($msg);
});
}, function(\Exception $e) use ($loop) {
/** hard error */
echo "Could not connect: {$e->getMessage()}\n";
$loop->stop();
});
$loop->run();
However, I am receiving error:
This server only speaks WebSocket subprotocols wamp.2.cbor.batched, wamp.2.cbor, wamp.2.msgpack.batched, wamp.2.msgpack, wamp.2.json.batched, wamp.2.json
I also tried $connector('wss://api.poloniex.com', ['wamp']) and $connector('wss://api.poloniex.com', ['wamp2']) but same error persists.
What am I doing wrong?

How to implement a loop inside a websocket?

I have a php code that calls another php script to get new data for a cache "JSON string". Here is my current code what does the communication
if( ! isset($_COOKIE['ICWS_SESSION']) ){
throw new exception('Not Clocked In');
}
if( ! isset($_COOKIE['ICWS_STATION']) ){
throw new exception('Missing Station Name');
}
$url = \classes\Cipher::decrypt($_COOKIE['ICWS_SESSION']);
$attrebutes = array('test');
//configure the connection
$conf = new ICWS\Config\Config($url, $_COOKIE['ICWS_STATION']);
//create a new instance of icws
$icws = new ICWS\Connection($conf, $attrebutes);
if ( !$icws->isLogged() ){
throw new exception('Something Went Wrong when trying to login');
}
$messaging = new ICWS\Messaging($icws);
while(1){
$messaging->processMessages();
$myQueue = array();
$myQueue = array_merge( (array) $messaging->getCallsQueue(), (array) $messaging->getCurrentUserStatusQueue()) ;
echo json_encode($myQueue);
sleep(1);
}
Now, I want to add this code in a Ratchet PHP WebSocket to ensure 1 TCP connection is made to the server per users to handle this request.
Here is my current websocket implementation.
<?php
namespace MyApp;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
class Chat implements MessageComponentInterface {
protected $clients;
public function __construct() {
$this->clients = new \SplObjectStorage;
}
//open connection
public function onOpen(ConnectionInterface $conn) {
// Store the new connection to send messages to later
$this->clients->attach($conn);
echo "New connection! ({$conn->resourceId})\n";
}
//handle incoming messages from the client
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 === $client) {
// The sender is not the receiver, send to each client connected
$client->send($msg);
}
}
}
//handle close connection
public function onClose(ConnectionInterface $conn) {
// The connection is closed, remove it, as we can no longer send it messages
$this->clients->detach($conn);
echo "Connection {$conn->resourceId} has disconnected\n";
}
//handle errors
public function onError(ConnectionInterface $conn, \Exception $e) {
echo "An error has occurred: {$e->getMessage()}\n";
$conn->close();
}
}
And here is how I start this WebSocket
<?php
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use MyApp\Chat;
require dirname(__DIR__) . '/../vendor/autoload.php';
require dirname(__DIR__) . '/icws/MyApp/Chat.php';
$server = IoServer::factory(
new HttpServer(
new WsServer(
new Chat()
)
),
8010
);
$server->run();
Question
Where in my implementation I would add my loop after the connection is made?
Here is what I have tried
<?php
namespace MyApp;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
class icws implements MessageComponentInterface {
protected $clients;
protected $messaging;
public function __construct() {
$this->clients = new \SplObjectStorage;
}
//open connection
public function onOpen(ConnectionInterface $conn) {
// Store the new connection to send messages to later
$this->clients->attach($conn);
echo "New connection! ({$conn->resourceId})\n";
if( ! isset($_COOKIE['ICWS_SESSION']) ){
throw new exception('Not Clocked In');
}
if( ! isset($_COOKIE['ICWS_STATION']) ){
throw new exception('Missing Station Name');
}
$url = \classes\Cipher::decrypt($_COOKIE['ICWS_SESSION']);
$attrebutes = array('test');
//configure the connection
$conf = new ICWS\Config\Config($url, $_COOKIE['ICWS_STATION']);
//create a new instance of icws
$icws = new ICWS\Connection($conf, $attrebutes);
if ( !$icws->isLogged() ){
throw new exception('Something Went Wrong when trying to login');
}
$this->messaging = new ICWS\Messaging($icws);
}
//handle incoming messages from the client
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 === $client) {
// The sender is not the receiver, send to each client connected
$client->send($msg);
}
}
}
//handle close connection
public function onClose(ConnectionInterface $conn) {
// The connection is closed, remove it, as we can no longer send it messages
$this->clients->detach($conn);
echo "Connection {$conn->resourceId} has disconnected\n";
}
//handle errors
public function onError(ConnectionInterface $conn, \Exception $e) {
echo "An error has occurred: {$e->getMessage()}\n";
$conn->close();
}
}

Http calls to php RestApi take 15sec from angular and only 20ms from postman. What am I missing?

Making requests from angular http to local Apache 2.4 server running php restapi with slim framework. the requests take 15+ seconds to come back. however when I use postman to test the api the responses take 20ms which is what I expect. I'm brand new to php, is there some configuration I'm missing?
<?php
require 'vendor/autoload.php';
$app = new Slim\Slim();
// ==============================
// Connection ===================
// ==============================
function connect()
{
$servername = "localhost";
$username = "root";
$password = "******";
try {
$conn = new PDO("mysql:host=$servername;dbname=contacts", $username, $password);
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
//echo "Connected successfully<br>";
} catch (PDOException $e) {
echo "Connection failed: " . $e->getMessage();
}
return $conn;
}
$app->get('/contacts', function () use ($app) {
getContacts(connect(), $app);
});
$app->run();
function getContacts($conn, $app)
{
$app->response()->header("Content-Type", "application/json");
$app->response()->header('Access-Control-Allow-Origin', '*');
$app->response()->header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
$app->response()->header('Access-Control-Allow-Headers', 'X-Requested-With,content-type');
$app->response()->header('Access-Control-Allow-Credentials', true);
try {
$stmt = $conn->prepare("SELECT * FROM contacts");
$stmt->execute();
echo json_encode($stmt->fetchAll());
} catch (PDOException $e) {
echo $e->getMessage();
}
$conn = null;
}
The Request
$http({
method: 'GET',
url : "http://localhost/Contacts_PHP/contactsAPI.php/contacts"
}).success(function (data) {
$scope.contacts = data;
console.log($scope.contacts);
}).error(function (ex) {
console.log(ex);
});
Try closing the connection by sending the following header:
$app->response->headers->set('Connection', 'close');
But first, set your response body by using the setBody() method, instead of using echo:
$app->response->setBody(json_encode($data));

Categories