다음을 통해 공유


Abusing SignalR… doing good

Alsalam alikom wa ra7mat Allah wa barakatoh (i.e. May peace be upon you)

A couple of days ago, a colleague and myself got together for a few hours to hack something for fun. The nature of the hack is beside the point. What’s interesting to this post is that, for the first time, I got introduced to SignalR [Official Site]. From their website:

ASP.NET SignalR is a new library for ASP.NET developers that makes it incredibly simple to add real-time web functionality to your applications. What is "real-time web" functionality? It's the ability to have your server-side code push content to the connected clients as it happens, in real-time.

I’ve heard about the library and the technology behind (Web Sockets) before but this was the first time to experience it and I’ve to say, I love it!

The very basic scenario it addresses is this: traditionally, when you browse to a page, the only time the server can influence what’s render on the client side is during that page browse. Any additional content will have to be served upon request. To design a chat web application for example, your client code (javascript) will have to keep polling every few seconds to check if new data is available. WebSockets came to say this is ridiculous, let’s have a persisted connection between client and server to let the server send data whenever it sees appropriate. This made it more efficient (no polling) and faster to deliver near real-time information.

Now, that’s of itself IS awesome –I thought- but wouldn’t it be cooler if the server can ask the client for information too not just send notifications/data? sure it would!

For the purpose of keeping this post focused, I’ll assume minimum SignalR knowledge. If you think you don’t have that, I would strongly advise you to go through the walkthroughs here (https://github.com/SignalR/SignalR/wiki).

Imagine we want to implement a GetFiles method. The purpose of this method is to get a list of files that exist on the client machine.

I start by creating a static method in the hub class (If you don’t know what Hub is, I strongly advise you to go through its walkthrough on the wiki link above).

    1:  public class FilesHub : Hub
    2:  {
    3:      public static Task<IEnumerable<string>> GetFiles()
    4:      {
    5:      }
    6:      
    7:  }

A couple of notes:

  1. The method is static, because it’ll be called from anywhere on the server side, not necessarily as part of a response to a SignalR request.
  2. The return type is Task because we will need some waiting mechanism to receive the response. If you don’t know what Task is, take a look at this (https://msdn.microsoft.com/en-us/library/vstudio/hh191443.aspx)

Now, the basic idea is, GetFiles will call BeginGetFiles on some client, as part of that call, the client can not return data. The client will then compute the response (list of file paths in this case) and then call EndGetFiles on the server side. This call will trigger the finish action for the async method. Sounds cryptic? let’s look at the code.

Server side

    1:  public class FilesHub : Hub
    2:  {
    3:      private static Dictionary<string, TaskCompletionSource<IEnumerable<PersistenceObject>>> _getFilesTasks 
    4:          = new Dictionary<string, TaskCompletionSource<IEnumerable<PersistenceObject>>>();
    5:   
    6:      public static Task<IEnumerable<string>> GetFiles()
    7:      {
    8:          dynamic client = ClientLoadBalancer.Local.GetClient<FilesHub>();
    9:          TaskCompletionSource<IEnumerable<string>> tcs = new TaskCompletionSource<IEnumerable<string>>();
   10:          string taskId = Guid.NewGuid().ToString();
   11:          _getFilesTasks[taskId] = tcs;
   12:   
   13:          try
   14:          {
   15:              client.BeginGetFiles(taskId);
   16:          }
   17:          catch (Exception ex)
   18:          {
   19:              tcs.TrySetException(ex);
   20:              _getFilesTasks.Remove(taskId);
   21:          }
   22:   
   23:          if (tcs.Task.Exception != null)
   24:          {
   25:              throw tcs.Task.Exception;
   26:          }
   27:   
   28:          return tcs.Task;
   29:      }
   30:   
   31:      public void EndGetFiles(string operationId, IEnumerable<string> result)
   32:      {
   33:          TaskCompletionSource<IEnumerable<string>> tocall;
   34:          if (_getFilesTasks.TryGetValue(operationId, out tocall))
   35:          {
   36:              tocall.TrySetResult(result);
   37:          }
   38:      }
   39:  }

 

Client side

    1:  hub.On<string>("BeginGetFiles", (opId) =>
    2:  {
    3:      Console.WriteLine("BeginGetFiles - opId: " + opId);
    4:      ThreadPool.QueueUserWorkItem(new WaitCallback((state) =>
    5:      {
    6:          IEnumerable<string> result = null; // Calculate result here
    7:          Console.WriteLine("Calling EndGetFiles - opId:" + opId);
    8:          hub.Invoke<string>("EndGetFiles", opId, result).Wait();
    9:          Console.WriteLine("Called EndGetFiles - opId:" + opId);
   10:      }));
   11:  });

Now you can just call it like this

    1:  IEnumerable<string> files = await FilesHub.GetFiles();

Clean? yeah, that’s what I thought. In my opinion, SignalR should provide such functionality without the above hack. However, it sounds this would be abusing the library. The main goal of the library is to send notifications (signals :)) from server to client in a pushing mechanism. Even though I agree this would abuse the library, I think it’s still a good trick and might come handy at times.

Till next time,