Tuesday, June 23, 2020

Server-sent event (SSE) with ASHX (.Net framework 4.6)

It took me a while to solve the Server-sent event (SSE) implementation in .Net framework 4.6.

Notes:

- This new ASHX should not access any session data. Otherwise, the user access will be "blocked". To solve this, you might have to store the user session data in a database or in memory (using static Dictionary or something like that).

- If the user open multiple tabs, the server must force closing the earlier connections. This will reduce the use of server & browser resources.

- Creating more EventSource might have side effect in the browser as it will reduce the performance in downloading the CSS, images, etc. If you need more EventSource object, you may have to create more sub-domains such as WWW1, WWW2,...

- Don't forget that any request to ASP.NET will consume a shared thread which might affect C# performance. The server connection has a limit too.

- It's best to use HTTP/2 for SSE which has better performance as compared to HTTP/1.

Here's the server side coding - add a new handler - notify_me_endlessly.ashx.


<%@ WebHandler Language="C#" Class="notify_me_endlessly" %>

using System;
using System.Web;
using System.Threading;
using System.Threading.Tasks;

public class notify_me_endlessly : IHttpHandler
{

    /// <summary>
    /// NOTES:
    /// - use HTTP/2 (with SSL/TLS) to reduce the request payload.
    /// Otherwise, the network might be flooded.
    ///
    /// </summary>
    /// <param name="context"></param>
    public void ProcessRequest(HttpContext context)
    {
        // to indicate the content type is for EventSource() .
        context.Response.ContentType = "text/event-stream";

        // hold the connection in a separate thread.
        // NOTES: this might overload the server with client connections!!

        Task t = Task.Run(() =>
        {

            // run endlessly until the client close the connection      //<<=======
            while (true)
            {

                try
                {
                    //this is IMPORTANT. The message
                    //sent to the caller must be "data: xxx".
                    context.Response.Write(string.Format("data: {0}\n\n",
                    DateTime.Now.ToString("d/MMM/yy @ HH:mm:ss.fff")));

                    System.Diagnostics.Debug.WriteLine(string.Format("{0}-sending data to browser..",
                        DateTime.Now.ToString("d/MMM/yy @ HH:mm:ss.fff")));

                    context.Response.Flush();

                    Thread.Sleep(2000);
                }
                catch //(Exception x)
                {
                    System.Diagnostics.Debug.WriteLine(string.Format("{0}-failed sending data to client",
                          DateTime.Now.ToString("d/MMM/yy @ HH:mm:ss.fff")));
                    break;
                }
            }

        });

        // wait until end of the TASK.           //<<=======
        t.Wait();

        // disconnect the caller - this is optional and the connection
        // will be closed upon exiting this proc.       
        //context.Response.Close();

        System.Diagnostics.Debug.WriteLine(string.Format("{0}-client has quit",
                                                   DateTime.Now.ToString("d/MMM/yy @ HH:mm:ss.fff")));

    }

    public bool IsReusable
    {
        get
        {
            return false;
        }
    }

}


Here's the front end which consumes the SSE:


<!DOCTYPE html>
<html>
<head>
    <title></title>
    <meta charset="utf-8" />
</head>
<body>

    <h1>
        Msg from server
    </h1>

    <input type="button" id="btnListen" value="Start Listening" onclick="startListening(); return false;" />
    <input type="button" id="btnListen" value="Stop Listening" onclick="stopListening(); return false;" />
    <ul id="my-list"></ul>


    <script type="text/javascript">
        var sse = null;
        var mylist = document.getElementById('my-list');

        function startListening() {

            //-------------------------------
            var newElement = document.createElement("li");
            newElement.textContent = "message: connecting..";
            mylist.appendChild(newElement);

            //reset before use.
            if (sse != null) {
                sse.close();
                sse = null;
            }

            sse = new EventSource('notify_me_endlessly.ashx');
          

            //-------------------------------
            sse.addEventListener("open", function (event) {
                var newElement = document.createElement("li");
                newElement.textContent = "message: connected";
                mylist.appendChild(newElement);

                console.log('onopen=', event);

            }, false);

            sse.addEventListener("error", function (event) {
                var newElement = document.createElement("li");

                if (sse.readyState == 2) {
                    newElement.textContent = "ERR message: connection closed";
                }
                else if (sse.readyState == 1) {
                    newElement.textContent = "ERR message: open";
                }
                else if (sse.readyState == 0) {
                    newElement.textContent = "ERR message: connecting..";
                }
                else {
                    newElement.textContent = "ERR message: failed-";
                }

                mylist.appendChild(newElement);

                //22.Jun.20,lhw-you might have to call sse.close() manually upon any error
                // and reconnect after x seconds using setInterval() - in this case, you
                // will have full control on the reconnecting behavior (since all
                // browser has slightly different implementation).

                console.log('onerror=', event);

            }, false);


            sse.addEventListener("message", function (e) {
                var newElement = document.createElement("li");
                newElement.textContent = "message: " + e.data;
                mylist.appendChild(newElement);

                console.log('onmessage=', event);

            }, false);

        }

        function stopListening() {

            // stop listening to the server message.
            if (sse) {

                // this proc inform the browser to stop reconnecting the server
                // in case of connection failure.
                sse.close();
                sse = null;

                var newElement = document.createElement("li");
                newElement.textContent = "message: stopped by user";
                mylist.appendChild(newElement);
               
            }

        }

    </script>

</body>
</html>