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
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 retrieving the sid in my WebApi controller using
private string GetAzureSID()
{
var principal = this.User as ClaimsPrincipal;
var nameIdentifier = principal.FindFirst(ClaimTypes.NameIdentifier);
if (nameIdentifier != null)
{
var sid = nameIdentifier.Value;
return sid;
}
return null;
}
And I get a non-null value. However, when I try to call specific hub clients using
hubContext.Clients.User(sid).refresh()
the expected clients do not respond. Actually no clients respond. That said
hubContext.Clients.All.refresh()
does call everyone. I have not done anything like
var idProvider = new PrincipalUserIdProvider();
GlobalHost.DependencyResolver.Register (typeof(IUserIdProvider), () => idProvider);
But I think that should be the default right? What am I missing? Perhaps there is some way of checking what userIds are in Clients?
Update. I found this Context.User.Identity.Name is null with SignalR 2.X.X. How to fix it? which talks about having signalr before webapi, which I tried to no avail. I am using authentication from Azure though, so that could be the issue. HEre is what my ConfigureMobileApp looks like
public static void ConfigureMobileApp(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
new MobileAppConfiguration()
.UseDefaultConfiguration()
.ApplyTo(config);
// Use Entity Framework Code First to create database tables based on your DbContext
// Database.SetInitializer(new MobileServiceInitializer());
var migrator = new DbMigrator(new Migrations.Configuration());
migrator.Update();
MobileAppSettingsDictionary settings = config.GetMobileAppSettingsProvider().GetMobileAppSettings();
if (string.IsNullOrEmpty(settings.HostName))
{
app.UseAppServiceAuthentication(new AppServiceAuthenticationOptions
{
// This middleware is intended to be used locally for debugging. By default, HostName will
// only have a value when running in an App Service application.
SigningKey = ConfigurationManager.AppSettings["SigningKey"],
ValidAudiences = new[] { ConfigurationManager.AppSettings["ValidAudience"] },
ValidIssuers = new[] { ConfigurationManager.AppSettings["ValidIssuer"] },
TokenHandler = config.GetAppServiceTokenHandler()
});
}
app.MapSignalR();
app.UseWebApi(config);
}
It could be that the problem is that the authentication is somehow coming after from Azure? I tried calling the Hub from my Client
[Authorize]
public class AppHub : Hub
{
public string Identify()
{
return Context.User.Identity.Name;
}
}
but the result is 'null' so I think that Signalr is unable to get the User correctly.
Update 2. Could be I need to create a UseOAuthBearerAuthentication that reads [x-zumo-auth]
Update 3. I added some more functions into my Hub
public string Identify()
{
return HttpContext.Current.User.Identity.Name;
}
public bool Authenticated()
{
return Context.User.Identity.IsAuthenticated;
}
public string Bearer()
{
return Context.Headers["x-zumo-auth"];
}
and the results are
null
true
the correct bearer token
Not sure if this helps, but the sid from WebApi look like sid:8ba1a8532eaa6eda6758c3e522f77c24
Update 4. I found the sid! I changed my hub code to
public string Identify()
{
// return HttpContext.Current.User.Identity.Name;
var identity = (ClaimsIdentity)Context.User.Identity;
var tmp = identity.FindFirst(ClaimTypes.NameIdentifier);
return tmp.Value;
}
and I got the sid. Not sure how Context.User.Identity.Name is different than this, but this does work. Now the question is, how can I use a given sid to call
hubContext.Clients.User(...???...).refresh()
if I know the NameIdentifier of the user?
Special thanks to #davidfowler for the remarkably annoying and yet astute "why would it not be null :smile:". Once I finally accepted that Context.User.Identity.Name would always be null, I was able to get the hub to retrieve the sid using
var identity = (ClaimsIdentity)Context.User.Identity;
var tmp = identity.FindFirst(ClaimTypes.NameIdentifier);
return tmp.Value;
which led me to look through the signalr code for User.Identity.Name ultimately landing on PrincipalUserIdProvider. Surprise, surprise, it assigns GetUserId based on User.Identity.Name. I created a new IUserIdProvider:
public class ZumoUserIdProvider : IUserIdProvider
{
public string GetUserId(IRequest request)
{
if (request == null)
{
throw new ArgumentNullException("request");
}
if (request.User != null && request.User.Identity != null)
{
var identity = (ClaimsIdentity)request.User.Identity;
var identifier = identity.FindFirst(ClaimTypes.NameIdentifier);
if (identifier != null)
{
return identifier.Value;
}
}
return null;
}
}
and registered it before anything else in Startup.cs
public void Configuration(IAppBuilder app)
{
var userIdProvider = new ZumoUserIdProvider();
GlobalHost.DependencyResolver.Register(typeof(IUserIdProvider), () => userIdProvider);
ConfigureMobileApp(app);
}
and like magic, I can now hubContext.Clients.User(sid).refresh(). Hope this helps someone out there.
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);
}
}
}
I currently keep my users in a table called OnlineUsers. When a person connects or disconnects it adds the userid and his connectionid to the table, but for some reason (i believe when multiple browser windows are open) the Disconnect function does not fire sometimes, leaving users in the table and making them appear "online" when they really aren't. Has anyone ran into this problem before and what would be a good way to fix this issue?
UPDATE** (sorry about not putting code, I should have done it in the first place)
Here are my db functions to add and remove from the table:
public bool ConnectUser(Guid UserId, String ConnectionId)
{
if (!Ent.OnlineUsers.Any(x => x.UserId == UserId && x.ConnectionId == ConnectionId))
{
Ent.OnlineUsers.AddObject(new OnlineUser { UserId = UserId, ConnectionId = ConnectionId });
Ent.SaveChanges();
return true;
}
else
return false;
}
public void DisconnectUser(Guid UserId, String ConnectionId)
{
if (Ent.OnlineUsers.Any(x => x.UserId == UserId && x.ConnectionId == ConnectionId))
{
Ent.OnlineUsers.DeleteObject(Ent.OnlineUsers.First(x => x.UserId == UserId && x.ConnectionId == ConnectionId));
Ent.SaveChanges();
}
}
Here is my hub class connect and disconnect task:
public Task Disconnect()
{
disconnectUser();
return null;
}
public Task Reconnect(IEnumerable<string> connections)
{
connectUser();
return null;
}
public Task Connect()
{
connectUser();
return null;
}
private void connectUser()
{
if (onlineUserRepository.ConnectUser(MainProfile.UserId, Context.ConnectionId))
{
Groups.Add(Context.ConnectionId, Convert.ToString(MainProfile.ChatId));
}
}
private void disconnectUser()
{
onlineUserRepository.DisconnectUser(MainProfile.UserId, Context.ConnectionId);
Groups.Remove(Context.ConnectionId, Convert.ToString(MainProfile.ChatId));
}
I have checked that I am on the latest version of signalR (0.5.3) and this seems to happen when I have multiple browser windows open and I close them all at once, the users will get stuck in the database.
In case this is needed, this is my Connection Id Generator class:
public class MyConnectionFactory : IConnectionIdGenerator
{
public string GenerateConnectionId(IRequest request)
{
if (request.Cookies["srconnectionid"] != null)
{
return request.Cookies["srconnectionid"].ToString();
}
return Guid.NewGuid().ToString();
}
}
I think your connection factory is indeed the problem. If the case that you do not find a cookie, you go ahead and generate a new guid, but by that time it's already too late.
My understanding is that the connection id is established by the client (the client side hub) during initialization and cannot be changed at the server; it can only be read. In effect when you are returning a new Guid when you don't find the cookie you are changing the client id.
In my connection factory if the cookie is not found I throw. In the controller action that opens the page that is using signalr I make sure the cookie is planted.
Here is my connection factory:
public class ConnectionFactory : IConnectionIdGenerator
{
public string GenerateConnectionId(IRequest request)
{
if (request.Cookies["UserGuid"] != null)
return request.Cookies["UserGuid"].Value;
throw new ApplicationException("No User Id cookie was found on this browser; you must have cookies enabled to enter.");
}
}
I'm new to SignalR but I understand the concept.
My goal is to expose web services which when called will update all connected clients. I've built a little knockout js to connect to the hub and that works well. When I click my start button it sends a message to each of the clients connected like it is supposed to. Here is the code for a background.
Hub:
[HubName("realTimeData")]
public class RealTimeDataHub : Hub
{
public void UpdateFlash(string message)
{
Clients.flashUpdated(message);
}
public void clear()
{
Clients.cleared();
}
}
JS:
function viewModel(){
var self = this;
self.messages = ko.observableArray([]);
self.hub = $.connection.realTimeData;
self.hub.flashUpdated = function (m) {
self.messages.push(new message(m));
};
self.hub.cleared = function () {
self.messages.removeAll();
};
self.Start = function () {
self.hub.updateFlash("Updated Flashed from client");
};
self.Stop = function () {
self.hub.clear();
};
$.connection.hub.start();
}
ko.applyBindings(new viewModel());
MVC API:
public class HomeController : Controller
{
public ActionResult Update()
{
GetClients().updateFlash("From server");
return Json(new { result = "ok" }, JsonRequestBehavior.AllowGet);
}
private static dynamic GetClients()
{
return GlobalHost.ConnectionManager.GetHubContext<RealTimeData.Hubs.RealTimeDataHub>().Clients;
}
}
The problem:
When I call the "Update" method from my controller (Home/Update, not a real API yet) it sends one signal for each of the clients connected. So if I have 2 clients connected, I will get 2 "From Server" messages to each client and so on. I would only like the one...
Any suggestions? Anything is appreciated
Thanks,
Matt
Solution:
Must import 'using SignalR.Client;'
public ActionResult Update()
{
hub.Invoke("UpdateFlash", "From the Home Controller");
//GetClients().updateFlash("From server");
return Json(new { result = "ok" }, JsonRequestBehavior.AllowGet);
}
Here is where I found the solution:
https://github.com/SignalR/SignalR/wiki/SignalR-Client-Hubs
I hope this can help somebody out.
Matt