dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.47k stars 10.03k forks source link

How to disconnect specified client from outside hub #5333

Closed bluetianx closed 3 years ago

bluetianx commented 6 years ago

I have a web Api for disconnectting specified client ,how to disconnect specified client from outside hub. I do not find a method about disconnect client from IHubContext;

atresnjo commented 6 years ago

Yeah IClientProxy doesn't have the Abort method. It's just available in the hub context. I'd suggest invoking a method on client side which will gracefully disconnect then.

analogrelay commented 6 years ago

We don't really support this from HubContext, but it's something to consider for the future. A graceful shutdown based on invoking a "Shutdown" method on the client is more appropriate here, as @atresnjo suggests. Abruptly terminating connections should generally only be used when the client is unresponsive for uncooperative.

It's difficult to add this ability to the HubContext because in a multi-server scenario (when scaling), we don't know if the code that's calling "Abort" is running on the same server as the connection. If it's not, then we have to send that information to the appropriate server, which has a whole bunch of other issues behind it.

bluetianx commented 6 years ago

I want client to directly invok OnDisconnectedAsync method (public override async Task OnDisconnectedAsync (Exception exception) ) of Hub class. Is this a best practice ?

atresnjo commented 6 years ago

@bluetianx

No. Implement your own method and invoke it when needed.

Something like:

public class ServerHub : Hub
{
}

public class ClientHub : Hub
{
    public ClientHub()
    {
        hubConnection.On<string>("Shutdown",
            ShutdownReceived);
    }

    public void ShutdownReceived(string reason)
    {

    }
}

public class ShutdownService
{
    private readonly IHubContext<ServerHub> _hubContext;
    public ShutdownService(IHubContext<ServerHub> hub)
    {
        _hubContext = hub;
    }

    public void SendShutdown()
    {
        _hubContext.Clients.All.SendAsync("Shutdown", "Reason");
    }
}
bluetianx commented 6 years ago

You may have misunderstood me. My demo code is as follows :

-----server code

public class NotificationHub:Hub
{

        public override async Task OnConnectedAsync()
        {
            await base.OnConnectedAsync();
        }
        public override async Task OnDisconnectedAsync(Exception exception)
        {
            // deleteOffLineUser();
            await base.OnDisconnectedAsync(exception);
        }
        public async bool ShutDown(string reason)
        {
            //TODO
            // delete connect record  from database
            await base.OnDisconnectedAsync(null);
            return true;
        }
}

// webAPI

Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private readonly IHubContext<NotificationHub> _hubContext;
        [HttpPost]
        public bool DisconnectUser1()
        {
            _hubContext.Clients.All.SendAsync("Shutdown", "reason");
        }

        [HttpPost]
        public bool DisconnectUser2()
        {
            _hubContext.Clients.All.SendAsync("DIsconnect", "reason");
        }

    }

// web client JS

var connection = new signalR.HubConnectionBuilder().withUrl("http://localhost:5050/notificationhub").build();

    connection.on("Shutdown", function (message) {
        connection.invoke("Shutdown", message);
    });

    connection.on("Disconnect", function (message) {
        connection.invoke("OnDisconnectedAsync", null);
    });

    connection.start().catch(function (err) {
        console.log(err.toString());
        return console.error(err.toString());
    }).then(function(data) {
        //connection.invoke("Register", userId);
    });

I want to disconnect connection of HTTP, Such as websocket , Should I request DisconnectUser1 or DisconnectUser2 by WebAPI.

BrennanConroy commented 6 years ago

Do not call OnDisconnectedAsync manually. It will not do what you expect. OnDisconnectedAsync is called when the connection is terminated. So in your Shutdown method you can call Context.Abort(); which will then trigger the OnDisconnectedAsync method correctly.

analogrelay commented 6 years ago
connection.on("Shutdown", function (message) {
    connection.invoke("Shutdown", message);
});

connection.on("Disconnect", function (message) {
    connection.invoke("OnDisconnectedAsync", null);
});

In these cases, you can just use connection.stop on the client to disconnect. There's no reason to call OnDisconnectedAsync. And, as @BrennanConroy said, it will not work to call it manually.

