MaiDeveloper

Introduction to Server-Sent Events (EventSource) one way communication

Clients start requests, and servers react to those requests, according to the HTTP protocol, which is the foundation of the internet. However, some web apps find that having their server give them notifications as events occur is beneficial. The client sends a request to the server, but neither the client nor the server closes the connection. The server publishes data to the connection but maintains it open when it has something to inform the client about. The impression is as though the client sends a network request, and the server answers slowly and burstily, with long gaps between spurts of activity. These kinds of network connections don't normally last forever, but if the client notices that the connection has closed, it can simply request that it be reopened.

Server-Sent Events vs WebSockets

Server-sent events, unlike WebSockets, are unidirectional; that is, data packets are transmitted from the server to the client (such as a user's web browser). This makes them a good alternative when data from the client to the server does not need to be sent in message form. For example, EventSource may be used to handle social media status updates, news feeds, and data delivery to a client-side storage mechanism like IndexedDB or web storage.

Event Source

This method for allowing clients to submit messages to servers is surprisingly successful (though it can be expensive on the server side because the server must maintain an active connection to all of its clients). Client-side JavaScript supports the EventSource API because it is a useful programming paradigm. Simply supply a URL to the EventSource() constructor to build a long-lived request connection to a web server. The EventSource object converts (fully written) data that the server publishes to the connection into events that you may listen for:

const stock = new EventSource('/stock');
stock.addEventListener('bid', event => {
  display(event.data);
});

Message format

The protocol for Server-Sent Events is simple. When the client creates the EventSource object, it establishes a connection with the server, which the server maintains. The server publishes lines of text to the connection when an event happens.

event: chat
data: Hello world
data: Nice to meet you

Note that it starts with event type. In this case, chat is the event type. Follow by the data with data: prefix. THe message must end with a blank line.

Limitation

Browsers only allow up to 6 connections (assume users open the page on different tabs). In order to get around this limitation. We need to use http/2.0 protocol in which it increases the limitation up to 100.

Use cases

  • Twitter/Facebook updates
  • Stock price updates
  • News feed
  • Sport results
  • Chat

Simple chat example

Front-End

Create a textbox where users can enter their message.

<input id="input" />

Prompt to ask for name.

const name = prompt('Enter your name');

Initialize event source.

const chat = new EventSource('/chat');

Attach a handler to event source.

chat.addEventListener('chat', e => {
  const div = document.createElement('div');
  
  div.textContent = e.data;
  
  // Prepend the div above the input
  input.before(div);
  // Make sure the input is in viewport
  input.scrollIntoView();
});

Last, send user message to server.

input.addEventListener('change', () => {
  fetch('/chat', {
    method: 'POST',
    body: name + ': ' + input.value
  })
  .catch(e => console.error);
  
  input.value = '';
});

Back-End

When the client makes a get request to /chat as known as initialize event source object, we need to keep a reference to the response object; because later we use it to send the message. At the end, we respond with text/event-stream, keep-alive, and no-cache headers.

const clients = new Map();

// Keep a list of clients
clients.set(response, response);

// When users close connection
// Remove the client and close the connection
request.connection.on('end', () => {
  clients.delete(response);
  response.end();
});

// Keep the connection alive
response.write(200, {
  'Content-Type': 'text/event-stream',
  'Connection': 'keep-alive',
  'Cache-Control': 'no-cache'
});

When the clients make a post request to /chat as known as send message. It broadcast the message to all the clients.

  request.setEncoding('utf8');
  
  let chunk = '';
  
  // Get the body
  let await (const chunk of request) {
    body += chunk;
  }
  
  // Construct message
  const message = 'data: ' + body.replace('\n', '\ndata: ');
  const event = `event: chat\n${message}\n\n`;
  
  // Send message to all clients
  clients.forEach(client => client.write(event));

Full example

index.html

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Simple Chat Using Event Source</title>
  <style type="text/css">
    #input {
      width: 100%;
      padding: 10px;
      border: solid black 2px;
    }
  </style>
</head>
<body>
  <input id="input" />
  <script type="text/javascript">
    const name = prompt('Enter your name');
    const input = document.getElementById('input');
    const chat = new EventSource('/chat');

    input.focus();

    chat.addEventListener('chat', e => {
      const div = document.createElement('div');
      
      div.textContent = e.data;

      input.before(div);
      input.scrollIntoView();
    });

    input.addEventListener('change', () => {
      fetch('/chat', {
        method: 'POST',
        body: name + ': ' + input.value
      })
      .catch(e => console.error);

      input.value = '';
    });
  </script>
</body>
</html>

server.js

const http = require('http');
const fs = require('fs');
const url = require('url');

const indexHtml = fs.readFileSync('index.html');
const clients = new Map();
const server = new http.Server();

const acceptNewClient = (req, res) => {
  clients.set(res, res);

  req.connection.on('end', () => {
    clients.delete(res);
    res.end();
  });

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

  res.write('event: chat\ndata: Connected\n\n');
};

const broadcastNewMessage = async (req, res) => {
  req.setEncoding('utf8');

  let body = '';

  for await (const chunk of req) {
    body += chunk;
  }

  res.writeHead(200).end();

  const message = 'data: ' + body.replace('\n', '\ndata: ');
  const event = `event: chat\n${message}\n\n`;

  
  clients.forEach(client => client.write(event));
};

server.on('request', (req, res) => {
  const pathname = url.parse(req.url).pathname;

  if (pathname === '/') {
    return res.writeHead(200, {
      'Content-Type': 'text/html'
    }).end(indexHtml);
  }

  if (pathname === '/chat') {
    if (req.method === 'GET') {
      return acceptNewClient(req, res);
    }

    if (req.method === 'POST') {
      return broadcastNewMessage(req, res);
    }
  }

  return res.writeHead(404).end();
});

server.listen(8080);
console.log('Listening on port 8080');


Mike Mai
Mike Mai   Brooklyn, New York
I am full-stack web developer, passionate about building world class web applications. Knowledge in designing, coding, testing, and debugging. I love to solve problems.