phpwebsockets yii implementation - php

I am trying to implement websocket using php and as an extension for yii so that i can create a notification like feature for my web application
The code from below link is my starting point:
http://www.flynsarmy.com/2012/02/php-websocket-chat-application-2-0/
Its works perfectly in my local xampp..
I tried converting it as a Yii extension Steps that i followed..
I have put the class PHPWebSocket.php in the yii extension folder..
I created a controller websocket and a action startserver and a action index(for the chat interface to test out the example from the above link)
here is the code snippet
<?php
Yii::import("ext.websocket.PHPWebSocket");
class WebSocketController extends Controller {
public $layout = '//layouts/empty';
public function actionStartServer() {
set_time_limit(0);
function wsOnMessage($clientID, $message, $messageLength, $binary) {
global $Server;
$ip = long2ip($Server->wsClients[$clientID][6]);
// check if message length is 0
if ($messageLength == 0) {
$Server->wsClose($clientID);
return;
}
//The speaker is the only person in the room. Don't let them feel lonely.
if (sizeof($Server->wsClients) == 1)
$Server->wsSend($clientID, "There isn't anyone else in the room, but I'll still listen to you. --Your Trusty Server");
else
//Send the message to everyone but the person who said it
foreach ($Server->wsClients as $id => $client)
if ($id != $clientID)
$Server->wsSend($id, "Visitor $clientID ($ip) said \"$message\"");
}
// when a client connects
function wsOnOpen($clientID) {
global $Server;
$ip = long2ip($Server->wsClients[$clientID][6]);
$Server->log("$ip ($clientID) has connected.");
//Send a join notice to everyone but the person who joined
foreach ($Server->wsClients as $id => $client)
if ($id != $clientID)
$Server->wsSend($id, "Visitor $clientID ($ip) has joined the room.");
}
// when a client closes or lost connection
function wsOnClose($clientID, $status) {
global $Server;
$ip = long2ip($Server->wsClients[$clientID][6]);
$Server->log("$ip ($clientID) has disconnected.");
//Send a user left notice to everyone in the room
foreach ($Server->wsClients as $id => $client)
$Server->wsSend($id, "Visitor $clientID ($ip) has left the room.");
}
$Server = new PHPWebSocket();
$Server->bind('message', 'wsOnMessage');
$Server->bind('open', 'wsOnOpen');
$Server->bind('close', 'wsOnClose');
$Server->wsStartServer('127.0.0.1', 9300);
}
public function actionIndex() {
$this->render('index');
}
}
Is my approach to creating a websocket using php is correct or is it impossible to do so..
I want to implement the same using php only because i am preferred to use node.js or any other scripts

When using PHP with Apache, each request to PHP (usually) creates new process/thread. As web sockets are (somewhat) permanent connections, these PHP requests last for quite some time. Each process takes memory on the server. So, as I think this is possible, your server can probably just crash or reject requests if you would have many (or even not so many) users online at one time.
Node.js approach is different - each connection does not require separate process and so it can process many active connections at once.
You could use Node.js together with PHP connecting those two using queues or some other communication mechanism.

Just in case someone else stumbles upon this.
I was looking for a way to implement real-time-events to a Yii application myself.
In the comments above this (Yii) tutorial on HTML5 SSE is mentioned. As it seems to be very easy, it's just not enough if you need to support older browsers and mobile devices.
Browser support aka does it work in IE? Internet Explorer and Android browser (all versions) don't support Server-Sent Events out of
the box. Neither do older versions of Firefox (< 6), Chrome (< 6),
Safari (< 5), iOS Safari (< 4), or Opera (< 11).
Another solution is a fairly new Yii node socket extension. It's based on the node.js socket.io library and uses elephant.io to communicate with the server through php. Above all, the extension seems (I've only been using it for a month) to be well written. It has both Linux and Windows support, uses CLI to execute commands and it even has it's own database driver provided.
Other solutions are still welcome.

Related

Send and read serial data from a PHP Desktop app using Node.js or PHP

