Reading through here and looking at the example here:
I am trying to understand actually what the WebSocketAccept actually does. I know that WebSocketAccept is:
using WebSocketAccept =
Action
<
IDictionary<string, object>, // WebSocket Accept parameters
Func // WebSocketFunc callback
<
IDictionary<string, object>, // WebSocket environment
Task // Complete
>
>;
and is used in this manner:
public void Configuration(IAppBuilder app)
{
app.Use(UpgradeToWebSockets);
app.UseWelcomePage();
}
// Run once per request
private Task UpgradeToWebSockets(IOwinContext context, Func<Task> next)
{
WebSocketAccept accept = context.Get<WebSocketAccept>("websocket.Accept");
if (accept == null)
{
// Not a websocket request
return next();
}
accept(null, WebSocketEcho);
return Task.FromResult<object>(null);
}
So what is accept() actuallyt doing? Is it calling the Func<> property of the WebSocketAccept and a method WebSocketEcho is defined? WebSocketEcho is defined as:
private async Task WebSocketEcho(IDictionary<string, object> websocketContext)
So where does the websocketContext come from? What if we wanted to pass this further down the pipeline once we have identified it is a web socket request?
What is WebSocketAccept?
WebSocketAccept is a using alias
Example:
...
using Foo.Bar
using MyBar = Fee.Bar
...
Here we are using Bar from 2 different namespaces, but we alias the 2nd one with 'MyBar' so we can differentiate between the two.
Why use an alias as WebSocketAccept?
The alias in this case is just a convenience, so that you don't have to type the whole thing, which means that instead of writing the whole name when using it, you can use the alias instead.
Understanding WebSocketAccept
If we look closer we see that the type is:
Action<A, B>
This means that it is essentially a function that does not return and takes 2 arguments, in C# lambda:
(A, B) => { }
We see that the 1st argument (A) is: IDictionary<string, object>, which is also known as the Owin environment.
The 2nd argument is (B) is: Func<C, D> which means that it is a function that takes a C and returns a D. In C# lambda:
(C) => { return D; }
We then need to dive into the 1st argument (C) of the 2nd argument (B). And we see that it takes an Owin environment and returns a Task.
What is accept?
accept tries to extract the parameters from the IOwinContext and map them to the WebSocketAccept type.
If it can't extract them it is null and we proceed to the next middleware.
Otherwise it was a websocket request and we call the function that takes 2 parameters (WebSocketAccept), as we discussed above (Action<A, B>).
The first parameter being an ordinary dictionary, which contains the websocket accept parameters.
The second parameter being a function that takes a dictionary and returns a task.
This function is called by someone else, what the code does, is pass the callback function along to the caller.
The caller then calls the function with the correct argument. Because the caller knows the signature of the function. The function is called AFTER accepting the websocket connection request. Hence the comment callback.
What if we wanted to pass this further down the pipeline once we have identified it is a web socket request?
Well in the example, the callback function is WebSocketEcho but essentially you could pass in any function, that satisfies the function signature of:
Task MyCallbackFunction(IDictionary<string, object> context)
{
// Do something
return Task.FromResult(0);
}
The takeaway is that you don't call the function, the function is called for you. You specify that after negotiating the web socket request connection, you decide what happens.
The WebSocketEcho function is called once for every client, and loops until the client chooses to close the connection. Meanwhile it echo back whatever it receives.
Disclaimer: I too am just trying to wrap my head around web sockets and owin, but I wanted to share my findings for posterity, since no one had answered your question. I welcome any corrections.
EDIT
I noticed with my own experiment, that if you return from the callback function that the websocketContext connection will be Aborted. Meaning you can't send/receive messages on the connection if you pass the websocketContext around after ending the callback.
UPDATE
Last time I tried to use this on a Windows 2008 R2 IIS 7.5 server I could not get the websocket to work. Then according to this: https://stackoverflow.com/a/14130152/1640121 - The IIS 7.5 server does not support websockets.
Which means that if your application is hosted in IIS 7.5 it would not be able to have websockets.
Then I thought about a possible solution:
Use a separate application, e.g. a service program (outside the IIS) that handles the websocket requests.
Use a reverse proxy to map the request to the service application
This felt too cumbersome for me, which made me put aside implementing a websocket for now...
Related
I'm using the ASP.NET Core 6 minimal APIs to build an API. After extracting anonymous/lambda handlers from MapGet into their own methods, some endpoints act erroneously by always returning a 200 (OK) response with an empty body.
Not sure whether I've missed some gotcha regarding how async works in C# or whether this is a bug with ASP.NET Core.
Reproducible example
Following is a minimal example where endpoints /a and /b execute similar methods, but one of them never responds as expected. To run the example, just copy the code into a new project created with dotnet new web.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
async Task<int> longRunningValueGenerator() {
await Task.Delay(1000);
return 4; // chosen by a fair dice roll
// guaranteed to be random
}
async Task<IResult> getValueA(HttpContext http) {
if(http.Request.Query["secret"] != "test") {
return Results.Unauthorized();
}
var value = await longRunningValueGenerator();
return Results.Ok(new {
Value = value
});
}
app.MapGet("/", () => "get /a or /b with secret=test");
app.MapGet("/a", getValueA);
app.MapGet("/b", async (HttpContext http) => {
if(http.Request.Query["secret"] != "test") {
return Results.Unauthorized();
}
var value = await longRunningValueGenerator();
return Results.Ok(new {
Value = value
});
});
app.Run();
Some observations
This issue starts occurring once the secret check is added. This is just for example, adding different code also triggers this behavior.
It still takes a second (as determined by the delay) for the server to respond if secret is correct, even for getValueA.
/a will never respond with a different status code other than 200, even if secret is incorrect.
Non-async handlers that are separate methods work as expected.
There's a couple of overloads for MapGet:
MapGet(IEndpointRouteBuilder, String, Delegate)
MapGet(IEndpointRouteBuilder, String, RequestDelegate)
The difference between these two signatures is in that last parameter, Delegate vs RequestDelegate. Delegate represents any callable function, but RequestDelegate represents a function with one parameter, HttpContext, that returns a Task.
In your example, the getValueA function works for both of these delegates: it's any callable function and it's a function that takes HttpContext. For reasons beyond my understanding, the compiler picks the RequestDelegate version of MapGet, and effectively throws away the IResult value that it isn't expecting.
There might be a few options for fixing this, but the following solution should work well here. It changes your getValueA signature and takes advantage of parameter-binding for secret:
async Task<IResult> getValueA([FromQuery] string secret)
{
if (secret != "test")
{
return Results.Unauthorized();
}
var value = await longRunningValueGenerator();
return Results.Ok(new
{
Value = value
});
}
This change means that the version of MapGet that takes a RequestDelegate is no longer a candidate, and therefore the Delegate version is called and processed correctly.
This is a known issue with .NET 6 and minimal APIs and it has to do with overload resolution and what gets inferred by default https://github.com/dotnet/aspnetcore/issues/39956.
It has been fixed in .NET 7, we may backport to .NET 6 (you can request a back port on the issue).
I have a button, that when clicked will/should notify the server. The server will then save a value to the database. If all went well, it should return true, otherwise return false.
I instantiate a hub in my view
var signalRhub = $.connection.hubSignalR;
Start the connection:
$.connection.hub.start().done(function () {
$("#submitBut").click(function () {
signalRhub.server.cardAdded();
});
});
Define the function that will be used by the server to return the boolean value:
signalRhub.client.cardAddedRes = function (isSuccess) {
alert("From server: " + isSuccss);
}
My Hub class:
public class HubSignalR : Hub
{
public bool isSuccess = false; <-- Will be set from controller
public void CardAdded()
{
Clients.Caller.CardAddedRes(isSuccess); <-- Notice the isSuccess
}
}
My problem is that the isSuccess value is coming from my controller, that interacts with the model/database.
So I get the error:
Using a Hub instance not created by the HubPipeline is unsupported.
I tried using: GlobalHost.ConnectionManager.GetHubContext<HubSignalR>()
but I can't make it work.
Here is the relevant code in my controller:
private HubSignalR signalR = new HubSignalR(); <-- Field variable
[HttpPost]
public ActionResult AttachCard(Card model, int MemberID)
{
var hub = GlobalHost.ConnectionManager.GetHubContext<HubSignalR>();
...
//We saved to the database, so we call the client function with bool = true
hub.Clients.All.CardAdded(true); <-- Actually I want to send to one client, NOT ALL
//Something like hub.Clients.Caller.CardAdded();
}
I'm forced to make the isSuccess field in my: HubSignalR class, since I need to return that as the parameter from my controller. But when the button is clicked, this value has not yet been set (I think).
I can see from the debugger, that I do reach: signalRhub.server.cardAdded();
But the server never responds, so I don't reach this function:
signalRhub.client.cardAddedRes = function (isSuccess) {
alert("From server: " + isSuccss);
}
I don't really get to call the CardAdded() method from my controller, cus of the GlobalHost.ConnectionManager.GetHubContext. But you can see
If you got a nicer solution than what I'm trying to do, please tell. I'm total new with SignalR and fairly new with ASP.net MVC
Hope I got the problem right: you want to set isSuccess in controller and send it to specific client, but don't reach specific client from outside the hub?
As to "how":
You should find a way to identify your client, i.e. implement some kind of authentication. More on this here: Authentication in .net core When clients connect to SignalR, they get a connection ID. You can map the connection ID to real client identity. More info here: Mapping clients to connections
Than in you server method:
Get authenticated client identity
Get signalR hub context
Map client identity to existing signalR connection ID
Send message to that signalR connection
As I can see from you code you might be working on some personal-use/exploratory project and probably not interested in implementing authentication/don't care about security. You could get signalR connection ID in your client after connecting through $.connection.hub.id. Than you could send this ID to the server method as parameter or header. This is by no way should be used in production environment, as you would trust the client who he is, and pass parameters that are not strictly needed by your method.
As to "why":
In fact I don't think you need signalR for your use case. You call a server method, it saves to DB and returns you OK or not OK, client is happy. No need to pass it through signalR.
You do need signalR e.g. when:
- Same client is logged in on several devices and wants to get updates if changes were made on one of them
- Client works on something and another one changes same data. You want to inform the first client.
- Inform your client of an event that was not triggered by him (new notification)
In all this cases you have some kind of authentication and sending signalR message to the right client is not a problem.
Answer to comment below
I have little experience with ajax, i guess it might work. Another idea, if you want to avoid authentication, is a subscription model with SignalR.
You have to find out which specific resources you have, let's say "game" in your case, which have IDs. Than clients, interested in that particular resource, shall subscribe to changes.
All participants of a particular card game instance shall call a hub method defined like this:
public async Task SubscribeToGameChanges(long id)
{
await this.Groups.AddToGroupAsync(this.Context.ConnectionId, Helper.GetGameGroupName(id));
}
public static class Helper
{
public static string GetGameGroupName(long id)
{
return $"Game_{id}";
}
}
Than call it from client. Note the AddToGroupAsync. SignalR shall create a group with given name and add a client to it. If group exists, it will just add another client. So you have a group per game instance with a list of interested clients (players).
Now when a change happens to the game, you notify all your clients by calling from your controller on hub context:
await hubContext.Clients.Groups(Helper.GetGameGroupName(id)).SendAsync("myNotifyMethod", myParameters);
You could pack all your changes into parameters, or just inform the clients that the state of the game (or whatever other resource) has changed and clients shall requery the state through normal API call.
Also I noticed you use GlobalHost in your code. My code samples are for .net core SignalR version and might slightly differ in your case. See here on differences between .net core and full .net SignalR.
I am trying to send a message to a SignalR client, but it should not broadcast to all, I went through many article about clientId and groups, but there are no straightforward examples or points given. My client code is below:
proxy: null,
//Getting the connection object
connection = .hubConnection('url')
//Creating proxy
this.proxy = connection.createHubProxy('SignalRServerHub');
this.proxy.on('displayStatus', function (a) {
SignalRMessageTableService.getDataFromServer()
});
//Starting connection
connection.start()
//we are able to capture connection.id here
//under .done function but not sure how to use
Please let me know step by step solution or any article reference so I can understand it easily.
To respond to a specific client with connectionId you can do something like this:
var connectionId = Context.ConnectionId;
Clients.Client(connectionId).someMethod(resultValue);
Or to respond only to the caller:
Clients.Caller.someMethod(resultValue);
These calls are made from within public methods in your hub class, and do the same.
EDIT:
It looks like the connection.start() should be expanded a bit. You do not need to handle the connectionId, if the server are going to return data or callback to an event at the caller. This will be done by the server, and handled by the methods posted above.
Try changing the connection.start() line to something like this:
connection.start().done(function() {
$('#someButton').click(function () {
var param1 = $('#someField').val();
var param2 = $('#someField2').val();
this.proxy.invoke('myHubMethod', param1, param2, paramN);
});
});
Do the neccessary changes to apply this to yout implementation. This code is based on the one you'll find in the documentation. Since it looks like you don't use the generated proxy, we use the invoke method on the proxy to tell which method, and which parameters we are going to send. This also wires up an event to a button with id=someButton, which will fire the myHubMethod on the hub when clicked.
The alternative would be something like (with generated proxy):
this.proxy.server.myHubMethod(param1, param2, paramN);
You should take a look on the ASP.NET SignalR Hubs API Guide - JavaScript Client
If you want to send message to a specific user, you can use the new method in version 2.0
Clients.User(userId).send(message);
by default, the user id used is the IPrincipal.Identity.Name. If you want to override the user id mechanism with your own mapping, refer to this reply - https://stackoverflow.com/a/21355406/2489038
I am trying to write a host/client style system with WCF and I currently want to use the wsHttpBinding but I was reading up about something else (the isOneWay bool) when I cam across this line:
'HTTP can't be used for callbacks and therefore you can't use callbacks over BasicHttpBinding or WSHttpBinding'
Most of my methods will return values, for example the client would call a method which would send back specific data from the server depending on the method called. Like this, with a return line in:
public string SayHello(string name)
{
Console.WriteLine(">>> SayHello has been called");
return string.Format("Hello, {0}!", name);
}
Would the return line make it a callback so would I have to use a different binding? Or is a callback only when the client sends a 'message' to the server and the server sends a 'message' back? In my code it is just calling the method which returns data so would that not be classed as a callback and I can continue to use the wsHttpBinding? Thanks
You definitely don't need wsDualHttpBinding to return values. BasicHttpBinding or even better WSHttpBinding is what you need
wsDualHttpBinding is used to actually call the client from the server (callbacks). It's something which isn't widely used in the web world because it has limitations and there are better options like WebSockets and SignalR
I am learning how to use AngularJS and SignalR together and I am wondering if I can restart a SignalR connection withot losing the connectionId on the server side. The reason I am asking this has got to do with the client methods that needed to be called serverside. I haven't tried anything out yet and I just was thinking about this situation and what is the best practice solution and I am hoping some could think along or might have the solution and would like to explain it.
For example: I have two angularJS controllers, controller1 and controller2 and two signalR hubs, hub1 and hub2. controller1 is started on opening the website and in the initialisation of controller1 I can bind a function to a client method that needs to be called in hub1 before SignalR is started. This works fine and even after signalR is started I can still bind functions to client methods with the on function even if the signalR is started, although this probably isn't nessecary because I can bind the functions to the client methods before starting the signalR connection.
Next, on a form I got a button and that button is starting another div which has controller2 as ng-controller. In the initialisation of controller2 I want to bind functions to client methods that needs to be called in hub2. But since the signalR connection is already started by controller1, I can't do hub2.client.AMethod = function () { }. I was thinking, would this be possible if I can restart a signalR connection without losing the connectionId on the server side and by doing the restart, also refresh all the client methods bindings? And if not, can I use the on function even if there hasn't been a function binded to client method on hub2 before? Or do I have to bind an empty function to a client method on hub2 as well before I start my signalR connection?
EDIT: I took the time to set up a code example.
I got the 2 hubs: Hub1
[HubName("Hub1")]
public class Hub1 : Hub
{
public void TestMethod1(string test)
{
Clients.All.TestMethod1Hub1("Testing Hub1 method1; " + test);
}
public void TestMethod2(string test)
{
Clients.All.TestMethod2Hub1("Testing Hub1 method2; " + test);
}
}
and hub2:
[HubName("Hub2")]
public class Hub2 : Hub
{
public void TestMethod1(string test)
{
Clients.All.TestMethod1Hub2("Testing Hub2 method1; " + test);
}
public void TestMethod2(string test)
{
Clients.All.TestMethod2Hub2("Testing Hub2 method2; " + test);
}
}
And I got my angularJS controller:
testApp.controller('controller1', ['$scope', 'signalRService', function ($scope, signalRService) {
var self = this;
$scope.logs = [];
self.TestMethod = function(testString) {
$scope.logs.push({ text: testString });
$scope.$apply();
};
$scope.initialize = function() {
signalRService.connection.Hub1.client.TestMethod1Hub1 = self.TestMethod;
//signalRService.connection.Hub2.client.TestMethod1Hub2 = self.TestMethod;
signalRService.initialize();
};
$scope.addHandlers = function () {
//this will call the client method cause it is set before the connection start.
signalRService.connection.Hub1.server.testMethod1("Test 1");
//This is working, so on function isn't required?
signalRService.connection.Hub1.client.TestMethod2Hub1 = self.TestMethod;
signalRService.connection.Hub1.server.testMethod2("Test 2");
//So you don't need the on method (anymore?). (By the way, this is working as well ofcourse)
signalRService.connection.Hub1.on("TestMethod2Hub1", self.TestMethod);
signalRService.connection.Hub1.server.testMethod2("Test 3");
//this doesn't work (obviously?) unless the line in the initalize method is uncommented
signalRService.connection.Hub2.client.TestMethod1Hub2 = self.TestMethod;
signalRService.connection.Hub2.server.testMethod1("Test 4");
//but this doesn't work either. Same: works if line in the initialize method is uncommented
signalRService.connection.Hub2.on("TestMethod1Hub2", self.TestMethod);
signalRService.connection.Hub2.server.testMethod1("Test 5");
//Also, I get the test 4 and test 5 twice, so the event handlers are added, not replaced.
};
}]);
In the example, the binding to the client methods happens much later after the signalR is started (In this case, by pressing the button as example, but in a live example it could be when a user navigates to a different ng-view template and with that starts a different angular controller which also depends on client methods). In the test I see I have to add a client method (dummy or not) to every hub so I can add extra client methods later at a start up of another angular controller. I wonder if this could be done otherwise, so you don't get a long list of binding dummy functions to client methods?
Also, it doesn't seems to be nessecary to use the on function, binding straight away seems to work as well after the connection is started. Perhaps this is changed in SignalR version 2.0.0
Gonna give this a shot, I think I understand what you're asking so I'll provide some guidance:
It's not a good idea to restart a SignalR connection with the intent of maintaining a connection id. Instead track users in your hub via some sort of static concurrent dictionary. This way when a connection is established from a specific user you can associate them with your dictionary version of that user.
Prior to starting a SignalR connection (JavaScript) you MUST have at least 1 client side function bound; this process allows SignalR which hubs you want to subscribe to.
Hope this helps!