Building a real-time Web application is challenging, and we need to think about how to send data from the server to the client. The technology to implement this “initiative” has been around for quite some time and is limited to two general methods: client-side pull or server push.

There are several ways to do this:

  • Long/short polling (client pull)
  • WebSockets (Server send)
  • Server-sent Events

Client pull – The client requires regular updates from the server

Server push – The server actively pushes updates to the client (as opposed to client pull)

Let’s use a simple use case to compare and select the technologies described above.

Example

Our example is very simple. We need to develop a dashboard Web application that can be downloaded from a website (eg: Github/Twitter /…) Stream a list of activities. The purpose of this application is to select the right method from the various methods listed earlier.

A, polling

Polling is a technique in which clients periodically request new data from the server. We can poll in two ways: short poll and long poll.

Simply put, a short poll is an Ajax-based timer that calls at a fixed delay, while a long poll is based on Comet (that is, when a server event occurs, the server sends data to the client without delay (in real time)). Both have advantages and disadvantages, and both are suitable for use case-based situations. For further details, read the answers from the StackOverflow community.

Let’s see what a simple client-side long polling fragment might look like:

/* Client - subscribing to the github events */
subscribe: (callback) = > {
    const pollUserEvents = () = > {
        $.ajax({
            method: 'GET'.url: 'http://localhost:8080/githubEvents'.success: (data) = > {
                // Called when the request succeeds
                callback(data) // process the data
            },
            complete: () = > {
                // Call when the request completes (whether successful or not)
                pollUserEvents(); 
            },
            timeout: 30000
        })
    }
    pollUserEvents()
}
Copy the code

This is basically a long polling function that runs the first time as usual, but it sets a thirty (30) second timeout, and the callback calls Ajax again after each asynchronous Ajax call to the server.

The page makes a request to the server, and the server keeps the connection open until there is data to send. After sending the data, the browser closes the connection and randomly initiates a new request to the server. This process continues as long as the page is open.

Ajax calls work over the HTTP protocol, which means that requests to the same domain should be multiplexed by default. We found some flaws in this approach.

  • Polling requiring 3 round trips (TCP SIN, SSL, and Data)
  • Timeout (If the proxy server is idle for too long, the connection will be closed)

Second, the WebSocket

An HTTP-based extension that supports long connections and is used to establish a bidirectional channel between a client and a server.

The disadvantages of the traditional polling method (that is, the use of HTTP protocol to continuously send requests) are: waste of traffic (HTTP request headers are large), waste of resources (requests without updates), consumption of server CPU (requests without information).

Implementation process:

After WebSockets are created in Javascript, an Http Upgrade request is sent to the server. Upon receiving the server’s response, the established connection is upgraded from Http to WebSocket.

So:

1. The client sends an Http GET request, and upgrade 2. The server responds to the client switching Protocol. [Http => WebSocket] 3. WebSocket communication is enabled.

Both HTTP(protocol) and WebSocket(protocol) are located at the application layer of the OSI model and therefore rely on TCP at Layer 4.

There is an MDN documentation that explains Websockets in detail, and I recommend that you read it as well.

Let’s take a look at what a very simple WebSocket client implementation might look like:

$(function () {
  // if user is running mozilla then use it's built-in WebSocket
  window.WebSocket = window.WebSocket || window.MozWebSocket;

  const connection = new WebSocket('ws://localhost:8080/githubEvents');

  connection.onopen = function () {
    // connection is opened and ready to use
  };

  connection.onerror = function (error) {
    // an error occurred when sending/receiving data
  };

  connection.onmessage = function (message) {
    // try to decode json (I assume that each message
    // from server is json)
    try {
      const githubEvent = JSON.parse(message.data); // display to the user appropriately
    } catch (e) {
      console.log('This doesn\'t look like a valid JSON: '+ message.data);
      return;
    }
    // handle incoming message
  };
});
Copy the code

If the server supports the WebSocket protocol, it will agree to the upgrade and communicate via the upgrade header in the response.

Let’s see how to implement it in NodeJs:

const express = require('express');
const events = require('./events');
const path = require('path');

const app = express();

const port = process.env.PORT || 5001;

const expressWs = require('express-ws')(app);

app.get('/'.function(req, res) {
	res.sendFile(path.join(__dirname + '/static/index.html'));
});

app.ws('/'.function(ws, req) {
	const githubEvent = {}; // sample github Event from Github event API https://api.github.com/events
	ws.send('message', githubEvent);
});

app.listen(port, function() {
	console.log('Listening on', port);
});
Copy the code

