DEV Community

Amin
Amin

Posted on

Realtime without Websocket using Server-Sent Events

For the following tutorial, we will be using Express & Node.js, as long as cURL for simulating POST requests. This can also be done using any programming language that has an implementation of the HTTP protocol.

Create the Node configuration

touch package.json
Enter fullscreen mode Exit fullscreen mode
{
  "type": "module",
  "scripts": {
    "start": "node sources/server/index.js
  }
}
Enter fullscreen mode Exit fullscreen mode

Install Express

npm install --save --save-exact express
Enter fullscreen mode Exit fullscreen mode

Create the server file

mkdir -p sources/server
touch sources/server/index.js
Enter fullscreen mode Exit fullscreen mode
import express from "express"

// Express server initialization
const server = express()

// Port exposing the HTTP server
const port = 8000

// Host to listen for HTTP requests
const host = "0.0.0.0"

// Used for hosting our client files (HTML, CSS, JavaScript, ...)
server.use(express.static("sources/client"))

// Server-Sent Event HTTP route
server.get("/api/users/event", (request, response) => {
    // Used to prevent the browser from closing the connection after receiving the first body
    response.setHeader("Connection", "keep-alive")

    // Used to tell the browser that we are sending Server-Sent Events
    response.setHeader("Content-Type", "text/event-stream")

    // Used as a security measure to prevent caching the data we send
    response.setHeader("Cache-Control", "no-cache")

    // Loop to simulate realtime data updates at regular intervals
    setInterval(() => {
        // Expected format that the browser will decode for use in the client
        // write is used to prevent closing the connection from the server with a send, json, end, ...
        response.write("event: update\ndata: Hello, world!\n\n")
    }, 1000)
})

// Start listening for HTTP requests
server.listen(port, host, () => {
    // Displaying the host & port listened in the console for debugging purposes
    console.log(`Listening on http://${host}:${port}`)
})
Enter fullscreen mode Exit fullscreen mode

Create the HTML file

mkdir -p sources/client
touch sources/client/index.html
Enter fullscreen mode Exit fullscreen mode
<!DOCTYPE html>
<html lang="en-US">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta name="description" content="Test for demonstrating how to use the Server-Sent Events">
        <title>Server-Sent Events</title>
    </head>
    <body>
        <script src="./index.js" type="module"></script>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Create the JavaScript file

touch sources/client/index.js
Enter fullscreen mode Exit fullscreen mode
const usersEventSource = new EventSource("/api/users/event")

usersEventSource.addEventListener("open", () => {
    console.log("Connected to the server")
})

usersEventSource.addEventListener("error", error => {
    console.error("Oops! Something went wrong, here is why")
    console.error(error)
})

usersEventSource.addEventListener("update", event => {
    console.log("Successfully received a notification from the server")
    console.log(event.data)
})
Enter fullscreen mode Exit fullscreen mode

Start the server

npm start
Enter fullscreen mode Exit fullscreen mode

Open the browser

Open the browser at http://localhost:8000, or using the port and host of your choice if you have decided to change those.

You should see in the console that the message gets updated every second.

If you open the network tab of your browser, you should also see that it do not sends periodic HTTP requests, this is because we established an connection that remains alive even after the body is received from the server which is the standard behavior when received data from a server to a client.

Let's improve this example by creating a more real-world example.

Install BodyParser

npm install --save --save-exact body-parser
Enter fullscreen mode Exit fullscreen mode

Add a middleware for using BodyParser

import express from "express"

// Let us import the BodyParser library
import bodyParser from "body-parser"

const server = express()

const port = 8000

const host = "0.0.0.0"

server.use(express.static("sources/client"))

// Let us parse the request body and access the
// JSON content easily
server.use(bodyParser.json())

server.get("/api/users/event", (request, response) => {
    response.setHeader("Connection", "keep-alive")
    response.setHeader("Content-Type", "text/event-stream")
    response.setHeader("Cache-Control", "no-cache")

    setInterval(() => {
        response.write("event: update\ndata: Hello, world!\n\n")
    }, 1000)
})