Here's a complete sample that will allow you to signal a client to disconnect from the server outside the Hub:

NotificationHub.cs:

public class NotificationHub: Hub
{
    // You don't need any code here to specifically support this. But if you want to
    // run code when a client does disconnect, you can override OnDisconnectedAsync
}

Client.js

var connection = new signalR.HubConnectionBuilder().withUrl("http://localhost:5050/notificationhub").build();

connection.on("RequestShutdown", function (reason) {
    console.log("Server requested we disconnect because: " + reason);
    connection.stop();
});

connection.start().catch(function (err) {
    console.log(err.toString());
    return console.error(err.toString());
}).then(function(data) {
    // Do startup things
});

WebAPI.cs

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IHubContext<NotificationHub> _hubContext;

    [HttpPost]
    public bool DisconnectUser(string userName)
    {
        _hubContext.User(userName).SendAsync("RequestShutdown", "reason");
    }
}
bluetianx commented 6 years ago

OK,I understand,Thank you very much @atresnjo @anurse @BrennanConroy

analogrelay commented 6 years ago

Blocked by #3284

thorgeirk11 commented 5 years ago

Has there been any progress made on this issue? Can the server disconnect a client without having to send a message to the client? @anurse @BrennanConroy

analogrelay commented 5 years ago

It's still in the backlog. Let us know your scenario though and it will help us prioritize this work in a future release!

thorgeirk11 commented 5 years ago

We are developing an MMO game, and we are in the process of switching over to use SignalR.

The functionality we have not been able to port over to SignalR includes:

In both of these scenarios, the server needs to have the authority to disconnect players without sending a disconnect request.

analogrelay commented 5 years ago

That makes sense! Thanks for the info.

Inurias commented 4 years ago

For us, this would also be a very important feature.

We are working on a new backend for a game. There are many scenarios where a client could not be cooperative and will try to cause harm - in which case we have to forcefully close the connection from the server. In addition, we also have similar scenarios like those described by @thorgeirk11

Modifying the client and changing any client-side disconnection logic in order to ignore the requested disconnect from the server is trivial. This is a very important and security-critical feature. Even if we can disallow a client to execute any further logic on the server, the underlying connection will stay open, since the server cannot close it. I think this should have been possible from the first-ever version. Please consider assigning this a high priority. @anurse @BrennanConroy

analogrelay commented 4 years ago

There is likely a workaround way to implement this right now. The necessary logic should already exist in the Hub itself (via Context.Abort()). The request here is to be able to do it from outside the hub.

While we don't generally recommend doing this, it is currently OK to capture the HubCallerContext (from the Context property) in OnConnectedAsync, store it somewhere (a dictionary indexed by ConnectionID perhaps) and then call .Abort() on it to terminate the connection.

I do agree that we should look at a more first-class way to do this, but I'd be curious if this workaround works for your scenario. Would you be able to try it and report back? We'll look at this for 5.0, but that's not coming until November 2020 and I'd like to be able to give you something that works before then :).

analogrelay commented 4 years ago

An important note about my workaround: This will only work on the physical server process on which the client is connected. It will not be possible to forcibly disconnect a connection that resides on a different physical server process (in a scenario where you have multiple servers connected via a backplane like Redis or Azure SignalR).

Honestly, I'd expect the first-class solution to have a similar restriction, since it's very tricky to make that work reliably (since we don't have a single source of truth for which connections exist and on which servers they reside).

Inurias commented 4 years ago

There is likely a workaround way to implement this right now. The necessary logic should already exist in the Hub itself (via Context.Abort()). The request here is to be able to do it from outside the hub.

While we don't generally recommend doing this, it is currently OK to capture the HubCallerContext (from the Context property) in OnConnectedAsync, store it somewhere (a dictionary indexed by ConnectionID perhaps) and then call .Abort() on it to terminate the connection.

I do agree that we should look at a more first-class way to do this, but I'd be curious if this workaround works for your scenario. Would you be able to try it and report back? We'll look at this for 5.0, but that's not coming until November 2020 and I'd like to be able to give you something that works before then :).

