Server-Sent Events (SSE) with Classic ASP: Implementing LastEventID and supporting multiple listeners

573 views Asked by At

Apparently, because Internet Explorer never supported Server-Sent Events, very few examples written for classic ASP are available. Most published examples use NodeJS or PHP, but articles on this site and others state or imply that any middleware language can be used.

I have gotten the barebones code working with ASP (using VBS) but really don't understand 2 things: how to get the most recent EventID successfully sent to a particular listener and to what extent (if any) my code needs to keep track of the list of current listeners. Full disclosure -- I don't have much of a "feel" for this kind of code. For example, it took me a while to realize that the "messages" sent from the server just use the syntax for regular text output, using Response.Write.

OK, before I ask my two questions, here is the code I am using. This is taken from a PHP example I found (http://www.howopensource.com/2014/12/introduction-to-server-sent-events/). It outputs values from an array being read (meant to look like stock price data). In the php example, the sleep() function is used to put a delay between events. In place of that for asp I have used a different technique suggested to me in the comments (see below) that produce a synchronous delay of about 3 seconds. So here is the server-side ASP code:

<%@ LANGUAGE="VBSCRIPT" %>
<%
    Dim demoarr(21,1)
        demoarr(0,0) = "GOOG"
        demoarr(0,1) =  533.37 
        demoarr(1,0) = "MSFT"
        demoarr(1,1) =   47.59 
        demoarr(2,0) = "IBM"
        demoarr(2,1) =   162.99 
        demoarr(3,0) = "AAPL"
        demoarr(3,1) =  114.12 
        demoarr(4,0) = "MSFT"
        demoarr(4,1) =   47.29 
        demoarr(5,0) = "GOOG"
        demoarr(5,1) =  533.95 
        demoarr(6,0) = "IBM"
        demoarr(6,1) =   163.78 
        demoarr(7,0) = "GOOG"
        demoarr(7,1) =  533.55 
        demoarr(8,0) = "AAPL"
        demoarr(8,1) =  113.67 
        demoarr(9,0) = "GOOG"
        demoarr(9,1) =  533.91 
        demoarr(10,0) = "MSFT"
        demoarr(10,1) =   48.12 
        demoarr(11,0) = "IBM"
        demoarr(11,1) =   162.37 
        demoarr(12,0) = "AAPL"
        demoarr(12,1) =  114.12 
        demoarr(13,0) = "MSFT"
        demoarr(13,1) =   48.05 
        demoarr(14,0) = "AAPL"
        demoarr(14,1) =  114.32 
        demoarr(15,0) = "GOOG"
        demoarr(15,1) =  533.97 
        demoarr(16,0) = "MSFT"
        demoarr(16,1) =   48.54 
        demoarr(17,0) = "IBM"
        demoarr(17,1) =   162.69 
        demoarr(18,0) = "AAPL"
        demoarr(18,1) =  114.45 
        demoarr(19,0) = "IBM"
        demoarr(19,1) =   162.74 
        demoarr(20,0) = "AAPL"
        demoarr(20,1) =  114.67 

    response.ContentType = "text/event-stream"
    response.AddHeader "Cache-Control", "no-cache"
    response.AddHeader "Connection", "keep-alive"

    lastID = 0
    pickstock = 0
    Do While True
        pickstock = (pickstock + 1) MOD 20
        lastid = lastid + 2
        sendMessage lastId, demoarr(pickstock,0), demoarr(pickstock,1)
        sendMessage lastId+1, demoarr(pickstock,0), demoarr(pickstock,1)
        x = sleep(3)        
    Loop

Function sendMessage(id, ticket, price)
    Response.Write "id: " & id & vbcrlf & vbcrlf
    response.flush
    Response.Write "data: " & ticket & ":" & price & vbcrlf & vbcrlf
    response.flush
End Function    

Function sleep(scs)
    Dim lo_wsh, ls_cmd
    Set lo_wsh = CreateObject( "WScript.Shell" )
    ls_cmd = "%COMSPEC% /c ping -n " & 1 + scs & " 127.0.0.1>nul"
    lo_wsh.Run ls_cmd, 0, True 
End Function %>

And here is the client html/javascript:

<!doctype html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <title>Server Sent Events ASP Example - Stock Tickets</title>

<style media="screen" type="text/css">

H1 {
    text-align: center;
    font-size: 150%;
    margin-bottom: 60px;
}

H2 {
    text-align: center;
    font-size: 125%;
    margin-bottom: 15px;
}

DIV#tickets {
    margin: 10px auto 80px auto;
}

DIV.ticket {
    margin: 5px auto;
    width: 160px;
    font-size: 115%;
}

DIV.name {
    display: inline-block;
    width: 80px;
    padding: 3px;
}

DIV.price {
    display: inline-block;
    width: 60px;
    padding: 3px;
    text-align: right;
    transition: all 0.2s ease-out;
}

DIV#log {
    margin: 10px auto;
    width: 600px;
    height: 200px;
    background: gainsboro;
    padding: 5px;
    overflow-y: scroll;
}

