1. 程式人生 > >SignalR訊息傳送後的返回值(Return value in SignalR)

SignalR訊息傳送後的返回值(Return value in SignalR)

Return Values

On question during my lecture about SignalR was, how return values can be achieved. Here we have to look at two cases: first, if the client calls a server method and second, if the server calls client methods.

Client calling Server Method

To call a server method that returns a value, you can use an overload of the Invoke

method for .NET clients. Please see the following definition of this method. This method takes a generic parameter that defines the type of the return value. As you can see in the method definition, this method returns a Task of T. So this method invokes the server method asynchronously and when the server method finished, it returns the return value that is of type T.

// Summary:
//     Executes a method on the server side hub asynchronously.
//
// Parameters:
//   method:
//     The name of the method.
//
//   args:
//     The arguments
//
// Type parameters:
//   T:
//     The type of result returned from the hub.
//
// Returns:
//     A task that represents when invocation returned.
Task<T> Invoke<T>(
string method, params object[] args);

For the example I used in the lecture, I simply return a boolean values that indicates whether the operation was successful or not:

bool success = await _proxy.Invoke<bool>("SaveAccountData", accountId, values);

For JavaScript clients you can define a function that is called when the server method returns:

proxy.server.saveAccountData().done(function (success) 
{
    alert(success);
});

Server calling Client Method

First I want to say: it is not possible to get return values from methods called at the client. One can argument, that a strong-typed hub can be used where the used interface defines some methods that return values. Consider the code below.

public interface IPlanningGridClient
{
    bool AccountDataChanged(string accountId, decimal[] values);
}

public class PlanningGridHub Hub<IPlanningGridClient>
{
    public void SaveAccountData(string accountId, decimal[] values)
    {
        var result = Clients.Caller.AccountDataChanged(accountId, values);
    }
}

This code compiles pretty good, but if you execute the code, you’ll run into an exception. The screenshot below shows that exception: The return value of a client method must be void or of type Task.

Exception on Return Value

Exception on Return Value

So let’s go one step further and have a look into the source code of SignalR. SignalR uses a very flexible, extensible and modular pipeline under the hood. The hub pipeline. The hub pipeline consists of various modules through which are messages are passed. In both directions. A pipeline module implements the interface IHubPipelineModule. This interface defines methods for incoming and outgoing messages, as well as some methods for connection-related tasks such as connecting, reconnecting and so on. But let’s have a look at the method that is defined for calling methods at the client:BuildOutgoing(). The code below shows the definition.

/// <summary>
/// An <see cref="IHubPipelineModule"/> can intercept and customize various stages of hub processing such as connecting,
/// reconnecting, disconnecting, invoking server-side hub methods, invoking client-side hub methods, authorizing hub
/// clients and rejoining hub groups.
/// Modules can be be activated by calling <see cref="IHubPipeline.AddModule"/>.
/// The combined modules added to the <see cref="IHubPipeline" /> are invoked via the <see cref="IHubPipelineInvoker"/>
/// interface.
/// </summary>
public interface IHubPipelineModule
{
    /// <summary>
    /// Wraps a function that invokes a client-side hub method.
    /// </summary>
    /// <param name="send">A function that invokes a client-side hub method.</param>
    /// <returns>A wrapped function that invokes a client-side hub method.</returns>
    Func<IHubOutgoingInvokerContextTask> BuildOutgoing(Func<IHubOutgoingInvokerContext, Task> send);

    ...
}

You can see that this method gets a function as an input parameter and returns another function with the same generic parameters. With that definition, the pipeline is build. Messages will be passed from one pipeline module to the next module and the implementation of each function works on the message. The return type of each function is Task. No place for returns values that could be typed as the generic type in Task of Tor something like that. The return type Task is only to get the pipeline working asynchronous. And that corresponds with the exception above, that the return type of client-side methods must be void or Task.

Let’s have another look at the client side. At clients, you can use the On-method to “listen” for method calls that come from the server. Consider the following code from my demo project, that implements the AccountDataChanged-method that is called from the server.

_proxy.On<stringdecimal[]>("AccountDataChanged", (accountId, values) =>
{
    var row = new AccountData()
    {
        AccountId = accountId,
        Jan = values[0], Feb = values[1], Mar = values[2], Apr = values[3], May = values[4], Jun = values[5],
        Jul = values[6], Aug = values[7], Sep = values[8], Oct = values[9], Nov = values[10], Dec = values[11]
    };
 
    Dispatcher.Invoke(() => AccountDataRows.Add(row));
});

