Polling Based Message Bus For ASP.NET Web Services

I have a client / server application where the server is an ASP.NET web service and the client is a WinForm application. There will be multiple clients and they will be deployed behind the firewall of the host computer. The firewall will allow outgoing HTTP connections (port 80 for internet traffic). The server will be hosted on the internet. In this situation, the client (WinForm) can initiate conversations with the server (ASP.NET), but the server cannot do the same.

The Problem

The root problem is that HTTP is a request / response protocol where each request is typically closed by a response. No open connections are kept. Because of the firewall, requests can only initiate from the client side. This is shown in the following image:

image

There are protocols for establish bi-directional communication for just this type of situation. Listing them results in alphabet soup: COMET, BOSH, XMPP, and many others. These protocols all try to manipulate the request / response paradigm to allow the server to send data to the client. This results in a very special requirements for the pipeline on the server. Out of the box, the ASP.NET server does not scale well when supporting these protocols. There have been attempts to sort this out, but that is much more complex than the simple message passing that I require.

So how do I achieve a message bus that allows messages to be sent bi-directionally?

Solution

The solution that I will employ here is a polling architecture. In this architecture, the client initiates all communications. In the request call, the client will deliver a message to the server and the response will deliver messages to the client. During one HTTP request / response cycle both the client and server can get messages.

This pattern will be repeated at a rate determined by the required responsiveness. Some scenarios only require a very low polling rate (once per hour or day for example). This might be used to send the day’s aggregate data to the server and check for messages. Moderate polling rates (once per minute to once per day) can be used to report system health data. High polling rates (once per second to once per minute) can be used for data that changes more often.

You need to consider if your server can handle the polling rate you have chosen:

  • If you get into the high polling regime, you need to be sure that your server can handle the load. Imagine every client hitting your server every second. It is possible to quickly tax the server thread pool.
  • Consider the message payload. You can transport lighter payloads faster which allows higher polling rates.
  • Take into account acquisition time for the payload. If it takes 10 seconds to acquire, a polling rate faster than that does not make much sense.

In my particular case, I will implement a dynamic polling rate. The client initially starts off polling at about 15 seconds per poll rate.  When a higher rate is necessary a message is sent to increase the polling rate. So let’s look at the code on the client and server to implement this architecture.

Client Side

Sending and receiving message is initiated on the WinForm client using the following method:

private void PostMessage(BaseMessage message)
{
    BaseMessage[] messages = null;
    try
    {
        messages = _webService.Poll(message);
    }
    catch (Exception ex)
    {
        // Service must be down.
        //
        MessageBox.Show(ex.ToString());
        _pollMode = PollModeOptions.Register;
    }

    // Process commands.
    //
    if (messages != null)
    {
        ProcessMessages(messages);
    }
}

This call takes two parameters. The message parameter is a class that inherits from the BaseMessage class (more on messages later). This is the message that is being sent to the server. The message is sent via a web service call using the _webService object. This object is simple an implementation of the proxy that you achieve by adding a service reference to your WinForm project. This proxy wraps the complexity of calling the web service and parsing the result. The web service call returns an array of messages to the client. These messages are then sent to the ProcessMessages method for processing.

In the above code, you also see a hint of the state machine for the client (more on this application in later blog posts). The client begins in the PollModeOptions.Register state. The state of the client determines what type of message it sends to the server. For instance, in PollModeOptions.Register state the client will send a RegisterMessage to the server.

The ProcessMessages method serves as the post office where messages are sorted and forwarded to the method that processes the specific message type. Here is the code for that:

private void ProcessMessages(IEnumerable<BaseMessage> messages)
{
    string msgInfo = "";
    if (messages != null)
    {
        foreach (BaseMessage message in messages)
        {
            if (message is PollIntervalMessage)
            {
                msgInfo += ProcessPollIntervalMessage(message as PollIntervalMessage);
            }
            else if (message is ThumbsizeMessage)
            {
                msgInfo += ProcessThumbsizeMessage(message as ThumbsizeMessage);
            }
            else if (message is ChatMessage)
            {
                msgInfo += ProcessChatMessage(message as ChatMessage);
            }
            else if (message is PollModeMessage)
            {
                msgInfo = ProcessPollModeMessage(message as PollModeMessage);
            }
        }
    }
    status.Text = "Last Poll: " + DateTime.Now + msgInfo;
}