I was thinking about storing the HubCallerContext and using it outside a hub, but I was not sure if it was safe (ie. HubCallerContext instance might be different every call). I will gladly try it and report back once we are testing our SignalR code. Thank you for the quick help!

analogrelay commented 4 years ago

I was not sure if it was safe

It should be safe. I do want to clarify that it's a bit of an unsupported path. I don't expect we'd break it (and certainly not until we have a first-class way to disconnect a specific client) but I would definitely put some comments in there referencing this issue and plan to switch to the actual feature once we get one in ;). It may be that we just make this a fully-supported thing (capturing the HubCallerContext). There's nothing terribly wrong with doing it, it's just not something we have documentation and tests in place for :).

davidfowl commented 4 years ago

As useful as this feature is I don't think we should implement anything that needs to communicate across servers over the backplane to take an action on a connection. Aborting the socket needs to happen from the server where that socket is. It's simple an reliable (at least is should be) and there's no possibility of missing the notification because redis went down or because a backplane is poorly written.

This one of the major reason the old version of SignalR has such gnarly reliability bugs and those mistakes shouldn't be repeated here. SignalR needs to remain a super thin layer of RPC over WebSockets with some nice ways to send/receive messages with different protocols.

Tommigun1980 commented 4 years ago

@davidfowl et al; I am using SignalR for sending notifications to clients in a social app (ASP.NET sever and Xamarin .NET client). It is of utmost importance that when a client logs out (either from the current session, or all of his sessions), that the server aborts connections to said client. I can not rely on the client aborting the connection itself; no the server must obey when a client requests a disconnection. In no scenario can a connection remain open to a client after he has requested a disconnection or I won't be able to ship my product, as it is trivial to hack a client to not obey.

I have implemented what was suggested here, ie I cache the context object in a ConcurrentDictionary in a singleton service. @davidfowl you said that even when Abort() is fully supported outside of a hub you won't implement it "at scale". My plan was to use the SignalR Service on Azure. So how can I ensure that the server actually disconnects a client when need be, after deploying my server to the SignalR Service? This is not a "nice to have" feature but an absolute requirement for any application that deals with personal and/or sensitive information. If I understand your guidance correctly, even when saving the context Abort() will only work for clients that are connected to the same server where Abort() is called. That is not good enough, so eagerly awaiting for guidance on this.

Also, is it possible that I'll run into memory issues by saving the context object? I have to do it now as there is no supported way to call Abort() outside of the hub, but I want to make sure I am not opening up more problems by following this guidance.

Thanks.

davidfowl commented 4 years ago

Abort works fine on the Azure signalr service and that’s the way to do it

Tommigun1980 commented 4 years ago

Abort works fine on the Azure signalr service and that’s the way to do it

Thank you. It was not clear to me that Azure SignalR service would work out of the box. Thanks for the clarification.

Tommigun1980 commented 4 years ago

An important note about my workaround: This will only work on the physical server process on which the client is connected. It will not be possible to forcibly disconnect a connection that resides on a different physical server process (in a scenario where you have multiple servers connected via a backplane like Redis or Azure SignalR).

@davidfowl @anurse I'd like to just verify that it will indeed work with the Azure SignalR service at scale, as @anurse wrote in the quoted section that it won't. So I have conflicting information here.

Thank you.

Tommigun1980 commented 4 years ago

@davidfowl @anurse Hi, is there any way you guys could confirm whether calling Abort() works in all cases when using the SignalR service? @anurse said it only works when they are on the same server, while @davidfowl said it works fine in all cases.

I will be deploying my server with the SignalR service. My server sends notifications to connected clients through SignalR. It also provides a REST API where clients can log out, in which case the server revokes his access token and calls Abort() on any connection(s) the client may have. Is it guaranteed that Abort() will break the pipe to the user in all cases in this scenario? The connection sends, amongst other things, chat messages so it is paramount that the connection is severed on the server side when a client requests so.

If it works, fantastic, if not, what is the guidance on how to work around this?

