I use workerman/workerman for websockets.
The main question – how to send messages on websocket closing?
Also I tried to send messages to other connections. No result too.
public function run()
{
$this->ws = new Worker(
'websocket://0.0.0.0:' . $_ENV['WS_PORT'],
['ssl' => [
'local_cert' => $_ENV['WS_CERTIFICATE_CRT'],
'local_pk' => $_ENV['WS_CERTIFICATE_KEY'],
'verify_peer' => false,
]]
);
$this->ws->count = 1;
$this->ws->transport = 'ssl';
$this->ws->onConnect = function ($connection) {
$this->onConnect($connection);
};
$this->ws->onMessage = function ($connection, $data) {
$this->onMessage($connection, $data);
};
$this->ws->onClose = function ($connection) {
$this->onClose($connection);
};
Worker::runAll();
}
This doesn't work
private function onClose($connection)
{
$connection->send("Hello");
}
Looking at the source code of workerman/workerman we can see that the onClose event is implemented as follows:
// Close socket.
try {
#fclose($this->socket);
} catch (Throwable) {
}
$this->status = self::STATUS_CLOSED;
// Try to emit onClose callback.
if ($this->onClose) {
try {
($this->onClose)($this);
} catch (Throwable $e) {
$this->error($e);
}
}
As you can see, onClose is emitted AFTER the socket has been closed.
So you will not be able to use that socket in your onClose handler anymore.
A better solution would be that the client sends some sort of "close" packet to the server.
The server can then run some logic and close the connection after it is done.
Related
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();
I create a web socket server from this tuts https://www.sitepoint.com/how-to-quickly-build-a-chat-app-with-ratchet/
now I want to know how can I send a message to the specific connection. these are my code. in these codes send a message to all connection but i want to know which connection send me a message then send a message to that connection.
my client js
(function(){
var user;
var messages = [];
function updateMessages(msg){
messages.push(msg);
}
var conn = new WebSocket('ws://127.0.0.1:4510');
conn.onopen = function(e) {
console.log("Connection established!");
conn.onmessage = function(e) {
var msg = JSON.parse(e.data);
alert(msg);
updateMessages(msg);
};
conn.onclose = function () {
// conn.close();
}; // disable onclose handler first
var i = 0;
$('#start').click(function(){
user = $('#user').val();
var msg = {
"name" : 'start'
};
updateMessages(msg);
conn.send(JSON.stringify(msg));
});
};
})();
and my php server file
<?
protected $clients;
public $i = 0;
public function __construct() {
$this->clients = new \SplObjectStorage;
}
public function onOpen(ConnectionInterface $conn) {
$this->clients->attach($conn);
echo "New connection! ({$conn->resourceId})\n";
}
public function onMessage(ConnectionInterface $from, $msg) {
foreach ($this->clients as $client) {
if ($from !== $client) {
$client->send($rsid);
}
}
}
public function onClose(ConnectionInterface $conn) {
$this->clients->detach($conn);
echo "Connection {$conn->resourceId} has disconnected\n";
}
public function onError(ConnectionInterface $conn, \Exception $e) {
echo "An error has occurred: {$e->getMessage()}\n";
$conn->close();
}
I have the similar issue days ago, and I finally find a good way to handle all the connections in a good way:
Instead of doing
var conn = new WebSocket('ws://127.0.0.1:4510');
I pass the userId as an URL query like that:
var conn = new WebSocket('ws://127.0.0.1:4510/?id=123');
On the server-side, store the userId and incoming connection as a key-value pair in PHP array. When you want to send to specific user, just index that array and you will get the connection.
$query = $conn->httpRequest->getUri()->getQuery();
//get query from URL like ws://127.0.0.1:8080/?id=123456
$query_list = explode("&", $query);
$user_id = trim(substr($query_list[0], 3));
$this->users[$user_id] = $conn;
If you also want to limit one connection per user and notify the old connection if the same user login elsewhere, you can refer to my code here:
https://github.com/tli4/ratchet-practice
I am using ReactPHP for TCP listener component. This component listens for incoming connections and exchanges data with them. $connections array is updated as clients connect/disconnect from listener.
$loop = React\EventLoop\Factory::create();
$connections = [];
$socket = new React\Socket\Server($loop);
$socket->on('connection', function ($conn) use($loop, $db){
global $connections;
$connections[] = $conn;
$conn->on('data', function ($data) use ($conn,$loop, $db) {
global $connections;
// ...
// ...
$conn->on('close', function ($conn) use($loop, $db){
global $connections;
if(($key = array_search($conn, $connections, true)) !== FALSE) {
unset($connections[$key]);
}
});
});
$socket->listen(16555, '127.0.0.1');
$loop->run();
If client is connected via telnet 'close' will be emitted so I can remove closed connection from $connection array.
However, I have problem with some devices that connect to my listener too. If I turn off device 'close' will not be emitted.
I tried to solve problem with periodical timer:
$loop->addPeriodicTimer(10, function () use($db, $loop){
global $connections;
foreach($connections as $c) {
$remoteAddress = $c->getRemoteAddress();
$metaData = #stream_get_meta_data($c->stream);
if(!$metaData) {
if(($key = array_search($c, $connections, true)) !== FALSE) {
unset($connections[$key]);
}
}
}
});
But seems that it is not reliable enough. Function stream_get_meta_data returns valid metadata array even though client is disconnected.
It is some while since this question was asked, but I've found what works for me is to use an SplObjectStorage() to be the connection pool. This is a collection which doesn't (externally) have an index. It works quite well for connections.
https://www.php.net/manual/en/class.splobjectstorage.php
I think the source of your original problem is that you are unsetting an element within a foreach, which does not automatically update the keys, and you can end up with your objects out of sequence.
In order to iterate over a collection (or an array) with code where you may be removing one or more elements while within the loop, it can be safer to use clone.
So where your pool is:
$connections_pool = new SplObjectStorage();
To iterate you would do (per your original request)
$loop->addPeriodicTimer(10, function () use($db, $loop){
global $connections_pool;
foreach(clone($connections_pool) as $c) {
$remoteAddress = $c->getRemoteAddress();
$metaData = #stream_get_meta_data($c->stream);
if(!$metaData) {
$connections_pool->offsetUnset($c);
}
}
});
I've been having issues with reading data from a Python socket server. I've tried several methods and have been searching for a solution for months.
The response I am trying to get from the Python socket server is different every time. It can be 40 characters the first time and over 10k characters the next.
I've tried to use socket_recv() and fgets() and so far fgets works the best for what I need, as socket_recv doesn't get the whole response when fgets does. There's just one problem. It's so much slower compared to socket_recv, and doesn't always get the response.
The problem I am having with fgets is that it takes 2.02 seconds to get the response on a local connection, no matter how big or small it is.
I need it to go down, but I can't for the life of me figure out how to fix it..
Connecting to the Python socket server only takes 22ms, so I don't get why it'd take long at all to get the whole reponse.
Oh and if it helps, the response is a JSON string.
Here's the code I use:
/*
* Recieve
*/
public function recv() {
if (!$this->connected()) {
$this->_errorStr = 'Recieve timeout';
$this->_error = true;
return false;
}
$buf = '';
while ($line = fgets($this->_socket)) {
$buf .= $line;
}
return json_decode($buf);
}
And if you need the whole class:
class Sockets {
/*
* Variables
*/
private $_id,
$_name,
$_ip,
$_port,
$_socket,
$_socketTimeout = 1,
$_triedConnect = false,
$_errorStr = '',
$_error = false;
/*
* Construct class
*/
public function __construct($ip, $port) {
$this->_ip = $ip;
$this->_port = $port;
$this->_socket = false;
}
/*
* Send command
*/
public function command($cmd, $json = true) {
if ($json) {
$cmd = json_encode($cmd);
}
if (!$this->send($cmd)) {
return $this->_errorStr;
}
$r = $this->recv();
if (!$r) {
return $this->_errorStr;
}
return $r;
}
/*
* Connect to server
*/
public function connect() {
$this->_error = false;
if ($this->_triedConnect) {
$this->_errorStr = 'Failed to connect.';
$this->_error = true;
return false;
}
$this->_triedConnect = true;
$this->_errorStr = '';
$errno = 0;
$errstr = '';
$this->_socket = #pfsockopen($this->_ip, $this->_port, $errno, $errstr, $this->_socketTimeout);
if (!$this->_socket) {
$this->_errorStr = sprintf('Can\'t connect to server.. (%errno: %errstr)', $errno, $errstr);
$this->_error = true;
$this->_socket = false;
return false;
}
stream_set_timeout($this->_socket, $this->_socketTimeout);
// Clear stream
while ($this->dataReady()) {
if (!fgets($this->_socket)) {
break;
}
}
if (!$this->connected()) {
$this->_errorStr = 'Lost connection to server!';
$this->_error = true;
$this->_socket = false;
return $this->_errorStr;
}
return true;
}
/*
* Authentication
*/
public function auth() {
}
/*
* Connected
*/
public function connected() {
return $this->_socket !== false;
}
/*
* Data ready
*/
public function dataReady() {
if (!$this->connected()) {
return false;
}
return #stream_select($r = array($this->_socket), $w = null, $x = null, 0) > 0;
}
/*
* Send data
*/
public function send($data) {
if (!$this->connected()) {
$this->_errorStr = 'Not connected!';
$this->_error = true;
return false;
}
if (#fwrite($this->_socket, $data) === false) {
$this->_errorStr = 'Failed to send command!';
$this->_error = true;
return false;
}
return true;
}
/*
* Recieve
*/
public function recv() {
if (!$this->connected()) {
$this->_errorStr = 'Recieve timeout';
$this->_error = true;
return false;
}
$buf = '';
while ($line = fgets($this->_socket)) {
$buf .= $line;
}
return json_decode($buf);
}
/*
* Disconnect
*/
public function disconnect() {
if (!$this->connected()) {
return;
}
fclose($this->_socket);
$this->_socket = false;
$this->_triedConnect = false;
}
}
Any help is really appreciated!
EDIT
The machine I am using is running Windows 8.1 Pro, with Media Center.
I am using Python 2.7.9 for the server with the following dependencies installed:
psutil <- https://pypi.python.org/pypi/psutil
colorama <- https://pypi.python.org/pypi/colorama
pycrypto <- http://www.voidspace.org.uk/python/modules.shtml#pycrypto
It shouldn't matter what type of TCP socket server at all. Just a basic one should work just to test this. Even without the dependencies. Something like this works.
For PHP I am using Wamp and PHP 5.5.12 with the following modules enabled:
php_bz2
php_com_dotnet
php_curl
php_exif
php_fileinfo
php_gd2
php_gettext
php_gmp
php_imap
php_intl
php_ldap
php_mbstring
php_mysql
php_mysqli
php_openssl
php_pdo_mysql
php_pdo_sqlite
php_shmop
php_soap
php_sockets
php_sqlite3
php_xmlrpc
php_xsl
Some, if not all of those, are enabled on default.
To test the Sockets class, all you need is to is something like this:
// Import class file
require_once 'Sockets.php';
$socket = new Sockets('127.0.0.1', 21); // Change the port accordingly
// Connect to socket server
$socket->connect();
// Now in my case, the socket server responds to JSON strings, and nothing else.
// So I am going to show you show I send a command.
$command = array(
'key' => 'encrypted key', // This key is to do some validation on the server-side
'command' => 'command' // This is the command to be issued.
);
// Send command to socket server and dump the response
var_dump($socket->command($command));
// To send a plainstring command use this instead
var_dump($socket->command('command here', false));
After quite a lot of debugging and further searching the interwebs and not finding anything, I finally found an answer to my issue.
The issue was with the Python socket server. After sending a command to the socket server, it sends a response back with the requested data. The socket server should then close the connection. And this is where the issue was. It was sending the response, but not closing the connection, so all I had to do was close the connection after every command. The response time went from 2.02 seconds down to 20ms, which is what I wanted.
I'm looking for a guide on how to use fsockopen() to communicate with a telnet system.... I'm connected just fine, but the command is failing to send. I've seen some documentation fwrite() that shows people sending some headers.
Currently the command I'm running against the telnet server is version via $class->send("version");. Do I need to send headers or anything along with this for the telnet server to pick up the command, or can I just send that?
/**
* Connect to the GMC telnet system
*/
public function connect () {
$this->connection = fsockopen($this->socket['host'], $this->socket['port'], $errorNumber, $errorMessage, 30);
if (!$this->connection) {
$this->error = 'Unable to connect to GMC: '.$errorMessage.' ('.$errorNumber.')';
return false;
}
stream_set_timeout($this->connection, $this->commandTimeout);
return true;
}
/**
* Send a command to GMC
*/
public function send ($command) {
//write to socket
if (fwrite($this->connection, $command) === false) {
$this->error = 'Unable to write to socket';
return false;
}
sleep(1);
//read socket
if (($response = fgets($this->connection)) === false) {
$this->error = 'Unable to write to socket';
return false;
}
return $response;
}
/**
* Disconnects from the GMC telnet system
*/
public function disconnect () {
return fclose($this->connection);
}
Apparently all I needed to do was be sure I included a \n at the end of my command!!