The meat of this code is a for loop over the messages returned from the web service with an if-else-if filter for the message types. The actual code for the specific message handlers is important  to the application domain (more in later blog posts), but not so important to grasp the concept of this message bus architecture. You can imagine for instance that the ProcessPollIntervalMessage method adjusts the client side polling rate at the request of the server.

The client side code is initiated using a WinForm Timer object with the Interval property set to the polling period. This same client side code could easily be ported to exist in a web page using JavaScript to call the web service and process the returned messages.

Server Side

The server side web service is implemented using the following code:

[WebMethod]
[XmlInclude(typeof(EmptyMessage))]
[XmlInclude(typeof(RegisterMessage))]
[XmlInclude(typeof(ImageDataMessage))]
[XmlInclude(typeof(PollModeMessage))]
[XmlInclude(typeof(PollIntervalMessage))]
[XmlInclude(typeof(ThumbsizeMessage))]
[XmlInclude(typeof(ImageNoChangeMessage))]
[XmlInclude(typeof(ChatMessage))]
public List<BaseMessage> Poll(BaseMessage message)
{
    if (message is EmptyMessage)
    {
        return ProcessEmptyMessage(message as EmptyMessage);
    }
    if (message is RegisterMessage)
    {
        return ProcessRegisterMessage(message as RegisterMessage);
    }
    if (message is ImageDataMessage)
    {
        return ProcessImageDataMessage(message as ImageDataMessage);
    }
    if (message is ImageNoChangeMessage)
    {
        return ProcessImageNoChangeMessage(message as ImageNoChangeMessage);
    }
    if (message is ChatMessage)
    {
        return ProcessChatMessage(message as ChatMessage);
    }
    return ProcessEmptyMessage(message as EmptyMessage);
}

This code resides in a .asmx.cs file and the Poll method is marked with the WebMethod attribute. This exposes this method as a web service. The various XmlInclude attributes ensure the types for the various messages will be serialized / de-serialized over the wire. This code simple filters the incoming message based upon type and passes it to the appropriate method for handling. Again the specific implementation of the message handlers is important to the application domain and not so much to the architecture of this message bus.

Messages

The message format is very important in this architecture. It must at minimum relay an ID for the client that is posting the message. Since there are multiple clients the ID is used to add context to the conversation between the server and the client to allow the server to recognize which client is currently being serviced. The following message base class provides this functionality.

public class BaseMessage
{
    public Guid AgentId { get; set; }
    public DateTime TimeStamp { get; set; }
}

Notice that the BaseMessage class itself is not marked with the Serializable attribute and as such cannot be sent over the wire via the web service. Therefore messages that derive and extend the BaseMessage type are used for communication between the client and server.

For example, each client starts be registering with the server. This process of registering sends the following message from the client to the server:

[Serializable]
public class RegisterMessage : BaseMessage
{
    public string ComputerName { get; set; }
    public string UserInfo { get; set; }
}

This is additional information that can be used to register the client with the service. In response the server can then send configuration messages back to the client. This may include the following message to set the polling interval:

[Serializable]
public class PollIntervalMessage : BaseMessage
{
    public static int DefaultIntervalInMilliseconds = 5000;
    public int IntervalInMilliseconds { get; set; }

    public PollIntervalMessage()
    {
        IntervalInMilliseconds = DefaultIntervalInMilliseconds;
    }

    public PollIntervalMessage(AgentData agent, int intervalInMilliseconds)
    {
        AgentId = agent.AgentId;
        TimeStamp = DateTime.Now;
        IntervalInMilliseconds = intervalInMilliseconds;
    }
}

There may be times when there is no new messages to send between the client and the server. The polling still occurs, so some message must be sent. In this case the client (or server) will send the following:

[Serializable]
public class EmptyMessage : BaseMessage
{
    public EmptyMessage() { }

    public EmptyMessage(AgentData agent)
    {
        AgentId = agent.AgentId;
        TimeStamp = DateTime.Now;
    }
}

Summary

Sending messages between a WebForm client and an ASP.NET web service is easily done using a polling architecture. This architecture trades responsiveness for simplicity. Using strongly typed messages allows the client & server to filter the message and forward to a handler specific for that type.

This architecture can be used by clients to publish data to a server. This data can then be shared with other clients on their next poll as part of their returned message array.

I will be using this architecture in an upcoming project that I will blog about soon. As always, I appreciate any comments.

Comments
  1. Manikandan B
  2. gunasekar

Leave a Reply

Your email address will not be published. Required fields are marked *

*