MVC: returning multiple results on stream connection to implement HTML5 SSE
- by eddo
I am trying to set up a lightweight HTML5 Server-Sent Event implementation on my MVC 4 Web, without using one of the libraries available to implement sockets and similars.
The lightweight approach I am trying is:
Client side:
EventSource (or jquery.eventsource for IE)
Server side:
long polling with AsynchController (sorry for dropping here the raw test code but just to give an idea)
public class HTML5testAsyncController : AsyncController
{
private static int curIdx = 0;
private static BlockingCollection<string> _data = new BlockingCollection<string>();
static HTML5testAsyncController()
{
addItems(10);
}
//adds some test messages
static void addItems(int howMany)
{
_data.Add("started");
for (int i = 0; i < howMany; i++)
{
_data.Add("HTML5 item" + (curIdx++).ToString());
} _data.Add("ended");
}
// here comes the async action, 'Simple'
public void SimpleAsync()
{
AsyncManager.OutstandingOperations.Increment();
Task.Factory.StartNew(() =>
{
var result = string.Empty; var sb = new StringBuilder();
string serializedObject = null;
//wait up to 40 secs that a message arrives
if (_data.TryTake(out result, TimeSpan.FromMilliseconds(40000)))
{
JavaScriptSerializer ser = new JavaScriptSerializer();
serializedObject = ser.Serialize(new { item = result, message = "MSG content" });
sb.AppendFormat("data: {0}\n\n", serializedObject);
}
AsyncManager.Parameters["serializedObject"] = serializedObject;
AsyncManager.OutstandingOperations.Decrement();
});
}
// callback which returns the results on the stream
public ActionResult SimpleCompleted(string serializedObject)
{ ServerSentEventResult sar = new ServerSentEventResult();
sar.Content = () => { return serializedObject; };
return sar;
}
//pushes the data on the stream in a format conforming HTML5 SSE
public class ServerSentEventResult : ActionResult
{
public ServerSentEventResult() { }
public delegate string GetContent();
public GetContent Content { get; set; }
public int Version { get; set; }
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
} if (this.Content != null)
{
HttpResponseBase response = context.HttpContext.Response;
// this is the content type required by chrome 6 for server sent events
response.ContentType = "text/event-stream";
response.BufferOutput = false; // this is important because chrome fails with a "failed to load resource" error if the server attempts to put the char set after the content type
response.Charset = null;
string[] newStrings = context.HttpContext.Request.Headers.GetValues("Last-Event-ID");
if (newStrings == null || newStrings[0] != this.Version.ToString())
{
string value = this.Content();
response.Write(string.Format("data:{0}\n\n", value));
//response.Write(string.Format("id:{0}\n", this.Version));
}
else
{
response.Write("");
}
}
}
}
}
The problem is on the server side as there is still a big gap between the expected result and what's actually going on.
Expected result:
EventSource opens a stream connection to the server,
the server keeps it open for a safe time (say, 2 minutes) so that I am protected from thread leaking from dead clients,
as new message events are received by the server (and enqueued to a thread safe collection such as BlockingCollection) they are pushed in the open stream to the client:
message 1 received at T+0ms, pushed to the client at T+x
message 2 received at T+200ms, pushed to the client at T+x+200ms
Actual behaviour:
EventSource opens a stream connection to the server,
the server keeps it open until a message event arrives (thanks to long polling)
once a message is received, MVC pushes the message and closes the connection.
EventSource has to reopen the connection and this happens after a couple of seconds.
message 1 received at T+0ms, pushed to the client at T+x
message 2 received at T+200ms, pushed to the client at T+x+3200ms
This is not OK as it defeats the purpose of using SSE as the clients start again reconnecting as in normal polling and message delivery gets delayed.
Now, the question:
is there a native way to keep the connection open after sending the first message and sending further messages on the same connection?