DIV#notSupported {
    display: none;
    margin: 10px auto;
    text-align: center;
    color: red;
    font-size: 150%;
}

P.hint {
    width: 600px;
    margin: 10px auto;
    text-align: justify;
    text-indent: 20px;
    line-height: 135%;
}

DIV#download {
    margin: 50px auto;
    text-align: center;
}

DIV#download A {
    padding: 10px 25px;
    background: #F1592A;
    color: white;
    text-decoration: none;
    font-size: 20px;
    border-radius: 5px 5px;
}

DIV#download A:hover {
    text-decoration: underline;
    background: #FF592A;
}

</style>

<script type="text/javascript">

    window.onload = function setDataSource() {
        if (!!window.EventSource) {
            var source = new EventSource("stocks.asp");

            source.addEventListener("message", function(e) {
                updatePrice(e.data);
                logMessage(e);
            }, false);
            
            source.addEventListener("open", function(e) {
                logMessage("OPENED");
            }, false);

            source.addEventListener("error", function(e) {
                logMessage("ERROR");
                if (e.readyState == EventSource.CLOSED) {
                    logMessage("CLOSED");
                }
            }, false);
        } else {
            document.getElementById("notSupported").style.display = "block";
        }
    }

    function updatePrice(data) {
        var ar = data.split(":");
        var ticket = ar[0];
        var price = ar[1];
        var el = document.getElementById("t_" + ticket);
        var oldPrice = el.innerHTML;
        el.innerHTML = price;
        if (parseFloat(oldPrice) < parseFloat(price)) {
            el.style.backgroundColor = "lightgreen";
        } else {
            el.style.backgroundColor = "tomato";
        }
        window.setTimeout(function clearBackground() {
            el.style.backgroundColor = "white";
        }, 500);
    }

    function logMessage(obj) {
        var el = document.getElementById("log");
        if (typeof obj === "string") {
            el.innerHTML += obj + "<br>";
        } else {
            el.innerHTML += obj.lastEventId + " - " + obj.data + "<br>";
        }
        el.scrollTop += 20;
    }

</script>

</head>

<body>

<h1>Server Sent Events ASP Example</h1>

<div id="notSupported">
    Your browser does not support Server Sent Events.
    Please use <a href="https://www.mozilla.org/firefox/new/">Firefox</a>
    or <a href="https://www.google.com/chrome/browser">Google Chrome</a>.
</div>

<h2>Tickets</h2>

<div id="tickets">
    <div class="ticket"><div class="name">IBM</div><div class="price" id="t_IBM">161.57</div></div>
    <div class="ticket"><div class="name">AAPL</div><div class="price" id="t_AAPL">114.45</div></div>
    <div class="ticket"><div class="name">GOOG</div><div class="price" id="t_GOOG">532.94</div></div>
    <div class="ticket"><div class="name">MSFT</div><div class="price" id="t_MSFT">47.12</div></div>
</div>


<h2>Simple Log Console</h2>
<p class="hint">
    This is simple log console. It is useful for testing purposes and to understand better how SSE works.
    Event id and data are logged for each event.
</p>
<div id="log">
</div>


</body>
</html>

(Please ignore the use of OnEventListener and not onmessage -- I may not be illustrating the best syntax but I don't think that's the cause of my problems).

These sources work, but are VERY quirky. For example, if I don't call SendMessage twice in a row it doesn't work, and both Response.flush statements seem to be necessary on the server. Why is that??

More importantly, how is lastID supposed to be handled. In the PHP code, there is something called $_SERVER_LAST_EVENT_ID. What is that? Is there an equivalent for ASP? Does it matter? I haven't tried getting the event IDs working according to the SSE docs, which show the IDs being used to help with dropped connections.

I really have a lot more questions about this, but most importantly, what is really going on on the server? For example, if I were to institute a connection to the database (MSSQL) to check if a particular record has been modified, and there were 10 listeners, would that mean 10 database connections, or does the test/event-stream content type magically take care of this? I have read in Remy Sharp's blog (html5doctor.com/server-sent-events/) that "You need to maintain a list of all the connected users in order to emit new events." Is this really true? Doesn't an event-stream object take care of this aspect "automatically"? And why does Remy Sharp write "Ideally you should use a server that has an event loop. This means you should not use Apache, but instead use a platform such as Node.js..." What does that mean? Is the use of a While True infinite loop totally wrong-headed?

There are a bunch more things I don't understand (reconnecting, etc.), but permit me just one more question: I've read (https://medium.com/conectric-networks/a-look-at-server-sent-events-54a77f8d6ff7#:~:text=When%20working%20with%20Server%20Sent,stream%20of%20events%20over%20time.) that browsers are limited to 6 SSE connections, e.g., six tabs pointing to this .htm file calling the .asp file. But when I try it, I can only get one tab to work at a time. If I open a second tab, the first one shows an error and then the second one starts working. Only by opening two different browsers (e.g., firefox and chrome) can I show two windows that are both processing this simultaneously. My application is in an environment where users typically will be looking at these events in multiple tabs on a single browser. I know this works with the original PHP example. Why won't my ASP code do that?

0

There are 0 answers