Thank you so much!

mgx0612 commented 4 years ago

really need a way to disconnect Clients/Users from the server side, API like below: Clients.Users( [userIds] ).Disconnect(); Clients.Clients( [connectionIds] ).Disconnect();

should remove all those connectionIds from the joined Group automatically when call those APIs.

to manually bookkeeping the connectionId and intercept the call to Hub then call Context.Abort() if Context.connectionId match the saved connectionId,

there is too much work to do out of the lib and hardly do it right by the application developers.

if simply one server, keep in the memory is fine, but if want to scale, say, with Redis, then need another different implimentation,to keep the connectionId in the redis as well.

better to have this feature in the signalr lib itself, and can scale the same way like scale out signalr.

davidfowl commented 4 years ago

Not gonna happen for the reasons I mentioned above

Tommigun1980 commented 4 years ago

Not gonna happen for the reasons I mentioned above

Hi @davidfowl! I'm reaching out regarding "Abort works fine on the Azure signalr service and that’s the way to do it" -- could you please specify that Abort works fine also at scale with the Azure SignalR service. In other words, if I use the Azure SignalR service and multiple servers are involved, will it just work out of the box? Thanks so much.

mgx0612 commented 4 years ago

" SignalR needs to remain a super thin layer of RPC over WebSockets with some nice ways to send/receive messages with different protocols."

Look at the API of Signalr, quite a few API require connectionId as a parameter. It's better to call SignalR a websocket server in RPC disguise. it's a websocket server you don't have to write your own decoder/encoder only.

and it supports groups and broadcasting, so it's very useful to manage the sub/pub pattern on server side. but classical RPC is request/response pattern.

Donistivanov commented 4 years ago

Would you be able to try it and report back?

Here is something if anyone needs a starting point:

public class ConnectionManagerService
{
    private Dictionary<string, List<HubCallerContext>> teamConnections = new Dictionary<string, List<HubCallerContext>>();
    public void AddConnection(string teamId, HubCallerContext newContext)
    {
        lock (teamConnections)
        {
            if (teamConnections.TryGetValue(teamId, out var list))
            {
                if (list.Count == 5)
                {
                    list[0].Abort(); // Oldest connection
                    list.RemoveAt(0);
                }
                list.Add(newContext);
            }
            else
            {
                var newList = new List<HubCallerContext>(5);
                newList.Add(newContext);
                teamConnections.Add(teamId, newList);
            }
        }
    }

    public void DisconnectTeam(string teamId)
    {
        lock (teamConnections)
        {
            if (teamConnections.Remove(teamId, out var list))
            {
                list.ForEach(ctx => ctx.Abort());
            }
        }
    }

    public void DisconnectConnectionId(string teamId, string connectionId)
    {
        lock (teamConnections)
        {
            if (teamConnections.Remove(teamId, out var list))
            {
                var foundConnection = list.Find(ctx => ctx.ConnectionId == connectionId);

                if (foundConnection != null)
                {
                    foundConnection.Abort();
                    list.Remove(foundConnection);

                    if (list.Count == 0)
                    {
                        return;
                    }
                    else
                    {
                        teamConnections.Add(teamId, list);
                    }
                }
            }
        }
    }
}
Tommigun1980 commented 4 years ago

Would you be able to try it and report back?

Here is something if anyone needs a starting point:

public class ConnectionManagerService
{
    private Dictionary<string, List<HubCallerContext>> teamConnections = new Dictionary<string, List<HubCallerContext>>();
    public void AddConnection(string teamId, HubCallerContext newContext)
    {
        lock (teamConnections)
        {
            if (teamConnections.TryGetValue(teamId, out var list))
            {
                if (list.Count == 5)
                {
                    list[0].Abort(); // Oldest connection
                    list.RemoveAt(0);
                }
                list.Add(newContext);
            }
            else
            {
                var newList = new List<HubCallerContext>(5);
                newList.Add(newContext);
                teamConnections.Add(teamId, newList);
            }
        }
    }

