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?
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.
Related
I'm using Ratchet PHP for a chat.
I need to be able to display how many users are online.
So in my Chat.php file:
public function onOpen(ConnectionInterface $conn)
{
$count = $this->clients->count() + 1;
print_r($count);
$conn->countClient = $count;
$this->clients->attach($conn);
echo "New connection! ({$conn->resourceId})\n";
}
Since this function is called every time a new connection is made.. I chose to make my count there but... correct me if I'm wrong ;)
print_r($count);
Give give me 1... even if I have multiple users connected.
But I also struggle with the JS part:
<script>
function connect() {
var conn = new WebSocket('ws://localhost:8080?token={{ user.user_id }}');
// OPEN THE CONNECTION
conn.onopen = function (e) {
console.log("Connection established!", );
};
}
</script>
How can I pass the value count (attach to the conn object in my PHP function) ?
Thanks for any advice or help
Im trying to get SSE working. I have a simple web with two buttons. Each one send a POST request to the server that adds a message to a list.
When the eventsource is listening, the server checks the list once each second and sends all the available messages to the client, at the same time that it marks them as readed so they won't be sent again to that client.
It kind of works but does all sorts of weird stuff:
Sometimes the button POST requests are delayed for no apparent reason and then sent all at once.
Sometimes the EventSource restarts itself making a GET request to the server.
Sometimes the server generates an exception when calling Response.Flush() after spamming the buttons: "The remote host closed the connection. The error code is 0x800704CD"
After pressing the buttons a few times, when I try to reload the page, it stays "loading" forever.
After starting the EventSource in javascript it generates a GET request that stays open and after that, any POST request that the buttons send is never sent until the event source GET request ends. Once the EventSource connection ends, all POST requests from the buttons are sent.
I know a lot of things from this code can be improved but I simplified it a lot by leaving just the essential for it to "work".
Also note that:
NotificationSystem.GetNotifications() gets all messages available for the user in a thread safe way.
NotificationSystem.AddNotification() adds the messages.
So here is the server code:
public void GetNotifs() {
try {
Response.ContentType = "text/event-stream";
while(true) {
List<string> Notifs = NotificationSystem.GetNotifications( GetUserId() );
if(Notifs != null && Notifs.Count > 0) {
foreach(var Text in Notifs) {
Response.Write( string.Format( "data: {0}\n\n", Text ) );
}
Response.Flush();
}
Thread.Sleep( 1000 );
}
} catch(Exception ex) {
Response.Close();
}
}
public ActionResult AddButton1() {
NotificationSystem.AddNotification( GetUserId(), "BTN1 (" + GetUserId() + ")" );
return Json( "OK" );
}
public ActionResult AddButton2() {
NotificationSystem.AddNotification( GetUserId(), "BTN2 (" + GetUserId() + ")" );
return Json( "OK" );
}
And the client JS code:
var urlMessages = "/Notifs/GetNotifs";
var urlButton1 = "/Notifs/AddButton1";
var urlButton2 = "/Notifs/AddButton2";
function PushBtn1() {
$.post(urlButton1);
}
function PushBtn2() {
$.post(urlButton2);
}
var myEventSource;
$(document).ready(function () {
myEventSource = new EventSource(urlMessages);
myEventSource.onmessage = function (e) {
console.log(e.data);
$('#EventLog').append("<li>Received: " + e.data + "<li>");
}
});
I need a help with a singleton class. I`m creating a wordpress plugin, and need to have live notifications from server. For that I used AJAX long polling and my code looks like this.
This is a php code used for serving AJAX request and for LOG class which is singleton and called from many different places in project
if (isset($_GET['log']) && $_GET['log'] == 'true')
{
$response = array();
$response['msg'] = SI_log::get_instance()->get_message();
$response['type'] = 'something';
echo json_encode($response);
}
class SI_log{
private $log_messages = array();
private static $instance = NULL;
private $log_file;
public static function get_instance()
{
if (static::$instance === NULL) {
static::$instance = new static();
}
return static::$instance;
}
public function add_message( $message, $type )
{
array_push($this -> log_messages, $message);
}
public function get_message()
{
return end($this->log_messages);
}}?>
This is javascript for retrieving notifications and its a part of admin section in the wordpress.
<script type="text/javascript" charset="utf-8">
function waitForMsg(){
setTimeout(waitForMsg,5000);
document.getElementById("alerts").childNodes = new Array();
var request = new XMLHttpRequest();
request.open('GET', '<?php echo get_site_url() . '/wp-content/plugins/si/admin/c-si-log.php?log=true'?>', true);
request.onload = function() {
if (request.status >= 200 && request.status < 400) {
var resp = request.responseText;
alert(resp);
var json = eval('('+resp+ ')');
document.getElementById("alerts").innerHTML= json['type'] +"<hr>";
if (json['type'] == 'WARNING'){
var html_element = '<div class="alert-message warning" id = "alert_warning"><div class="box-icon"></div><p>'+json['msg']+'</p></div>';
}
if (json['type'] == 'INFO'){
var html_element = '<div class="alert-message info" id = "alert_info"><div class="box-icon"></div><p>'+json['msg']+'</p></div>';
}
if (json['type'] == 'ERROR'){
var html_element = '<div class="alert-message errorr" id = "alert_error"><div class="box-icon"></div><p>'+json['msg']+'</p></div>';
}
document.getElementById("alerts") . innerHTML= html_element;
}else{
alert('<?php echo get_site_url() . '/wp-content/plugins/si/admin/c-si-log.php?log=true' ?>');
}
};
request.onerror = function() {
// There was a connection error of some sort
alert("request isnt good");
};
request.send();
}
window.onload = function (){
if (document.readyState != 'loading'){
waitForMsg();
} else {
document.addEventListener('DOMContentLoaded', waitForMsg);
}
}
</script>
This is how is singleton class called from another class for notification input
SI_log::get_instance()->add_message("action triggered", 'INFO');
I assume the problem is singleton pattern implementation in SI_log class, so there is not only one instance of that class but many more, and when i try to retrieve the notification ie. when I trigger some action, notification isn`t stored in the same object. I used alert(resp); in cilent page to display response and response looks like this
{
"msg":false,
"type":"something"
}
and in log.php you can see that the type value is fine, so it's not communication problem. Can anyone help me please?
NOTE: I must use Javascript because versioning problems so don't ask me why i didn't use JQuery
The singleton pattern is useful when we need to make sure we only have a single instance of a class for the entire request lifecycle in a web application.
So, you can't do the thing you want to achieve in this way.
Instead, use it as a base/parent class and extend it on other classes when you need it.
Following scenario/my solution consists of the following:
Project one: (SELF HOST) I have a SignalR console application which handles the logic including the authentication process ( queries database with EF ). Project two: (CLIENT) I have an ASP.Net web application with an AngularJS client.
So far I can talk to the hub just fine. The problem is, I cannot seem to get the authentication to work. I've tried a bunch of things I've found but none of them worked. Most of them didn't even apply to my problem..
Currently I've stripped my project back to the basics and I have the following code:
Startup class:
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseCors(CorsOptions.AllowAll);
app.MapSignalR();
}
}
My hub:
[HubName("systemHub")]
public class systemHub : Hub
{
public void Authenticate(String pLogin, String pPassword)
{
User curUser = new AuthManager().Authenticate(pLogin, pPassword);
//this is where I'd want to send the auth cookie or whatever and contact the "loginCallback" function in my client
}
[Authorize]
public void Hello(String pMessage)
{
Clients.All.callbackFunc(pMessage);
}
}
Js client:
hinagApp.controller('hinagController', function ($scope) {
$(document).ready(function () {
var conURL = 'http://localhost:8080/signalr';
$.getScript(conURL + '/hubs', function () {
$.connection.hub.url = conURL;
var lHub = $.connection.systemHub;
lHub.client.callbackFunc = function(pM){
alert(pM);
}
lHub.client.loginCallback = function (pSuccess) {
if (pSuccess) {
//if logged in
lHub.server.hello("test");
}
else {
alert("fail");
}
}
$('#loginbutton').click(function () {
lHub.server.authenticate($('#input_login').val(), $('#input_pass').val());
});
$.connection.hub.start();
});
})
});
I recently ran into a similar problem. If I understand you right, you want to do the authentication on your signalr server application. Signalr can accept standard webrequests just fine.
Set the authenticationtype to cookies:
CookieAuthenticationOptions lOptions = new CookieAuthenticationOptions()
{
AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
LoginPath = new PathString("/Auth/Login"),
LogoutPath = new PathString("/Auth/Logout"),
};
app.UseCookieAuthentication(lOptions);
If user wants to login, set the claims you'd like to use
var lForm = await context.Request.ReadFormAsync();
if (!String.IsNullOrEmpty(lForm["input_login"]) && !String.IsNullOrEmpty(lForm["input_pass"]))
{
//Benutzer authentifizieren
var lAuthenticatedUser = new UserManager().Authenticate(lForm["input_login"], lForm["input_pass"]);
if (lAuthenticatedUser != null)
{
//Existiert der Nutzer legen wir die Claims an
ClaimsIdentity lIdentity = new ClaimsIdentity(lOptions.AuthenticationType);
lIdentity.AddClaim(new Claim(ClaimTypes.Name, lAuthenticatedUser.Username));
lIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, lAuthenticatedUser.InternalUserId.ToString()));
lIdentity.AddClaim(new Claim(ClaimTypes.SerialNumber, context.Request.RemoteIpAddress));
//Und zum Schluss einloggen
context.Authentication.SignIn(lIdentity);
//Und auf die Spieleseite weiterleiten
context.Response.Redirect(BLL._Configuration.HinagGameURL);
}
}
If you want to serve the login page you can do it like this (_Authpage is your page as String, for example)
else if (context.Request.Path.Value == "/Auth/")
{
if (context.Authentication.User != null)
context.Response.Redirect(BLL._Configuration.HinagGameURL);
context.Response.ContentType = "text/html";
await context.Response.WriteAsync(_Authpage);
}
If the user needs anything else ( such as additional style files in your authpage )
else
{
await next();
}
All of this belongs in your Startup.
In Startup.cs you need to add forms authentication middleware (probably you need to tune it a bit):
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie
});
https://msdn.microsoft.com/en-us/library/microsoft.owin.security.cookies.cookieauthenticationoptions(v=vs.113).aspx
You kind screwed up Angular with that code. Try this one:
hinagApp
.controller('hinagController', function ($scope, $http) {
var conURL = 'http://localhost:8080/signalr';
var lHub = $.connection.systemHub;
lHub.client.callbackFunc = function(pM){
alert(pM);
}
lHub.client.loginCallback = function (pSuccess) {
if (pSuccess) {
//if logged in
lHub.server.hello("test");
}
else {
alert("fail");
}
}
$http
.get(conURL + '/hubs')
.then(function(response) {
$.connection.hub.url = conURL;
$('#loginbutton').click(function () {
lHub.server.authenticate($('#input_login').val(), $('#input_pass').val());
});
$.connection.hub.start();
});
});
Hello i want to send message to all connected client but my code dont work (for one user its good)
require_once('websockets.php');
$userConnected = array();
class echoServer extends WebSocketServer {
protected function process ($user, $exec) {
foreach($userConnected as $u) {
$this->send($u,'test');
}
}
protected function connected ($user) {
array_push($userConnected, $user);
}
protected function closed ($user) {
if(($key = array_search($user, $userConnected)) !== false) {
unset($userConnected[$key]);
}
}
}