server.listen(port, host, () => {
    console.log(`Listening on http://${host}:${port}`)
})
Enter fullscreen mode Exit fullscreen mode

Add a route for creating a user

import express from "express"
import bodyParser from "body-parser"

// Import the EventEmitter class
import { EventEmitter } from "events"

const server = express()
const port = 8000
const host = "0.0.0.0"

// Create the event emitter for sending and receiving updates
const usersEventEmitter = new EventEmitter()

server.use(express.static("sources/client"))
server.use(bodyParser.json())

server.get("/api/users/event", (request, response) => {
    response.setHeader("Connection", "keep-alive")
    response.setHeader("Content-Type", "text/event-stream")
    response.setHeader("Cache-Control", "no-cache")

    // Wait and listen for user updates
    usersEventEmitter.on("user", user => {
        // Sends back the user that has been created with the /api/users route
        response.write(`event: update\ndata: ${JSON.stringify(user)}\n\n`)
    })
})

// Adds a new route handler for user creation requests
server.post("/api/users", (request, response) => {
    // Get the firstname from the parsed JSON body
    const firstname = request.body.firstname

    // Get the lastname from the parsed JSON body
    const lastname = request.body.lastname

    // Create a user object
    const user = {firstname, lastname}

    // Sends the event to the /api/users/event route
    usersEventEmitter.emit("user", user)

    // Tells the client that the user has been created
    response.status(201)

    // Closes the HTTP connection without any body
    response.end()
})

server.listen(port, host, () => {
    console.log(`Listening on http://${host}:${port}`)
})
Enter fullscreen mode Exit fullscreen mode

Restart the server

npm start
Enter fullscreen mode Exit fullscreen mode

Reload the browser

You should have nothing in the console, that is normal because we didn't create a user yet. Let's do that now.

Create a new user using cURL

curl -X POST -H 'Content-Type: application/json' -d '{"firstname":"John","lastname":"DOE"}' localhost:8000/api/users
Enter fullscreen mode Exit fullscreen mode

Go back to your browser. You should now see a new notification.

Notice that you only get the notification once you created a new user.

Purpose

Server-Sent Event are really great if you need to get updated data in realtime from your server, such as the state of an order, or a notification system.

They are also really easy to setup and does not require implementing a whole new protocol with the added complexity of following an RFC. They are implemented as a Web API in all modern browsers and the server implementation is very trivial.

For most case scenario where you need to fetch data from the server using polling or long polling, Server-Sent Events can be a more efficient alternative since it only creates one HTTP connection and do not fetch data when it is not needed.

Limitations

You won't be able to send back realtime data from the client to the server, and in this case using an implementation of the Websocket protocol will be mandatory.

What's next?

  • Choose a database
  • Store the user created in the POST /api/users route in the database
  • Before connecting to the EventSource, fetch the list of users that are already stored in the database
  • Find a way to reconnect to the server in case the connection is closed (error, network issue, etc...)
  • Ditch fetching users in realtime and start from scratch by creating an instant chat messaging system by using both HTTP & SSE requests

Bonus

Here is what it would look like from the server side if we were to create a SSE server in PHP even though it sounds less interesting than in Node.js.

<?php

header("Content-Type: text/event-stream");
header("Connection: keep-alive");
header("Cache-control: no-cache");

while (true) {
    try {
        $connection = new PDO("driver:dbname=name;host=localhost", "user", "pass");
        $query = $connection->query("SELECT * FROM users");
        $users = $query->fetchAll(PDO::FETCH_ASSOC);
        $data = json_encode($users);
        $event = "message";
        echo "event: $event\ndata: $data\n\n";
        sleep(5);
    } catch (Exception $exception) {
        // TODO: register the error somewhere
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
anwar_nairi profile image
Anwar

Amazing, appreciate you did the example showing PHP code as well!