    public void DisconnectTeam(string teamId)
    {
        lock (teamConnections)
        {
            if (teamConnections.Remove(teamId, out var list))
            {
                list.ForEach(ctx => ctx.Abort());
            }
        }
    }

    public void DisconnectConnectionId(string teamId, string connectionId)
    {
        lock (teamConnections)
        {
            if (teamConnections.Remove(teamId, out var list))
            {
                var foundConnection = list.Find(ctx => ctx.ConnectionId == connectionId);

                if (foundConnection != null)
                {
                    foundConnection.Abort();
                    list.Remove(foundConnection);

                    if (list.Count == 0)
                    {
                        return;
                    }
                    else
                    {
                        teamConnections.Add(teamId, list);
                    }
                }
            }
        }
    }
}

To the best of my understanding that dictionary should be a ConcurrentDictionary to make it thread safe.

And it’s still an open question whether you can disconnect clients from other machines at a scale, like with SignalR Service. It doesn’t require sticky sessions so what happens if a rest call, that ultimately needs to disconnect a SignalR client, executes on another machine than which serves the connection (and stores the id)? I’d love to get confirmation on this.

Also, considering that the entire context needs to be retained, how much memory will this take when you have lots of clients? It’s such a hack that I want to understand the ramifications of it. Thanks.

Donistivanov commented 4 years ago

@Tommigun1980 I just set up a test SignalR service and I aborted the connection on my server and my client said the connection to azure was disconnected.

I wonder about the things you mention too, but one thing that might help you be confident in continuing to work is to do some very dirty math. Even if your revenue is $1 per connection a month, then at max capacity for one SignalR Service, your app will be bringing in $100k/mo. The SignalR service maybe would cost ~$5k/mo, and if you just throw more memory/compute azure offers some serious VMs for ~$75k/mo. You are left with a tidy sum of ~$20k/mo which you will use to pay to have it be some architect's problem, which David suggests as a solution to a similar problem here: https://youtu.be/J-xqz_ZM9Wg?t=2552 (Thanks for the talks David, I am a big fan!)

Tommigun1980 commented 4 years ago

I still haven't gotten a reply to this and I need to know this soon;

When keeping a list of connections in memory with the purpose of being able to kick connections, and when using the SignalR service -- can ARR affinity still be turned off?

@davidfowl

Thank you!

davidfowl commented 4 years ago

When keeping a list of connection in memory with the purpose of being able to kick connections,

