I'm somewhat new to SignalR. I understand hubs to a limited degree, but I don't understand how two users can share a connection while excluding others.
My scenario is that I want an unauthenticated public website user to be able to initiate a private (not necessarily secure) chat session with a customer service user.
Is there an example or resource that my point me in the right direction?
I've looked at a few resources, including http://www.asp.net/signalr/overview/signalr-20/hubs-api/mapping-users-to-connections but haven't found the right scenario.
You can create groups, so add some methods to your hub (a subscribe method should return a Task as they are asynchronous...)
public Task SubscribeToGroup(string groupName)
{
return Groups.Add(Context.ConnectionId, groupName);
}
Then you publish notifications to users of that group as normal but via the groups collection...
public void BroadcastMessageToGroup(string groupName, string message)
{
Clients.Group(groupName).onCaptionReceived(message);
}
Now only subscribers of that particular group will get the message!
Hope this helps.
You can find a tutorial here for SignalR Groups.
http://www.asp.net/signalr/overview/signalr-20/hubs-api/working-with-groups
You can create a group in Hub's API, in this method each user is a member of that group. And they send a message to that group ( via the server), and because they are only 2 members they are the only one's who see the messages ( privatly)
You can also message a group member directly by connection ID. This requires your app to keep track of connection IDs of users as they connect and disconnect, but this isn't too difficult:
//Users: stores connection ID and user name
public static ConcurrentDictionary Users = new ConcurrentDictionary();
public override System.Threading.Tasks.Task OnConnected()
{
//Add user to Users; user will supply their name later. Also give them the list of users already connected
Users.TryAdd(Context.ConnectionId, "New User");
SendUserList();
return base.OnConnected();
}
//Send everyone the list of users (don't send the userids to the clients)
public void SendUserList()
{
Clients.All.UpdateUserList(Users.Values);
}
//Clients will call this when their user name is known. The server will then update all the other clients
public void GiveUserName(string name)
{
Users.AddOrUpdate(Context.ConnectionId, name, (key, oldvalue) => name);
SendUserList();
}
//Let people know when you leave (not necessarily immediate if they just close the browser)
public override System.Threading.Tasks.Task OnDisconnected()
{
string user;
Users.TryRemove(Context.ConnectionId, out user);
SendUserList();
return base.OnDisconnected();
}
//Ok, now we can finally send to one user by username
public void SendToUser(string from, string to, string message)
{
//Send to every match in the dictionary, so users with multiple connections and the same name receive the message in all browsers
foreach(KeyValuePair user in Users)
{
if (user.Value.Equals(to))
{
Clients.Client(user.Key).sendMessage(from, message);
}
}
}
Related
I want to create a dashboard using signalr server broadcasting to a single client only, not all clients.
first you create group for each user
(user use multitab in browser and call back new connectionid)
https://learn.microsoft.com/en-us/aspnet/core/signalr/groups?view=aspnetcore-5.0
and
public class MyEndPoint : PersistentConnection
{
protected override IList<string> OnRejoiningGroups(IRequest request, IList<string> groups, string connectionId)
{
return groups;
}
}
Here we are using asp.net core singnalR alpha2 version. How to send notification for groups (group of users)? If any sample is there for scenario, post here the sample link.
In Hub write method with group.
public class SignalRCommonHub : Hub
{
public void SendToGroup(int groupId, int userId)
{
Clients.Group(groupId.ToString()).InvokeAsync("refresh", groupId, userId);
}
}
In controller called the hub method.
private readonly IHubContext<SignalRCommonHub> isignalRhub;
public SignalRModel(IHubContext<SignalRCommonHub> signalRhub)
{
this.isignalRhub = signalRhub;
}
public void RefreshPage(int groupId, int userId)
{
this.isignalRhub.Clients.Group(groupId.ToString()).InvokeAsync("refresh", groupId, userId);
}
In client side not trigger when call method in client side.
User online status update based on groups (group of users) using signalR sample available means post here link. Suggest idea for how to implement the user online status.
Thanks,
from your front end interface, you will need to setup the hub/connection, then make a call to Add/Remove groups using the connectionId and the group id/name.
eg: await _viewerHubContext.Groups.AddAsync(connectionId, groupName);
Then when you broadcast calls, you just use the group id/name and signalR will send to those groups.
eg: return Clients.Group(groupName).InvokeAsync("Send", "SendData", data);
Question: How do I manage anonymous users so that multiple tabs in a single browser are all updated when the Hub sends out a response?
The scenario is as follows:
I would like to integrate SignalR into a project so that anonymous users can live chat with operators. Obviously user's that have authenticated with iIdentity are mapped via the Client.User(username) command. But currently say an anonymous user is browsing site.com/tools and site.com/notTools I can not send messages to all tabs with only a connectionID. Only one tab gathers the response.
I have tried using IWC patch but that tool doesn't account for saving chat information into a database and I think passing variables via ajax isn't a secure way to read/write to a database.
I have also looked in the following: Managing SignalR connections for Anonymous user
However creating a base such as that and using sessions seems less secure than Owin.
I had the idea to use client.user() by creating a false account each time a user connects and delete it when they disconnect. But I would rather not fill my aspnetusers db context with garbage accounts it seems unnecessary.
Is it possible to use UserHandler.ConnectedIds.Add(Context.ConnectionId); to also map a fake username? Im at a loss.
Would it make sense to use iUserId provider?
public interface IUserIdProvider
{
string GetUserId(IRequest request);
}
Then create a database that stores IP addresses to connectionIDs and single usernames?
database:
Users: With FK to connection IDs
|userid|username|IP |connectionIDs|
| 1 | user1 |127.0.0.1| 1 |
| 2 | user2 |127.0.0.2| 2 |
connectionIDs:
|connectionID|SignalRID|connectionIDs|
| 1 | xx.x.x.x| 1 |
| 2 | xx.xxx.x| 2 |
| 3 | xx.xxxxx| 2 |
| 4 | xxxxx.xx| 2 |
Then Possibly write logic around the connection?
public override Task OnConnected()
{
if(Context.Identity.UserName != null){
//add a connection based on the currently logged in user.
}
else{
///save new user to database?
}
}
but the question still remains how would I handle multiple tabs with that when sending a command on the websocket?
update
To be clear, my intent is to create a live chat/support tool that allows for in browser anonymous access at all times.
The client wants something similar to http://www.livechatinc.com
I have already created a javascript plugin that sends and receives from the hub, regardless of what domain it is on. (my client has multisites) the missing piece to the puzzle is the simplest, managing the anonymous users to allow for multi-tabbed conversations.
I'm not following the false user account idea (don't know if it works), but will develop an alternative.
The goal could be achieved through a ChatSessionId cookie shared by all browser tabs and creating Groups named as this ChatSessionId.
I took basic chat tutorial from asp.net/signalr and added functionality to allow chat from multiple tabs as the same user.
1) Assign a Chat Session Id in "Chat" action to identify the user, as we don't have user credential:
public ActionResult Chat()
{
ChatSessionHelper.SetChatSessionCookie();
return View();
}
2) Subscribe to chat session when enter the chat page
client side
$.connection.hub.start()
.then(function(){chat.server.joinChatSession();})
.done(function() {
...
server side (hub)
public Task JoinChatSession()
{
//get user SessionId to allow use multiple tabs
var sessionId = ChatSessionHelper.GetChatSessionId();
if (string.IsNullOrEmpty(sessionId)) throw new InvalidOperationException("No chat session id");
return Groups.Add(Context.ConnectionId, sessionId);
}
3) broadcast messages to user's chat session
public void Send(string message)
{
//get user chat session id
var sessionId = ChatSessionHelper.GetChatSessionId();
if (string.IsNullOrEmpty(sessionId)) throw new InvalidOperationException("No chat session id");
//now message will appear in all tabs
Clients.Group(sessionId).addNewMessageToPage(message);
}
Finally, the (simple) ChatSessionHelper class
public static class ChatSessionHelper
{
public static void SetChatSessionCookie()
{
var context = HttpContext.Current;
HttpCookie cookie = context.Request.Cookies.Get("Session") ?? new HttpCookie("Session", GenerateChatSessionId());
context.Response.Cookies.Add(cookie);
}
public static string GetChatSessionId()
{
return HttpContext.Current.Request.Cookies.Get("Session")?.Value;
}
public static string GenerateChatSessionId()
{
return Guid.NewGuid().ToString();
}
}
a solution widely adopted is to make the user register with his some kind of id with connection back , on the onConnected.
public override Task OnConnected()
{
Clients.Caller.Register();
return base.OnConnected();
}
and than the user returns with a call with some kind of your own id logic
from the clients Register Method
public void Register(Guid userId)
{
s_ConnectionCache.Add(userId, Guid.Parse(Context.ConnectionId));
}
and you keep the user ids in a static dictionary ( take care of the locks since you need it to be thread safe;
static readonly IConnectionCache s_ConnectionCache = new ConnectionsCache();
here
public class ConnectionsCache :IConnectionCache
{
private readonly Dictionary<Guid, UserConnections> m_UserConnections = new Dictionary<Guid, UserConnections>();
private readonly Dictionary<Guid,Guid> m_ConnectionsToUsersMapping = new Dictionary<Guid, Guid>();
readonly object m_UserLock = new object();
readonly object m_ConnectionLock = new object();
#region Public
public UserConnections this[Guid index]
=>
m_UserConnections.ContainsKey(index)
?m_UserConnections[index]:new UserConnections();
public void Add(Guid userId, Guid connectionId)
{
lock (m_UserLock)
{
if (m_UserConnections.ContainsKey(userId))
{
if (!m_UserConnections[userId].Contains(connectionId))
{
m_UserConnections[userId].Add(connectionId);
}
}
else
{
m_UserConnections.Add(userId, new UserConnections() {connectionId});
}
}
lock (m_ConnectionLock)
{
if (m_ConnectionsToUsersMapping.ContainsKey(connectionId))
{
m_ConnectionsToUsersMapping[connectionId] = userId;
}
else
{
m_ConnectionsToUsersMapping.Add(connectionId, userId);
}
}
}
public void Remove(Guid connectionId)
{
lock (m_ConnectionLock)
{
if (!m_ConnectionsToUsersMapping.ContainsKey(connectionId))
{
return;
}
var userId = m_ConnectionsToUsersMapping[connectionId];
m_ConnectionsToUsersMapping.Remove(connectionId);
m_UserConnections[userId].Remove(connectionId);
}
}
a sample call to Register form an android app
mChatHub.invoke("Register", PrefUtils.MY_USER_ID).get();
for JS it would be kind of the same
chat.client.register = function () {
chat.server.register(SOME_USER_ID);
}
I have a signalr hub with a function like this
public void SendSurveyNote(int surveyId,List<string> users){}
Here I want to add all users of the list into a group "Survey_" + surveyId Then sending the group a message. But I only have the user id but joining a group requires a connection id. So how could I manage that.
Also I wonder would it be a performance issue to send each user a message without a group?
I call the function above when I add a new Survey like this
private static HubConnection hubConnection = new HubConnection(ConfigurationManager.AppSettings["BaseUrl"]);
private static IHubProxy hubProxy = hubConnection.CreateHubProxy("myHub");
await hubConnection.Start();
hubProxy.Invoke("SendSurveyNote", model.Id, users);
thanks
You have access to the connection ID within Context. You'll want to establish groups within OnConnected. Observe the following implementation on your hub, where we will call it MyHub. We'll also group by Context.User.Identity.Name to establish a unique group per user, but this could be any value you wish to group by.
public class MyHub: Hub
{
public override Task OnConnected()
{
Groups.Add(Context.ConnectionId, Context.User.Identity.Name)
return base.OnConnected();
}
}
See Working with Groups in SignalR for more information
I'm writing a C#-based web application using SignalR. So far I have a 'lobby' area (where open communication is allowed), and an 'session' area (where groups of 5 people can engage in private conversation, and any server interactions are only shown to the group).
What I'd like to do is create a 'logging' object in memory - one for each session (so if there are three groups of five people, I'd have three logging objects).
The 'session' area inherits from Hubs (and IDisconnect), and has several methods (Join, Send, Disconnect, etc.). The methods pass data back to the JavaScript client, which calls client-side JS functions. I've tried using a constructor method:
public class Session : Hub, IDisconnect
{
public class Logger
{
public List<Tuple<string, string, DateTime>> Log;
public List<Tuple<string, string, DateTime>> AddEvent(string evt, string msg, DateTime time)
{
if (Log == null)
{
Log = new List<Tuple<string, string, DateTime>>();
}
Log.Add(new Tuple<string, string, DateTime>(evt, msg, time));
return Log;
}
}
public Logger eventLog = new Logger();
public Session()
{
eventLog = new Logger();
eventLog.AddEvent("LOGGER INITIALIZED", "Logging started", DateTime.Now);
}
public Task Join(string group)
{
eventLog.AddEvent("CONNECT", "User connect", DateTime.Now);
return Groups.Add(Context.ConnectionId, group);
}
public Task Send(string group, string message)
{
eventLog.AddEvent("CHAT", "Message Sent", DateTime.Now);
return Clients[group].addMessage(message);
}
public Task Interact(string group, string payload)
{
// deserialise the data
// pass the data to the worker
// broadcast the interaction to everyone in the group
eventLog.AddEvent("INTERACTION", "User interacted", DateTime.Now);
return Clients[group].interactionMade(payload);
}
public Task Disconnect()
{
// grab someone from the lobby?
eventLog.AddEvent("DISCONNECT","User disconnect",DateTime.Now);
return Clients.leave(Context.ConnectionId);
}
}
But this results in the Logger being recreated every time a user interacts with the server.
Does anyone know how I'd be able to create one Logger per new session, and add elements to it? Or is there a simpler way to do this and I'm just overthinking the problem?
Hubs are created and disposed of all the time! Never ever put data in them that you expect to last (unless it's static).
I'd recommend creating your logger object as it's own class (not extending Hub/IDisconnect).
Once you have that create a static ConcurrentDictionary on the hub which maps SignalR groups (use these to represent your sessions) to loggers.
When you have a "Join" method triggered on your hub it's easy as looking up the group that the connection was in => Sending the logging data to the groups logger.
Checkout https://github.com/davidfowl/JabbR when it comes to making "rooms" and other sorts of groupings via SignalR
Hope this helps!