The On-method takes a string value that represents the client-side method name and a Lambda-expression that acts as the delegate that is called when the server calls the client method. The Lambda-expression takes two parameters that are defined by the generic parameters of the On-method. This parameters represents the signature of the client-side method. You can see that these are input parameters (please see the server code above). The Lambda-expression represents an Action. And actions do not have return types. Below you can see all overloads of the On-method taken from the SignalR source code. These methods are defined as extension methods on IHubProxy. Hub proxies are used for communication with the server at the client side. There are actions only. So the client side cannot implement methods the return any values.

public static classHubProxyExtensions
{
    public static T GetValue<T>(this IHubProxy proxy, string name);
    public static IObservable<IList<JToken>> Observe(this IHubProxy proxy, string eventName);
    public static IDisposable On(this IHubProxy proxy, string eventName, [Dynamic(new[] { falsetrue })]Action<dynamic> onData);
    public static IDisposable On(this IHubProxy proxy, string eventName, Action onData);
    public static IDisposable On<T>(this IHubProxy proxy, string eventName, Action<T> onData);
    public static IDisposable On<T1T2>(this IHubProxy proxy, string eventName, Action<T1T2> onData);
    public static IDisposable On<T1T2T3>(this IHubProxy proxy, string eventName, Action<T1T2T3> onData);
    public static IDisposable On<T1T2T3T4>(this IHubProxy proxy, string eventName, Action<T1T2T3T4> onData);
    public static IDisposable On<T1T2T3T4T5>(this IHubProxy proxy, string eventName, Action<T1T2T3T4T5> onData);
    public static IDisposable On<T1T2T3T4T5T6>(this IHubProxy proxy, string eventName, Action<T1T2T3T4T5T6> onData);
    public static IDisposable On<T1T2T3T4T5T6T7>(this IHubProxy proxy, string eventName, Action<T1T2T3T4T5T6T7> onData);
}

So far about the internals of SignalR. Now, the questions is, why did the SignalR team choose this framework design approach? In my opinion, they had simply no other choice. Please let me explain why they choose this design and why SignalR does not support method return values on the client-side. Better, I try to explain, because maybe there are other reasons and I did not got them. But here is my guess:

SignalR is a framework for real-time web communication. Therefore, the framework encapsulates underlying communication protocols and technologies. The user of the framework does not need to handle technical problems on transport layer. Instead, he can concentrate on his application. SignalR uses newest web communication standards under the hood and the development team around SignalR will maintain them. So, if new standards evolve some day, SignalR will cover it and applications using SignalR do not need to change their app implementations. As far as the API of SignalR does not change, applications do not need to be changed. They simply run on a newer version of SignalR.

To answer the question above, we have to look at the underlying protocols that are used in SignalR. Currently there are 4 technologies: Web Sockets, Server Sent Events (SSE), Long Polling and Forever Frame. The framework automatically choose the best technology when you use it depending on the operating system, browser, .Net Framework version and so on. It is completely transparent for the application that uses SignalR. The application uses the same API – always. To ensure that the SignalR API works always in the same way, the framework can only use the most common subset of features and limitations that are given by all the underlying technologies. And that is the crux of the matter (from my point of view).

If you look e.g. at long polling. When long polling is used, the client sends a request to the server. Then the server waits until new information is available and then returns with the response to the client. After the response arrived at the client, the client starts a new request (to simulate a persistent connection). So if the client sends a request to the server (and with that, calls a method at the server), the server can process the request (and execute the method) and send back the response that contains the return value from the method. The server can only call a client method within a response. After the response, there is no way to send a return value to the server within the same context. Of course, the client can send a next request that contains the return value from the previous method call, but what if the client has been shut down? No reliable answer/return value will arrive at the server.

The same is valid for Forever Frame that is proprietary to Microsoft. The client sends one request after the other to “establish” a persistent connection. So it’s similar to long polling.

Let’s have a look at Server Send Events (SSE). SSE are designed to have one-way communication – from the server to the client. Therefore, the client sends a request and opens a connection to the server. Then, the server sends events to the client. This event stream is one-way and read-only for the client. There is no chance for the client to send “answers” (and so return values) to the server.

Web Sockets on the other side are designed to use for two-way communication. But as stated above, SignalR uses the most common subset of features of all underlying technologies. And so the way back to the server with Web Sockets cannot be used by the framework. It would run in some environments but not in all. Microsoft could implement some workarounds, e.g. the client could send the return value in a second request. But then the framework has to deal with broken connections etc. a little bit more. So there would be a higher effort to gain reliability. Further, multiple client calls, as they are send over the MultipleSignalProxy class, require management of multiple return values. Return values must be identifiable by their clients they are coming from. So there is a lot of work that has to be done.

參考資料