This isn't a question but maybe I'm missing something. You can kick connections by calling abort (that's mentioned many times on this thread).

and when using the SignalR service -- can ARR affinity still be turned off?

Yes it can be turned off.

Tommigun1980 commented 4 years ago

When keeping a list of connection in memory with the purpose of being able to kick connections,

This isn't a question but maybe I'm missing something. You can kick connections by calling abort (that's mentioned many times on this thread).

and when using the SignalR service -- can ARR affinity still be turned off?

Yes it can be turned off.

Hi @davidfowl and thanks for the answer.

I would love to call Abort, but there isn't a way to do it, which this topic is about.

As @anurse said,

"There is likely a workaround way to implement this right now. The necessary logic should already exist in the Hub itself (via Context.Abort()). The request here is to be able to do it from outside the hub. While we don't generally recommend doing this, it is currently OK to capture the HubCallerContext (from the Context property) in OnConnectedAsync, store it somewhere (a dictionary indexed by ConnectionID perhaps) and then call .Abort() on it to terminate the connection.

An important note about my workaround: This will only work on the physical server process on which the client is connected. It will not be possible to forcibly disconnect a connection that resides on a different physical server process (in a scenario where you have multiple servers connected via a backplane like Redis or Azure SignalR)."

I am under the same impression as anurse -- I just can't see how it could work with ARR off?

The only Abort() I have found is in the HubCallerContext, which is only accessible when a client connects to a Hub. I may want to disconnect him hours later, so I store all users' HubCallerContexts in a dictionary. Considering that this adds state -- I don't understand how turning off ARR could ever work - please advice me @davidfowl (anurse goes as far to say it won't at all with SignalR service, but I think it would if ARR is on). I don't want to save the HubCallerContexts but it was the only way I could see it could be done, and when I found this thread it just reinforces it. If there is indeed a magical Abort() somewhere else I'd love it and this entire exercise would be moot!

Thanks again.

Tommigun1980 commented 4 years ago

When keeping a list of connection in memory with the purpose of being able to kick connections,

This isn't a question but maybe I'm missing something. You can kick connections by calling abort (that's mentioned many times on this thread).

And to get back to this once more -- the problem here is that Abort() is only in a HubCallerContext -- there is for example no abort in the users as fetched from IHubContext. Because of this every user's HubCallerContext has to be stored in memory so Abort() can be called later -- and this adds state.

So 1) Is there an abort somewhere else I and everyone else are missing? 2) If not, could one be added to IHubContext, for example myHubContext.Connections, or something? 3) In the interim, please explain what the recommended way to do this is? Is it indeed to store HubCallerContexts in memory? 4) If it is, please explain how it can work with ARR on as it introduces state? 5) Also, please confirm that storing HubCallerContext in memory works with the SignalR service at scale (it works in my small tests but can't say if it would work at scale).

If you could answer all of my questions it would be tremendous as I'm struggling with this a bit. Thanks again!

davidfowl commented 4 years ago

"There is likely a workaround way to implement this right now. The necessary logic should already exist in the Hub itself (via Context.Abort()). The request here is to be able to do it from outside the hub. While we don't generally recommend doing this, it is currently OK to capture the HubCallerContext (from the Context property) in OnConnectedAsync, store it somewhere (a dictionary indexed by ConnectionID perhaps) and then call .Abort() on it to terminate the connection.

Call Abort, it's fine. It'll abort the underlying connection as stated and should do the right thing.

I am under the same impression as anurse -- I just can't see how it could work with ARR off? The only Abort() I have found is in the HubCallerContext, which is only accessible when a client connects to a Hub. I may want to disconnect him hours later, so I store all users' HubCallerContexts in a dictionary. Considering that this adds state -- I don't understand how turning off ARR could ever work - please advice me @davidfowl (anurse goes as far to say it won't at all with SignalR service, but I think it would if ARR is on). I don't want to save the HubCallerContexts but it was the only way I could see it could be done, and when I found this thread it just reinforces it. If there is indeed a magical Abort() somewhere else I'd love it and this entire exercise would be moot!

First, if you're using Azure SignalR stickiness is handled on your behalf. The connections aren't going through your webserver at all. Abort is "magical" in the case of the service and it is wired up correctly to send a message to the service and disconnect connected client.

Tommigun1980 commented 4 years ago

First, if you're using Azure SignalR stickiness is handled on your behalf. The connections aren't going through your webserver at all. Abort is "magical" in the case of the service and it is wired up correctly to send a message to the service and disconnect connected client.

Thank you. I know the connections themselves are handled this way, but I'm not talking about that.

Without ARR, how could this ever work? 1) Client A connects to server 1. Server 1 puts A's context in a dictionary. 2) After an hour client A is assigned to server 2. 3) Server 2 does not have client A's context in a dictionary. 4) Client A makes a REST call that's not allowed, he should be booted... 5) ...BUT client A's context is not in the current server's memory, as the context is stored in server 1.

I can not understand how this could work. I know that the connection magic is handled by the service, but I am physically forced to store the client's context in memory on the servers, and that SignalR service can't of course magically propagate.

I am not concerned about Abort() not doing its thing - I am concerned about being able to access the context on other machines, so I could even call Abort().

Please advice.

davidfowl commented 4 years ago

Assuming we're still talking about Azure SignalR ARR is irrelevant as the relevant requests don't go through ARR or your webserver for that matter.

In your scenario, you need to decouple the notion of who is doing the abort from where that client is. To abort a client you need to make sure you know where that machine that has the client is. SignalR isn't going to do this for you so you need a bus to send a message to all machines (or be smart enough to know which machine has which client) and then call Abort on the appropriate target.

Tommigun1980 commented 4 years ago