As written in the title I made a stand alone web app using PHP Desktop, and now i want it to send and read data from a serial port.
I tried first to use this PHP Serial class (https://github.com/rubberneck/php-serial), but i had some problems, particularly on reading data, so i moved to javascript and node.js.
I installed Node.js and, using npm, serialport and ws packages, and I made the serialport work only with the node command on Windows cmd. I tried to follow some examples and extend the code to my PHP Desktop App using also the ws package but nothing seems to work and now I don't know how to go on.
Do you guys have any kind of suggestion? (Particularly on how to go on with the ws package)
What's the best solution to do this?
Thanks in advance.
Here's the code that I used:
PHP code:
<?php
session_start();
include_once 'PhpSerial.php';
if(isset($_POST['serial'])) {
$serial = new phpSerial;
$serial->deviceSet("COM3");
$serial->deviceOpen('w+') ;
$serial->confBaudRate(115200);
$serial->sendMessage($_POST['serial'],1);
$_SESSION['serial_data'] = $serial->readPort();
$serial->deviceClose();
}
else
header('Location: index.php');
?>
JS code: (without the ws part, that didn't want to work)
var serialport = require('serialport');
var portName = "COM3";
var myPort = new serialport(portName, {
baudRate: 115200
});
myPort.on('open', showPortOpen);
myPort.on('data', readSerialData);
myPort.on('close', showPortClose);
myPort.on('error', showError);
function showPortOpen() {
console.log('port open. Data rate: ' + myPort.baudRate);
}
function readSerialData(data) {
console.log(data);
}
function showPortClose() {
console.log('port closed.');
}
function showError(error) {
console.log('Serial port error: ' + error);
}

Ratchet PHP WAMP - React / ZeroMQ - Specific user broadcast

Note: This is not the same as this question which utilises MessageComponentInterface. I am using WampServerInterface instead, so this question pertains to that part specifically. I need an answer with code examples and an explanation, as I can see this being helpful to others in the future.
Attempting looped pushes for individual users
I'm using the WAMP part of Ratchet and ZeroMQ, and I currently have a working version of the push integration tutorial.
I'm attempting to perform the following:
The zeromq server is up and running, ready to log subscribers and unsubscribers
A user connects in their browser over the websocket protocol
A loop is started which sends data to the specific user who requested it
When the user disconnects, the loop for that user's data is stopped
I have points (1) and (2) working, however the issue I have is with the third one:
Firstly: How can I send data to each specific user only? Broadcast sends it to everyone, unless maybe the 'topics' end up being individual user IDs maybe?
Secondly: I have a big security issue. If I'm sending which user ID wants to subscribe from the client-side, which it seems like I need to, then the user could just change the variable to another user's ID and their data is returned instead.
Thirdly: I'm having to run a separate php script containing the code for zeromq to start the actual looping. I'm not sure this is the best way to do this and I would rather having this working completely within the codebase as opposed to a separate php file. This is a major area I need sorted.
The following code shows what I currently have.
The server that just runs from console
I literally type php bin/push-server.php to run this. Subscriptions and un-subscriptions are output to this terminal for debugging purposes.
$loop = React\EventLoop\Factory::create();
$pusher = Pusher;
$context = new React\ZMQ\Context($loop);
$pull = $context->getSocket(ZMQ::SOCKET_PULL);
$pull->bind('tcp://127.0.0.1:5555');
$pull->on('message', array($pusher, 'onMessage'));
$webSock = new React\Socket\Server($loop);
$webSock->listen(8080, '0.0.0.0'); // Binding to 0.0.0.0 means remotes can connect
$webServer = new Ratchet\Server\IoServer(
new Ratchet\WebSocket\WsServer(
new Ratchet\Wamp\WampServer(
$pusher
)
),
$webSock
);
$loop->run();
The Pusher that sends out data over websockets
I've omitted the useless stuff and concentrated on the onMessage() and onSubscribe() methods.
public function onSubscribe(ConnectionInterface $conn, $topic)
{
$subject = $topic->getId();
$ip = $conn->remoteAddress;
if (!array_key_exists($subject, $this->subscribedTopics))
{
$this->subscribedTopics[$subject] = $topic;
}
$this->clients[] = $conn->resourceId;
echo sprintf("New Connection: %s" . PHP_EOL, $conn->remoteAddress);
}
public function onMessage($entry) {
$entryData = json_decode($entry, true);
var_dump($entryData);
if (!array_key_exists($entryData['topic'], $this->subscribedTopics)) {
return;
}
$topic = $this->subscribedTopics[$entryData['topic']];
// This sends out everything to multiple users, not what I want!!
// I can't send() to individual connections from here I don't think :S
$topic->broadcast($entryData);
}
The script to start using the above Pusher code in a loop
This is my issue - this is a separate php file that hopefully may be integrated into other code in the future, but currently I'm not sure how to use this properly. Do I grab the user's ID from the session? I still need to send it from client-side...
// Thought sessions might work here but they don't work for subscription
session_start();
$userId = $_SESSION['userId'];
$loop = React\EventLoop\Factory::create();
$context = new ZMQContext();
$socket = $context->getSocket(ZMQ::SOCKET_PUSH, 'my pusher');
$socket->connect("tcp://localhost:5555");
$i = 0;
$loop->addPeriodicTimer(4, function() use ($socket, $loop, $userId, &$i) {
$entryData = array(
'topic' => 'subscriptionTopicHere',
'userId' => $userId
);
$i++;
// So it doesn't go on infinitely if run from browser
if ($i >= 3)
{
$loop->stop();
}
// Send stuff to the queue
$socket->send(json_encode($entryData));
});
Finally, the client-side js to subscribe with
$(document).ready(function() {
var conn = new ab.Session(
'ws://localhost:8080'
, function() {
conn.subscribe('topicHere', function(topic, data) {
console.log(topic);
console.log(data);
});
}
, function() {
console.warn('WebSocket connection closed');
}
, {
'skipSubprotocolCheck': true
}
);
});
Conclusion
The above is working, but I really need to figure out the following:
How can I send individual messages to individual users? When they visit the page that starts the websocket connection in JS, should I also be starting the script that shoves stuff into the queue in PHP (the zeromq)? That's what I'm currently doing manually, and it just feels wrong.
When subscribing a user from JS, it can't be safe to grab the users id from the session and send that from client-side. This could be faked. Please tell me there is an easier way, and if so, how?
Note: My answer here does not include references to ZeroMQ, as I am not using it any more. However, I'm sure you will be able to figure out how to use ZeroMQ with this answer if you need to.
Use JSON
First and foremost, the Websocket RFC and WAMP Spec state that the topic to subscribe to must be a string. I'm cheating a little here, but I'm still adhering to the spec: I'm passing JSON through instead.
{
"topic": "subject here",
"userId": "1",
"token": "dsah9273bui3f92h3r83f82h3"
}
JSON is still a string, but it allows me to pass through more data in place of the "topic", and it's simple for PHP to do a json_decode() on the other end. Of course, you should validate that you actually receive JSON, but that's up to your implementation.
So what am I passing through here, and why?
Topic
The topic is the subject the user is subscribing to. You use this to decide what data you pass back to the user.
UserId
Obviously the ID of the user. You must verify that this user exists and is allowed to subscribe, using the next part:
Token
This should be a one use randomly generated token, generated in your PHP, and passed to a JavaScript variable. When I say "one use", I mean every time you reload the page (and, by extension, on every HTTP request), your JavaScript variable should have a new token in there. This token should be stored in the database against the User's ID.
Then, once a websocket request is made, you match the token and user id to those in the database to make sure the user is indeed who they say they are, and they haven't been messing around with the JS variables.
Note: In your event handler, you can use $conn->remoteAddress to get the IP of the connection, so if someone is trying to connect maliciously, you can block them (log them or something).
Why does this work?
It works because every time a new connection comes through, the unique token ensures that no user will have access to anyone else's subscription data.
The Server
Here's what I am using for running the loop and event handler. I am creating the loop, doing all the decorator style object creation, and passing in my EventHandler (which I'll come to soon) with the loop in there too.
$loop = Factory::create();
new IoServer(
new WsServer(
new WampServer(
new EventHandler($loop) // This is my class. Pass in the loop!
)
),
$webSock
);
$loop->run();
The Event Handler
class EventHandler implements WampServerInterface, MessageComponentInterface
{
/**
* #var \React\EventLoop\LoopInterface
*/
private $loop;
/**
* #var array List of connected clients
*/
private $clients;
/**
* Pass in the react event loop here
*/
public function __construct(LoopInterface $loop)
{
$this->loop = $loop;
}
/**
* A user connects, we store the connection by the unique resource id
*/
public function onOpen(ConnectionInterface $conn)
{
$this->clients[$conn->resourceId]['conn'] = $conn;
}
/**
* A user subscribes. The JSON is in $subscription->getId()
*/
public function onSubscribe(ConnectionInterface $conn, $subscription)
{
// This is the JSON passed in from your JavaScript
// Obviously you need to validate it's JSON and expected data etc...
$data = json_decode(subscription->getId());
// Validate the users id and token together against the db values
// Now, let's subscribe this user only
// 5 = the interval, in seconds
$timer = $this->loop->addPeriodicTimer(5, function() use ($subscription) {
$data = "whatever data you want to broadcast";
return $subscription->broadcast(json_encode($data));
});
// Store the timer against that user's connection resource Id
$this->clients[$conn->resourceId]['timer'] = $timer;
}
public function onClose(ConnectionInterface $conn)
{
// There might be a connection without a timer
// So make sure there is one before trying to cancel it!
if (isset($this->clients[$conn->resourceId]['timer']))
{
if ($this->clients[$conn->resourceId]['timer'] instanceof TimerInterface)
{
$this->loop->cancelTimer($this->clients[$conn->resourceId]['timer']);
}
}
unset($this->clients[$conn->resourceId]);
}
/** Implement all the extra methods the interfaces say that you must use **/
}
That's basically it. The main points here are:
Unique token, userid and connection id provide the unique combination required to ensure that one user can't see another user's data.
Unique token means that if the same user opens another page and requests to subscribe, they'll have their own connection id + token combo so the same user won't have double the subscriptions on the same page (basically, each connection has it's own individual data).
Extension
You should be ensuring all data is validated and not a hack attempt before you do anything with it. Log all connection attempts using something like Monolog, and set up e-mail forwarding if any critical's occur (like the server stops working because someone is being a bastard and attempting to hack your server).
Closing Points
Validate Everything. I can't stress this enough. Your unique token that changes on every request is important.
Remember, if you re-generate the token on every HTTP request, and you make a POST request before attempting to connect via websockets, you'll have to pass back the re-generated token to your JavaScript before trying to connect (otherwise your token will be invalid).
Log everything. Keep a record of everyone that connects, asks for what topic, and disconnects. Monolog is great for this.
To send to specific users, you need a ROUTER-DEALER pattern instead of PUB-SUB. This is explained in the Guide, in chapter 3. Security, if you're using ZMQ v4.0, is handled at the wire level, so you don't see it in the application. It still requires some work, unless you use the CZMQ binding, which provides an authentication framework (zauth).
Basically, to authenticate, you install a handler on inproc://zeromq.zap.01, and respond to requests over that socket. Google ZeroMQ ZAP for the RFC; there is also a test case in the core libzmq/tests/test_security_curve.cpp program.

socket_select() with 2 dimensional array

I'm trying to make my PHP server a bit more efficient.
I've built an object named Client which contains the connected client (which has an open socket connection with the server) information such as name, id etc.
For now I have one array of socket connections, and one array of Client objects.
When I'm referring a connection, I'm searching inside my Client array to find the right client who matches this connection.
It works great, but it's a bit inefficient.. For small amount of clients in the server you don't feel it, but I'm afraid that if I'll have thousands of connection it will slow down the server.
As a solution I thought about 2 dimensional array, but I have a logic problem designing it.
Can I do something like this:
$clients = array();
$temp = array($newsock, new Client());
$clients[] = $temp;
I want my $clients[] to be the socket and the $clients[][] to be the client object.
In each row of $client I will have only $client[$index][0] which will be my client object for that connection.
Will I be able to send this to the socket_select() function?
You say that you have within your client object an id attribute. Why not use that id as the key for both arrays?
Socket connections array
Client object array
You might even be able to hold the connection and the client object in one array, each in one object under the same key I talked about before - the clients id.
In any case, wherever you decide to store your clients connection object, you will be able to pass it to all the relevant socket functions -
socket_select();
socket_accept();
socket_write();
etc...
With regard to the efficiency of your server, I implemented some forking for broadcasting data to large amounts of clients (all of them in the example of a chat server).
This is the implementation that I used for forking the broadcasts -
function broadcastData($socketArray, $data){
global $db;
$pid = pcntl_fork();
if($pid == -1) {
// Something went wrong (handle errors here)
// Log error, email the admin, pull emergency stop, etc...
echo "Could not fork()!!";
} elseif($pid == 0) {
// This part is only executed in the child
foreach($socketArray AS $socket) {
// There's more happening here but the essence is this
socket_write($socket,$msg,strlen($msg));
// TODO : Consider additional forking here for each client.
}
// This is where the signal is fired
exit(0);
}
// The child process is now occupying the same database
// connection as its parent (in my case mysql). We have to
// reinitialize the parent's DB connection in order to continue using it.
$db = dbEngine::factory(_dbEngine);
}
The code above was lifted from a previous question of mine (that was self answered).
Terminating zombie child processes forked from socket server
Perhaps it might assist you if you chose to start forking processes.

google app engine problem of post method in flash and php

I'm trying to deploy flash files embeded in html to the google app engine.
Flash(action script 2.0) uses "post" method to send hostname and get its ip address through php function gethostbyname().
In fact, I know google app engine does not support php.
So I tried to use another way to deploy ipPHP.php in other free web server and only flash file in google app engine.
But it does not work and I can not know why.
Can you give me a tip for this problem ?
--------------domaintoip.fla ---------------------
result_lv = new LoadVars();
result_lv.byname = _root.domainnm;
trace("Sending... " + result_lv.byname);
result_lv.onLoad = function (success)
{
if (success)
{
_root.ip = unescape(this.result);
trace("Return value from the PHP : " + unescape(this));
if(_root.ip.length==5){
_root.flag=1;
}
else{
var mystring=_root.ip;
arr=mystring.split(".");
_root.ipby1=arr[0];
_root.ipby2=arr[1];
_root.ipby3=arr[2];
if(arr[3].length==15)
{
_root.ipby4=arr[3].substr(0,3);
}
if(arr[3].length==14)
{
_root.ipby4=arr[3].substr(0,2);
}
if(arr[3].length==13)
{
_root.ipby4=arr[3].substr(0,1);
}
_root.flag=0;
}
}
else
{
trace("Cannot call the PHP file...");
_root.flag=1;
}
}
result_lv.sendAndLoad("http://anotherserver../ipPHP.php", result_lv, "POST");
-------------- ipPHP.php ---------------------
<?php
$Var1 = $_POST['byname'];
$rtnValue = gethostbyname(trim($Var1));
if(ip2long($rtnValue) == -1 || $rtnValue == $Var1 ) {
$rtnValue =0;
echo (result=$rtnValue");
}
else {
echo("result=$rtnValue");
}
?>
If your site is hosted on the app engine, you cannot make AJAX calls to a host other than the app engine due to the Same Origin Policy. This limitation is generally true, and is not specific to the app engine. To generalize, for any web page hosted at domain X, that web page cannot make AJAX requests to domain Y.
You actually are experiencing a much more fundamental problem: When the only tool you have is a hammer, every problem looks like a nail. In fact, you can trivially handle POST requests with the app engine using the doPost method, and you can very easily get the client's IP address in a very similar manner as your PHP script. There is absolutely no reason to use PHP here; you've set up a completely new server to call one built-in PHP function? That's insane; you can do the exact same thing with an app engine servlet.
Consider the following code:
public void doPost(HttpServletRequest request,HttpServletResponse response) {
/* get "byname" param, equivalent to $POST['byname'] */
String rtnValue = request.getParameter("byname");
/* TODO: your if statements and other logic */
/* print response to client, equivalent to your echo statement */
response.getWriter().print("result=" + rtnValue);
}

Using Cometchat & Rails together - is this possible?

I hope someone can advise / direct / shed some light on :
i have a rails application that uses Authlogic for authentication.
i would like to incorporate cometchat into this application - (in reality any chat IM would do - but cometchat seemed to fit nicely because ajax-im does not like windows)
The environment is the following : The rails app is running on a windows machine - and will be available to the local network (no internet)
So to be able to use cometchat - i am running the WAMP server.
Into the nitty gritty of php(which i dont know well at all)
authlogic keeps my user session for me.
but for cometchat to work i need to pass the getUserID function the current user.
(comet chat assumes that there is a php session variable - but i dont have this with rails)
So how can i pass the rails session user to the getUserID function.
the function looks like this:
*function getUserID() {
$userid = 0;
if (!empty($_SESSION['userid'])) {
$userid = $_SESSION['userid'];
}
return $userid;
}*
the next function has to do with the friends list - but im sure this can be solved with sql inside the php page once i have the current user.
Again - all and any guidance is welcome here. Even if it means an alternate chat solution.
Many thanks in advance.
akza,
You can store the userid(numeric, non-zero, unique preferably the primary key from your users table) of currently logged-in user in a cookie say 'userid_for_cometchat'
and can access it as $_COOKIE['userid_for_cometchat'] in PHP
So your getUserID() should look like:
function getUserID() {
$userid = 0;
if (!empty($_COOKIE['userid_for_cometchat'])) {
$userid = $_COOKIE['userid_for_cometchat'];
}
return $userid;
}
You need to create this cookie on login and make sure to destroy it on logout.
This should work well.
CometChat Support team is real great at doing such stuff and they have performed a lot of custom modifications for me and I'm really satisfied by their job.

Categories