Once we get the data from the Github event API, we can stream it to the client once the connection is established. In our case, this approach has some drawbacks.

  • For WebSockets, we need to deal with a lot of issues in HTTP.
  • WebSocket is a different data transfer protocol that is not automatically multiplexed over HTTP/2 connections. Implementing custom multiplexing on both the server and client is a bit complicated.
  • WebSockets are frame-based, not stream-based. When we open the Network TAB. You can see the WebSocket messages listed under the frame.

For in-depth details on WebSocket, read this great article, where you can read more about shards and how to deal with them under the hood.

三, SSE (Server-sent Events)

SSE is a mechanism that allows a server to asynchronously push data to a client after establishing a client-server connection. The server can then decide to send data when a new “data block” becomes available. It can be viewed as a one-way publish-subscribe model.

It also provides a standard javascript client API called EventSource, which is implemented in most modern browsers as part of the W3C’s HTML5 standard. Polyfills works with browsers that do not support the EventSource API.

We can see that Edge and Opera Mini lag behind this implementation, and SSE’s most important case is aimed at mobile browser devices where these browsers have no viable market share. Yaffle is a well-known EventSource pollyfill.

Because SSE is based on HTTP, it has a natural fit with HTTP/2 and can be combined to get the best of the two: HTTP/2 handles an efficient transport layer based on multiplexed streams and SSE, providing apis to applications to enable push. Therefore, multiplexing is achieved through HTTP/2. The client and server are notified when the connection is down. By maintaining a unique ID for messages, the server can see that the client has lost N messages and sends the missing backlogs when it reconnects.

Let’s look at what a simple client-side implementation might look like:

const evtSource = new EventSource('/events');

 evtSource.addEventListener('event'.function(evt) {
      const data = JSON.parse(evt.data);
      // Use data here
 },false);
Copy the code

This code is fairly simple. It connects to our source and waits to receive a message. The example NodeJS server will look something like this.

// events.js
const EventEmitter = require('eventemitter3');
const emitter = new EventEmitter();

function subscribe(req, res) {

	res.writeHead(200, {
		'Content-Type': 'text/event-stream'.'Cache-Control': 'no-cache'.Connection: 'keep-alive'
	});

	// Heartbeat
	const nln = function() {
		res.write('\n');
	};
	const hbt = setInterval(nln, 15000);

	const onEvent = function(data) {
		res.write('retry: 500\n');
		res.write(`event: event\n`);
		res.write(`data: The ${JSON.stringify(data)}\n\n`);
	};

	emitter.on('event', onEvent);

	// Clear heartbeat and listener
	req.on('close'.function() {
		clearInterval(hbt);
		emitter.removeListener('event', onEvent);
	});
}

function publish(eventData) {
  // Emit events here recieved from Github/Twitter APIs
	emitter.emit('event', eventData);
}

module.exports = {
	subscribe, // Sending event data to the clients 
	publish // Emiting events from streaming servers
};

// App.js
const express = require('express');
const events = require('./events');
const port = process.env.PORT || 5001;
const app = express();

app.get('/events', cors(), events.subscribe);

app.listen(port, function() {
   console.log('Listening on', port);
});
Copy the code

The main benefits of this approach are:

  • Simpler implementation and data efficiency
  • It is multiplexed automatically out of the box over HTTP/2.
  • Limit the number of connections for data on the client to one

How do I choose SSE, WebSocket, and polling?

After detailed client and server sample implementations, it looks like SSE is the final answer to our data transfer problem. It also has some problems, but it can be solved.

A few simple examples of applications that can use Server-Sent Events:

  • Streaming real-time chart of stock prices
  • Live news coverage of important events (Posting links, tweets and images)
  • Real-time Github/Twitter dashboard wall provided by the Twitter streaming API
  • Monitor server statistics such as uptime, health, and running processes.

However, SSE is more than just a viable alternative to other approaches that provide rapid updates. There are certain scenarios where each scenario is superior to the others, as in our case SSE proved to be an ideal solution. Consider a scenario such as an MMO (massively multiplayer online) game that requires a large number of messages to connect both ends. In this case, WebSockets control SSE.

If your use case needs to display real-time market news, market data, chat applications, etc., as in our example, relying on HTTP/2+SSE will give you an efficient two-way communication channel while benefiting from the HTTP world.

If you want to use the use case I just used, pull the Github code.

Other knowledge points

  • HTTP /1.1 Keep Alive vs. HTTP /2 multiplexing?

reference

  1. Polling vs SSE vs WebSocket — How to choose the right one
  2. Server-sent Events tutorial – Nguyen Yifeng
  3. WebSocket tutorial — Ruan Yifeng
  4. HTTP2.0 research on multiplexing
  5. Analysis of HTTP/2 multiplexing