Assuming we're still talking about Azure SignalR ARR is irrelevant as the relevant requests don't go through ARR or your webserver for that matter.

In your scenario, you need to decouple the notion of who is doing the abort from where that client is. To abort a client you need to make sure you know where that machine that has the client is. SignalR isn't going to do this for you so you need a bus to send a message to all machines (or be smart enough to know which machine has which client) and then call Abort on the appropriate target.

But with ARR then the client would always be connected to the same server, which has his context object in memory. That would solve the issue, no? The problem is that there's no way to disconnect a user when using the SignalR service and multiple servers, unless a user is parked to the same server, or a system like you described is added (which defeats the entire point with the SignalR service). If I'm doing all that then I don't see much value in the SignalR service.

Couldn't the IHubContext object just get abort functionality? One already uses it to call RPCs on the clients, so why not add a severe connection method? I would be fine with errors trying to use the connection later on - the point is it should be called for malicious clients, and it becomes important in chats etc. where personal information may be exchanged. Not providing that makes the entire system not really ready for production use imho. Calling a disconnect RPC, where the client can easily be hacked to not comply, just isn't enough.

So if SignalR service can't handle disconnection of clients, I am left puzzled of its use case. I would really like it if this could be implemented into the IHubContext object somehow. As I said I'd be perfectly fine if it wasn't perfect, the aim here is just to have control over stopping a connection in an emergency.

Thanks.

davidfowl commented 4 years ago

But with ARR then the client would always be connected to the same server, which has his context object in memory. That would solve the issue, no? The problem is that there's no way to disconnect a user when using the SignalR service and multiple servers, unless a user is parked to the same server, or a system like you described is added (which defeats the entire point with the SignalR service). If I'm doing all that then I don't see much value in the SignalR service.

How? When connections go through the SignalR service? How does those connections map to a REST API calls made directly to your webserver? They don't. You can surely self host SignalR (and ARR session affinity is required in that situation).

Couldn't the IHubContext object just get abort functionality? One already uses it to call RPCs on the clients, so why not add a severe connection method? I would be fine with errors trying to use the connection later on - the point is it should be called for malicious clients, and it becomes important in chats etc. where personal information may be exchanged.

No, we have no plans to add this functionality, at least in the short term.

Not providing that makes the entire system not really ready for production use imho. Calling a disconnect RPC, where the client can easily be hacked to not comply, just isn't enough.

I'm sorry you can't use SignalR for your scenario.

Tommigun1980 commented 4 years ago

But with ARR then the client would always be connected to the same server, which has his context object in memory. That would solve the issue, no? The problem is that there's no way to disconnect a user when using the SignalR service and multiple servers, unless a user is parked to the same server, or a system like you described is added (which defeats the entire point with the SignalR service). If I'm doing all that then I don't see much value in the SignalR service.

How? When connections go through the SignalR service? How does those connections map to a REST API calls made directly to your webserver? They don't. You can surely self host SignalR (and ARR session affinity is required in that situation).

Couldn't the IHubContext object just get abort functionality? One already uses it to call RPCs on the clients, so why not add a severe connection method? I would be fine with errors trying to use the connection later on - the point is it should be called for malicious clients, and it becomes important in chats etc. where personal information may be exchanged.

No, we have no plans to add this functionality, at least in the short term.

Not providing that makes the entire system not really ready for production use imho. Calling a disconnect RPC, where the client can easily be hacked to not comply, just isn't enough.

I'm sorry you can't use SignalR for your scenario.

I don't think we are on the same page here at all. Let me try to explain better;

I am using SignalR in serverful mode (or whatever the inverse of serverless is) with SignalR service. A user connects to a hub, on whatever server he is on. On said machine his context is saved. He does something malicious in a REST call --- I have his context in memory so I can call Abort(). If he was transferred to another server his context would no longer be present.

Why would this be a problem at all if ARR is turned on?

davidfowl commented 4 years ago

When you use Azure SignalR in server mode, the client is connected to the service and you application server is connected to the service. The service then routes traffic from your client to your service via those 2 connections. The REST API calls made directly to your server go have no bearing on where the SignalR connections are when using the service.

