I need to get the connection ID of a client. I know you can get it from the client side using $.connection.hub.id. What I need is to get in while in a web service I have which updates records in a database, in turn displaying the update on a web page. I am new to signalR and stackoverflow, so any advice would be appreciated. On my client web page I have this:
<script type="text/javascript">
$(function () {
// Declare a proxy to reference the hub.
var notify = $.connection.notificationHub;
// Create a function that the hub can call to broadcast messages.
notify.client.broadcastMessage = function (message) {
var encodedMsg = $('<div />').text(message).html();// Html encode display message.
$('#notificationMessageDisplay').append(encodedMsg);// Add the message to the page.
};//end broadcastMessage
// Start the connection.
$.connection.hub.start().done(function () {
$('#btnUpdate').click(function () {
//call showNotification method on hub
notify.server.showNotification($.connection.hub.id, "TEST status");
});
});
});//End Main function
</script>
everything works up until I want to update the page using signalR. The show notification function in my hub is this:
//hub function
public void showNotification(string connectionId, string newStatus){
IHubContext context = GlobalHost.ConnectionManager.GetHubContext<notificationHub>();
string connection = "Your connection ID is : " + connectionId;//display for testing
string statusUpdate = "The current status of your request is: " + newStatus;//to be displayed
//for testing, you can display the connectionId in the broadcast message
context.Clients.Client(connectionId).broadcastMessage(connection + " " + statusUpdate);
}//end show notification
how can I send the connectionid to my web service?
Hopefully I'm not trying to do something impossible.
When a client invokes a function on the server side you can retrieve their connection ID via Context.ConnectionId. Now, if you'd like to access that connection Id via a mechanism outside of a hub, you could:
Just have the Hub invoke your external method passing in the connection id.
Manage a list of connected clients aka like public static ConcurrentDictionary<string, MyUserType>... by adding to the dictionary in OnConnected and removing from it in OnDisconnected. Once you have your list of users you can then query it via your external mechanism.
Ex 1:
public class MyHub : Hub
{
public void AHubMethod(string message)
{
MyExternalSingleton.InvokeAMethod(Context.ConnectionId); // Send the current clients connection id to your external service
}
}
Ex 2:
public class MyHub : Hub
{
public static ConcurrentDictionary<string, MyUserType> MyUsers = new ConcurrentDictionary<string, MyUserType>();
public override Task OnConnected()
{
MyUsers.TryAdd(Context.ConnectionId, new MyUserType() { ConnectionId = Context.ConnectionId });
return base.OnConnected();
}
public override Task OnDisconnected(bool stopCalled)
{
MyUserType garbage;
MyUsers.TryRemove(Context.ConnectionId, out garbage);
return base.OnDisconnected(stopCalled);
}
public void PushData(){
//Values is copy-on-read but Clients.Clients expects IList, hence ToList()
Clients.Clients(MyUsers.Keys.ToList()).ClientBoundEvent(data);
}
}
public class MyUserType
{
public string ConnectionId { get; set; }
// Can have whatever you want here
}
// Your external procedure then has access to all users via MyHub.MyUsers
Hope this helps!
Taylor's answer works, however, it doesn't take into consideration a situation where a user has multiple web browser tabs opened and therefore has multiple different connection IDs.
To fix that, I created a Concurrent Dictionary where the dictionary key is a user name and the value for each key is a List of current connections for that given user.
public static ConcurrentDictionary<string, List<string>> ConnectedUsers = new ConcurrentDictionary<string, List<string>>();
On Connected - Adding a connection to the global cache dictionary:
public override Task OnConnected()
{
Trace.TraceInformation("MapHub started. ID: {0}", Context.ConnectionId);
var userName = "testUserName1"; // or get it from Context.User.Identity.Name;
// Try to get a List of existing user connections from the cache
List<string> existingUserConnectionIds;
ConnectedUsers.TryGetValue(userName, out existingUserConnectionIds);
// happens on the very first connection from the user
if(existingUserConnectionIds == null)
{
existingUserConnectionIds = new List<string>();
}
// First add to a List of existing user connections (i.e. multiple web browser tabs)
existingUserConnectionIds.Add(Context.ConnectionId);
// Add to the global dictionary of connected users
ConnectedUsers.TryAdd(userName, existingUserConnectionIds);
return base.OnConnected();
}
On disconnecting (closing the tab) - Removing a connection from the global cache dictionary:
public override Task OnDisconnected(bool stopCalled)
{
var userName = Context.User.Identity.Name;
List<string> existingUserConnectionIds;
ConnectedUsers.TryGetValue(userName, out existingUserConnectionIds);
// remove the connection id from the List
existingUserConnectionIds.Remove(Context.ConnectionId);
// If there are no connection ids in the List, delete the user from the global cache (ConnectedUsers).
if(existingUserConnectionIds.Count == 0)
{
// if there are no connections for the user,
// just delete the userName key from the ConnectedUsers concurent dictionary
List<string> garbage; // to be collected by the Garbage Collector
ConnectedUsers.TryRemove(userName, out garbage);
}
return base.OnDisconnected(stopCalled);
}
I beg to differ on the reconnect. The client remains in the list but the connectid will change. I do an update to the static list on reconnects to resolve this.
As Matthew C is not completely thread safe in situation of one user request multiple connection at same time, I used this code:
public static Dictionary<string, List<string>> ConnectedUsers = new ();
public override Task OnConnected()
{
var connectionId = Context.ConnectionId;
var userId = Context.User.Identity.Name; // any desired user id
lock(ConnectedUsers)
{
if (!ConnectedUsers.ContainsKey(userId))
ConnectedUsers[userId] = new();
ConnectedUsers[userId].Add(connectionId);
}
}
public override Task OnDisconnected(bool stopCalled)
{
var connectionId = Context.ConnectionId;
var userId = Context.User.Identity.Name; // any desired user id
lock (ConnectedUsers)
{
if (ConnectedUsers.ContainsKey(userId))
{
ConnectedUsers[userId].Remove(connectionId);
if (ConnectedUsers[userId].Count == 0)
ConnectedUsers.Remove(userId);
}
}
}
Related
I want to allow my users connect to a specific group in my SignalR hub, to do this i generate a unique id that the users in that group can share with others. Once a user connects to the hub, the id is generated. On the "connected" event their URL updates with the unique id. When I then use the URL to join the newly created room it seems like two negotiation requests are sent, both containing the the group id as well as the users connection id, yet sometimes(not always) I get a response from the Hub containing a newly generated Group.
Is the ?pod parameter I pass into the url not always assigned before the request is made?
To me it seems completely random, but it's most likely some error I've made in my connection code since I'm relatively new to Angular.
This request happened correctly and I joined the room I wanted.
Correct behavior
This one happened incorrectly and a new room was generated even though, seemingly(?), the request looks the same, save for the web socket connection containing the "pid".
Incorrect behavior
Any help is greatly appreciated!
The code for the Home component where the connection is initiated
export class HomeComponent implements OnInit, OnDestroy{
welcomeMessage:string;
podId:string;
users:User[];
constructor(private signalrService: SignalRService, private http: HttpClient, private activeRoute: ActivatedRoute,
private router: Router, private location: Location) {}
ngOnInit() {
this.activeRoute.queryParams.subscribe(params => {
this.podId = params['pod'];
this.connectToPod();
});
}
ngOnDestroy(): void {
}
connectToPod(){
this.signalrService.startConnection(this.podId);
this.signalrService.addPodConnectedLisener((data: Pod) => {
this.podId = data.id;
this.users = data.users;
this.location.replaceState('/?pod=' + this.podId)
this.welcomeMessage = window.location.origin + '/?pod=' + this.podId;
});
}
}
The code for the SignalR service
export class SignalRService {
private hubConnection: signalR.HubConnection;
private baseUrl = environment.apiUrl;
constructor() { }
public startConnection (podId?: string) {
let idToSend = podId == undefined ? '' : '?pid=' + podId;
this.hubConnection = new signalR.HubConnectionBuilder()
.withUrl(this.baseUrl + '/pod' + idToSend)
.build();
this.hubConnection
.start()
.then(() => console.log('Connection started'))
.catch(err => console.log('Error while starting connection: ' + err));
}
public addPodConnectedLisener (connectedCallback: Function) {
return this.hubConnection.on('connected', data => {
connectedCallback(data);
});
}
}
The code for the SignalR Hub
public class PodHub : Hub
{
private readonly IPodConnectionManger _podConnectionManager;
public PodHub(IPodConnectionManger podConnectionManager)
{
_podConnectionManager = podConnectionManager;
}
public override async Task OnConnectedAsync()
{
var podId = Context.GetHttpContext().Request.Query["pid"].ToString();
if (string.IsNullOrEmpty(podId))
{
await CreatePod();
}
else
{
await JoinPod(podId);
}
}
private async Task CreatePod()
{
var newPodId = await _podConnectionManager.AddPod();
var podToSend = await _podConnectionManager.GetPod(newPodId);
await podToSend.AddUser(Context.ConnectionId);
await Groups.AddToGroupAsync(Context.ConnectionId, podToSend.Id);
await Clients.Group(podToSend.Id).SendAsync("connected", podToSend);
}
private async Task JoinPod(string id)
{
var podToJoin = await _podConnectionManager.GetPod(id);
await podToJoin.AddUser(Context.ConnectionId);
await Groups.AddToGroupAsync(Context.ConnectionId, podToJoin.Id);
await Clients.Group(podToJoin.Id).SendAsync("connected", podToJoin);
}
}
I am following this Signal R tutorial from Microsoft. I have already configured everything for SignalR to be working and it does work.
This is my Front End script to call SignalR
<script>
$(function () {
// Reference the auto-generated proxy for the hub.
var chat = $.connection.chatHub;
// Create a function that the hub can call back to display messages.
chat.client.addNewMessageToPage = function (name, message) {
// Add the message to the page.
$('#discussion').append('<li><strong>' + htmlEncode(name)
+ '</strong>: ' + htmlEncode(message) + '</li>');
};
// Get the user name and store it to prepend to messages.
$('#displayname').val(prompt('Enter your name:', ''));
// Set initial focus to message input box.
$('#message').focus();
// Start the connection.
$.connection.hub.start().done(function () {
$('#sendmessage').click(function () {
// Call the Send method on the hub.
chat.server.send($('#displayname').val(), $('#message').val());
// Clear text box and reset focus for next comment.
$('#message').val('').focus();
});
});
});
// This optional function html-encodes messages for display in the page.
function htmlEncode(value) {
var encodedValue = $('<div />').text(value).html();
return encodedValue;
}
</script>
As you can see in the microsoft tutorial, I require to create a hub class with this method.
public void Send(string name, string message)
{
// Call the addNewMessageToPage method to update clients.
Clients.All.addNewMessageToPage(name, message);
}
My question is, given an mvc application with login (each user have its own Id), how can I send the receiver Id to my backend so I can save it into my database?
I have already tried to modify the Send function
public class chat
{
public int Id { get; set; }
public string Message { get; set; }
public string Name { get; set; }
public Guid receiverId { get; set; }
}
public void Send(string name, string message,string receiverId)
{
// Call the addNewMessageToPage method to update clients.
chatultimo chat = new chatultimo();
chat.Name = name;
chat.Message = message;
chat.Id = Guid.NewGuid();
_dbContext.chatultimo.Add(chat);
_dbContext.SaveChanges();
Clients.All.addNewMessageToPage(name, message);
}
I also made a modification in my function in the front end
chat.client.addNewMessageToPage = function (name, message)
you can see that the Send function is being called in the front end, but after modifying the send function my application is not hiting the backend.
chat.client.addNewMessageToPage = function (name, message,receiverId)
Another thing is I do not think it is a good idea to modify the send function. I think it is much better to do the database job in the $.connection.hub.start().done(function () ...
to make sure it is only executed after the handshake.
So I have the following architecture: an Angular SPA (single page application) performs a call to a .NET Web API controller, which publishers a message to a Publisher EasyNetQ window service, which sends an asynchronous request to a second EasyNetQ window service called Subscriber, which calls a backend class to generate a SSRS report, and finally sends an asynchronous response back to Publisher. Here is a diagram of the architecture in question:
So far so good, Subscriber receives the response, generates the report(s), and sends a message(s) back to Publisher. Here is how the Web API controller sends the report data message to the Publisher:
private IHttpActionResult generateReports(int[] incidentIds)
{
try
{
var incident = this.incidentRepository.GetIncident(incidentIds[0]);
var client = this.clientService.GetClient(incident.ClientId_Fk);
using (var messageBus = RabbitHutch.CreateBus("host=localhost"))
{
// Loop through all incidents
foreach (var incidentId in incidentIds)
{
foreach (var format in this.formats)
{
Dictionary<Dictionary<int, Client>, SSRSReportFormat> reportData = new Dictionary
<Dictionary<int, Client>, SSRSReportFormat>()
{
{new Dictionary<int, Client>() {{incidentId, client}}, format}
};
messageBus.Publish(new ReportData
{
clientId = client.Id,
incidentId = incidentId,
clientName = client.Name,
clientNetworkPath = client.NetworkPath,
formatDescription = EnumUtils.GetDescription(format),
reportFormat = format.ToString()
});
}
}
}
return this.Ok();
}
catch (Exception ex)
{
return this.InternalServerError(ex);
}
}
This is how I send a request from Publisher:
public partial class CreateRequestService : ServiceBase
{
private IBus bus = null;
public CreateRequestService()
{
this.InitializeComponent();
}
protected override void OnStart(string[] args)
{
this.bus = RabbitHutch.CreateBus("host=localhost");
this.bus.Subscribe<ReportData>("reportHandling", this.HandleReportData);
}
protected override void OnStop()
{
this.bus.Dispose();
}
private void HandleReportData(ReportData reportData)
{
int clientId = reportData.clientId;
int incidentId = reportData.incidentId;
string clientName = reportData.clientName;
string clientNetworkPath = reportData.clientNetworkPath;
string formatDescription = reportData.formatDescription;
string reportFormat = reportData.reportFormat;
var task = this.bus.RequestAsync<ReportData, TestResponse>(reportData);
task.ContinueWith(response => Library.WriteErrorLog("Got response: '{0}'" + response.Result.Response, "PublisherLogFile"));
}
}
And finally, the code for generating reports and sending responses back from Subscriber:
public partial class RequestResponderService : ServiceBase
{
private IBus bus = null;
public RequestResponderService()
{
this.InitializeComponent();
}
/// <summary>
/// Initialize the Bus to receive and respond to messages through
/// </summary>
/// <param name="args"></param>
protected override void OnStart(string[] args)
{
// Create a group of worker objects
var workers = new BlockingCollection<MyWorker>();
for (int i = 0; i < 10; i++)
{
workers.Add(new MyWorker());
}
workers.CompleteAdding();
// Initialize the bus
this.bus = RabbitHutch.CreateBus("host=localhost");
// Respond to the request asynchronously
this.bus.RespondAsync<ReportData, TestResponse>(request =>
(Task<TestResponse>) Task.Factory.StartNew(() =>
{
var worker = workers.Take();
try
{
return worker.Execute(request);
}
catch (Exception)
{
throw;
}
finally
{
}
}));
}
protected override void OnStop()
{
this.bus.Dispose();
}
}
class MyWorker
{
public TestResponse Execute(ReportData request)
{
int clientId = request.clientId;
int incidentId = request.incidentId;
string clientName = request.clientName;
string clientNetworkPath = request.clientNetworkPath;
string formatDescription = request.formatDescription;
string reportFormat = request.reportFormat;
ReportQuery reportQuery = new ReportQuery();
reportQuery.Get(incidentId, reportFormat, formatDescription, clientName, clientNetworkPath, clientId);
return new TestResponse { Response = " ***** Report generated for client: " + clientName + ", incident Id: " + incidentId + ", and format: " + reportFormat + " ***** " };
}
}
While this all works, I also need some way to notify the Angular SPA that a report has been generated so I can give the user an appropriate feedback. This is where I am a bit lost though. Can EasyNetQ interact with Angular code? Also, once I receive a response in Publisher, i can probably call some method in my Web API controller, but still the problem of alerting the Angular code remains. Any ideas?
First note that you have to store information about report status somewhere. You can store it in two places:
Persistent storage (database, redis cache, whatever).
In memory of web api service (because it's this service which client is communicating to).
When you decided where to store - there are again two options of how to pass this information to a client:
Client (Angular) can poll from time to time (note that it is not what is called "long polling"). If you store your status in database - you can just look it up there in this case.
There is a persistent connection between Angular and your api (web sockets, long polling also falls here). In this case you better store your status in memory of web api (by passing rabbit message with report status from your service to web api, which then stores that in memory and\or directly forwards that to Angular via persistent connection).
If you don't expect clients on different platforms (iOS, pure linux etc) - SignlarR can work fine. It will fallback from websockets to long polling to regular polling depending on user browser's capabilities.
Isnt the OnDisconnect method supposed to wait a default 30s before being fired? For me it fires instantly on page refresh(F5).
I have a User object which keeps track of a users connections in a hashset.
In my hub I have a dictionary to keep track of connected users.
OnConnected: I add that user to the dictionary, if the user is already there, I just add another connectionid to the users hashset.
OnDisconnected: I remove that connectionId from the calling users hashset, and if he doesnt have any connections left I remove the user object from the dictionary.
I need to keep track of the user object, and I lose it on every page refresh(F5) cause OnDisconnected gets fired straight away and removes the users only connection and the object. And when the page loads again, a new user object gets created, cause the old one was removed straight away.
My Implementation looks something like this
private static readonly ConcurrentDictionary<string, User> Users
= new ConcurrentDictionary<string, User>();
public override Task OnConnected() {
string userName = Context.User.Identity.Name;
string connectionId = Context.ConnectionId;
var user = Users.GetOrAdd(userName, _ => new User {
Name = userName,
ConnectionIds = new HashSet<string>()
});
lock (user.ConnectionIds) {
user.ConnectionIds.Add(connectionId);
// TODO: Broadcast the connected user
}
return base.OnConnected();
}
public override Task OnDisconnected() {
string userName = Context.User.Identity.Name;
string connectionId = Context.ConnectionId;
User user;
Users.TryGetValue(userName, out user);
if (user != null) {
lock (user.ConnectionIds) {
user.ConnectionIds.RemoveWhere(cid => cid.Equals(connectionId));
if (!user.ConnectionIds.Any()) {
User removedUser;
Users.TryRemove(userName, out removedUser);
// You might want to only broadcast this info if this
// is the last connection of the user and the user actual is
// now disconnected from all connections.
Clients.Others.userDisconnected(userName);
}
}
}
return base.OnDisconnected();
}
So I solved this by running a task inside the OnDisconnected method and delaying the method by x Seconds then checking if the user has reconnected, if he hasn't remove him from the list.
public override Task OnDisconnected(bool stopCalled)
{
Task mytask = Task.Run(() =>
{
UserDisconnected(Context.User.Identity.Name, Context.ConnectionId);
});
return base.OnDisconnected(stopCalled);
}
private async void UserDisconnected(string un, string cId)
{
await Task.Delay(10000);
string userName = un;
string connectionId = cId;
User user;
enqueuedDictionary.TryGetValue(userName, out user);
if (user != null)
{
lock (user.ConnectionIds)
{
user.ConnectionIds.RemoveWhere(cid => cid.Equals(connectionId));
if (!user.ConnectionIds.Any())
{
User removedUser;
enqueuedDictionary.TryRemove(userName, out removedUser);
ChatSession removedChatSession;
groupChatSessions.TryRemove(userName, out removedChatSession);
UpdateQ(removedUser.QPos);
}
}
}
}
My connection does not start.
This code worked in 1.x but in version 2 is not working.
SignalR seems to be trying to connect but without success.
The hub method is never called.
Attached sending an image with SignalR debug.
Javascript:
<script type="text/javascript">
$.connection.hub.logging = true;
var options = { transport: ['webSockets', 'longPolling'] };
$(function() {
var userHub = $.connection.userHub;
//Iniciar connecção
window.hubReady = $.connection.hub.start(options);
window.hubReady.done(function () {
userHub.server.ini();
});
userHub.client.iniDone = function (connectionId) {
console.log(connectionId);
};
$.connection.hub.connectionSlow(function() {
console.log('slow connection...');
});
window.hubReady.fail(function(error) {
console.log(error);
});
$.connection.hub.disconnected(function() {
setTimeout(function() {
$.connection.hub.start();
}, 2000);
});
});
</script>
Hub:
[HubName("userHub")]
public class UserHub : Hub
{
public void Ini()
{
Clients.Client(Context.ConnectionId).iniDone(string.Format("Conectado com o id: {0}", Context.ConnectionId));
}
public override Task OnConnected()
{
var connectionId = Context.ConnectionId;
var email = string.IsNullOrWhiteSpace(Context.User.Identity.Name) ? Context.Headers["email"] : Context.User.Identity.Name;
if (email != null && connectionId != null)
UserData.GetInstance(email).ConnectionsIds.Add(connectionId);
return base.OnConnected();
}
public override Task OnDisconnected()
{
var connectionId = Context.ConnectionId;
var email = string.IsNullOrWhiteSpace(Context.User.Identity.Name) ? Context.Headers["email"] : Context.User.Identity.Name;
if (email != null && connectionId != null)
UserData.GetInstance(email).ConnectionsIds.Remove(connectionId);
return base.OnDisconnected();
}
}
Debug:
SignalR Debug Image
EDIT:
I found the problem! The GetInstance method of my Singleton has problems.
public static UserData GetInstance(string username)
{
if (_sharedUsers == null)
lock (_lockCreate)
_sharedUsers = new Dictionary<string, UserData>();
if (!_sharedUsers.ContainsKey(username))
lock (_lockAdd)
_sharedUsers.Add(username, new UserData(username));
return _sharedUsers[username];
}
the method stops always here: lock (_lockAdd)
I want to save all user connectionsIds Any ideas?
Thanks
Try moving the client method subscription to be before you connect. If it's not registered by the time the connection is started, then it will not be callable from the server.
So change it to the following:
$(function() {
var userHub = $.connection.userHub;
//Register Client handlers first
userHub.client.iniDone = function (connectionId) {
console.log(connectionId);
};
//Now you can connect.
window.hubReady = $.connection.hub.start(options);
window.hubReady.done(function () {
userHub.server.ini();
});
$.connection.hub.connectionSlow(function() {
console.log('slow connection...');
});
window.hubReady.fail(function(error) {
console.log(error);
});
$.connection.hub.disconnected(function() {
setTimeout(function() {
$.connection.hub.start();
}, 2000);
});
});
Edit
Based on your comment around a server error in the OnConnected method, it seems like you may have a two problems then. Isolate the connection tracking part out (just comment it out) to get the full round-trip going between client and server. Then add back the connection tracking which is possibly a DB connection error - check the server logs.
Edit
In terms of storing the user connections, you've a few options.
Use ConcurrentDictionary:
One of the simplest is storing in a static ConcurrentDictionary, similar to what you have. Try to avoid the use of so many locks - using a ConcurrentDictionary means you'll actually end up with none.
e.g.
public class UserData
{
public UserData(string username)
{
UserName = username;
ConnectionIds = new HashSet<string>();
}
public string UserName { get; private set; }
public HashSet<string> ConnectionIds { get; private set; }
}
public static class ConnectionStore
{
private static readonly ConcurrentDictionary<string, UserData> _userData = new ConcurrentDictionary<string, UserData>();
public static void Join(string username, string connectionId)
{
_userData.AddOrUpdate(username,
u => new UserData(u), /* Lambda to call when it's an Add */
(u, ud) => { /* Lambda to call when it's an Update */
ud.ConnectionIds.Add(connectionId);
return ud;
});
}
}
See MSDN for more info: http://msdn.microsoft.com/en-us/library/ee378675(v=vs.110).aspx
Use a database:
The other option is to store in a database (using Entity Framework) which has the added benefit of tracking user data across server recycles.
Have a look at http://www.asp.net/signalr/overview/signalr-20/hubs-api/mapping-users-to-connections which shows all these options a couple of others.
Had the same problem for so long, so gave up the whole signalR at some point, but had to pick it up again for our project:
I have written an answer which might lead you and others on the right track (step by step)...In the answer I am using PersistentConnection rather than Hub, but the principle should be the same:
https://stackoverflow.com/a/25304790/3940626