Does that make sense?

Tommigun1980 commented 4 years ago

When you use Azure SignalR in server mode, the client is connected to the service and you application server is connected to the service. The service then routes traffic from your client to your service via those 2 connections. The REST API calls made directly to your server go have no bearing on where the SignalR connections are when using the service.

Does that make sense?

Yes, that's exactly how I have understood it, thanks for confirming it. Doesn't that mean turning on ARR would then work (assuming the server where the hub context is created is the same as where the user is routed for other calls)? As long as the user doesn't get assigned to another server, his context will be in memory.

Or are you saying the server where the hub connect is is called, where the context is saved, could be on another machine than where he's routed? That would be a problem for sure. Turning on ARR would for sure mean I have access to his context so that part would not be a problem, as long as it happens once on the same server.

davidfowl commented 4 years ago

I'm saying ARR session affinity isn't in the picture at all when it comes to determining where to route requests for SignalR clients that make it to your servers. Unless you somehow teach it how to route to the right server based on some information (and I don't know where you'd get it from), it doesn't help.

You might be under the impression that the signalr service is routing connections to your application via an inbound HTTP request but that's not the case.

Tommigun1980 commented 4 years ago

Thank you for your answers, it has been really helpful.

Is there a chance you could confirm one more thing for me, not related to Abort but the connections as a whole in regard to the SignalR service;

Let's say you have horizontally scaling REST servers, also using SignalR in serverful mode via the SignalR service. I am using SignalR to send notifications to clients by calling various RPCs. Let's say Client A makes a REST call of something client B should be notified of. Let's say the machine where client A's Hub.OnConnectedAsync has been called on is different than client B's. Client B should be notified of, so in the REST call handler for client A an IHubContext is injected. Will it be able to access client B?

The SignalR service is hard to test and the minutiae is not always well documented. How I had understood this, and implemented it, is 1) In serverful mode, when you have horizontally scaling servers and are using SignalR service, a random machine will be picked for where to call Hub.OnConnectedAsync. (Ie not all machines will get the call, and whatever machine gets the call, it won't change over the course of the connection). 2) On all servers, IHubContext will be able to send messages to all clients, even where the context is on another machine, as long as the connection id is known (as the messages will actually go through the service).

Are these assumptions correct? As I said it's hard to test the scaling of the SignalR service and I want to make sure if there's some misconceptions that I catch them now.

Thanks again @davidfowl for all your help!

davidfowl commented 4 years ago

Those assumptions are correct and also hold for both SignalR and Azure SignalR. Messages also aren't guaranteed to reach the client (e.g. if the client goes offline).

You can influence the behavior of where the client gets connected using the ServerStickyMode option.

Tommigun1980 commented 4 years ago

Those assumptions are correct and also hold for both SignalR and Azure SignalR. Messages also aren't guaranteed to reach the client (e.g. if the client goes offline).

You can influence the behavior of where the client gets connected using the ServerStickyMode option.

Thank you again.

One final question (I promise!) - I need to know if a user is connected at the time when trying to send a message. If he's not I would like to send a push instead. There seems to be no API for this and no return values from the RPCs (presumably to avoid additional data transfers), so I guess this is tangentially related to the whole Abort() thing. Is there any mode, API or anything, that could give this information when scaling out with the service? If not I think I'll go down the route you kinda suggested earlier, with another server where hubs notify of and can poll for connection status.

In the end my use case is really simple - I want to send a push if a client is offline, else make an RPC notification call.

Thanks a bunch for all your answers, I really appreciate it!

khalidhex commented 4 years ago

Sorry if this isn't relevant ... but is there is equivalent to this in SignalR Core version GlobalHost.DependencyResolver.Resolve<ITransportHeartbeat>().GetConnections().First(c => c.ConnectionId == "YourId").Disconnect();

davidfowl commented 4 years ago

There's no equivalent, you need to keep track of connections

khalidhex commented 4 years ago

Right now I'm keeping track of connections and everything works fine ... but this scenario is bugging me