CodeWiz Logo

    CodeWiz

    Building Real-Time Applications with Server-Sent Events (SSE) in Spring Boot

    Building Real-Time Applications with Server-Sent Events (SSE) in Spring Boot

    10/12/2024

    Introduction

    Similar to Websockets, Server-Sent Events (SSE) is a technology that enables real-time communication between client and a server over HTTP. However, SSE is a unidirectional communication channel where the server can push data to the client without the client needing to request it. This makes it ideal for scenarios where the server needs to send real-time updates to the client, such as news feeds, stock tickers, or social media updates. Since SSE uses HTTP unlike Websockets, it is easier to implement and works well with existing web infrastructure. Also, it can share the same TCP connection with other HTTP requests, making it more efficient than Websockets in some scenarios.

    Understanding Server-Sent Events (SSE) communication

    Now let us see how SSE works.

    SSE Communication


    • Client Request: Client initiates the communication by sending a request to the server for SSE. This is typically done using JavaScript's EventSource API.
    • Server Response: The server responds to the client's request by setting specific headers to establish an SSE connection. The Content-Type header is set to text/event-stream to indicate that the response will be a stream of events. The server also sets headers to keep the connection alive, signaling to the client that it will continuously push messages
    • Continuous Data Push: Once the connection is established, the server can continuously push messages to the client. This connection remains open until either the client or the server decides to close it.

    SSE Message Structure

    Each message sent from the server to the client follows a specific structure and can include the following fields:

    • ID An identifier for each message, used to keep track of the sequence of messages. If the connection is lost, the client can use this ID to request messages starting from the last received ID.
    • Event The type of event being sent. This can be used by the client to handle different types of messages differently.
    • Data The actual message content. This is the only mandatory field and contains the text message sent from the server to the client.
    • Retry A hint to the client indicating how long (in milliseconds) it should wait before attempting to reconnect if the connection is lost.

    If the connection between the client and server is lost, the EventSource API can automatically attempt to reconnect. When reconnecting, the client sends the last received message ID in a header, allowing the server to resume sending messages from that point.

    Sample Message

    id: 12345
    event: update
    retry: 10000
    data: {"message": "New article published!"}

    Now let us build real-time news feed application using Server-Sent Events in Spring Boot.

    Setting Up the Spring Boot Project

    Project Dependencies

    We will see how we can build real-time news feed application using Server-Sent Events using both the Spring Web and Spring Reactive Web. Let us create a new Spring Boot project with the following dependencies:

    • Spring Web
    • Spring Reactive Web

    You can use Spring Initializr to generate the project.

    Maven Dependencies (pom.xml)

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
    </dependencies>

    Records for news model

    public record Article(String author, String title, String description, String url, String urlToImage, String publishedAt, String content) {
    }
    
    public record News(List<Article> articles) {
    }

    Data source for news feed

    In real world scenario, the news feed data can be fetched from external APIs or some middleware services. For simplicity, we will use a static list of news articles in a JSON file like below.

    {
      "totalResults": 2,
      "articles": [
        {
          "source": {
            "name": "CNET"
          },
          "author": "Kevin Lynch",
          "title": "Arsenal vs. Everton Livestream: How to Watch English Premier League Soccer From Anywhere - CNET",
          "description": "The Gunners need a win at home and hope for a Man City slip-up for their title dream to come true.",
          "url": "https://www.cnet.com/tech/services-and-software/arsenal-vs-everton-livestream-how-to-watch-english-premier-league-soccer-from-anywhere/",
          "urlToImage": "https://www.cnet.com/a/img/resize/47fe72d55cd7f1f105a65f5bc76f356701eeac01/hub/2024/05/18/8c2df860-58a6-41f8-9344-bf2c33f607af/gettyimages-2150177293.jpg?auto=webp&fit=crop&height=675&width=1200",
          "publishedAt": "2024-05-19T12:03:05Z",
          "content": "Arsenal come into the final game of the season against Everton at home with their hopes of winning the title for the first time in 20 years very much in the balance.\r\nWith Manchester City two points … [+5356 chars]"
        },
        {
          "source": {
            "name": "CNET"
          },
          "author": "Kevin Lynch",
          "title": "Crystal Palace vs. Aston Villa Livestream: How to Watch English Premier League Soccer From Anywhere - CNET",
          "description": "The Eagles look to maintain a strong finish to the season as they face Champions League-bound Villains.",
          "url": "https://www.cnet.com/tech/services-and-software/crystal-palace-vs-aston-villa-livestream-how-to-watch-english-premier-league-soccer-from-anywhere/",
          "urlToImage": "https://www.cnet.com/a/img/resize/02dee24e5eff30abe63262f841cab20b1d37c71e/hub/2024/05/18/3ee52c4f-8124-4cbc-b60a-3a8c6fa420a0/gettyimages-2152468104.jpg?auto=webp&fit=crop&height=675&width=1200",
          "publishedAt": "2024-05-19T12:03:08Z",
          "content": "Crystal Palace will look to continue their sparkling close-season form on Sunday as they host an Aston Villa side basking in the afterglow of Champions League qualification.\r\nPalace fans will likely … [+5318 chars]"
        },
      ]
    }

    Create a Spring Web Controller and add an endpoint to serve the news feed using Web Flux

    @RestController
    public class NewsController {
    
        @GetMapping(value = "/news", produces = "text/event-stream")
        public Flux<ServerSentEvent<List<Article>>> news() throws IOException {
            Resource resource = new ClassPathResource("news.json");
            Path path = resource.getFile().toPath();
            String json = Files.readString(path);
    
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
            News news = objectMapper.readValue(json, News.class);
    
            return Flux.fromIterable(news.articles()).window(3)
                    .delayElements(Duration.ofSeconds(1))
                    .flatMap(Flux::collectList)
                    .map(articles -> ServerSentEvent.<List<Article>>builder()
                        .event("news").data(articles).build());
        }
    }

    • Here we are using Flux to represent a stream of Server-Sent Events. Web Flux provides a fluent way to create a stream of Server-Sent Events using the Flux API.
    • We read the news feed data from a JSON file using Jackson object mapper.
    • We then use Flux to window the news articles in a window of 3 and send it to the client every second.
    • In each SSE message we are setting the event type as news and sending the list of articles as data. You can set retry time and ID as well in the SSE message.

    Testing the News Feed Application

    Let us use httpie to test the news feed application.

    http localhost:8080/news

    You should see the news feed data being streamed to the client every second like below

    http localhost:8080/news
    HTTP/1.1 200 
    Connection: keep-alive
    Content-Type: text/event-stream
    Date: Tue, 10 Dec 2024 07:23:58 GMT
    Keep-Alive: timeout=60
    Transfer-Encoding: chunked
    
    // Message 1
    
    event:news
    data:[
        {
            "author": "Kevin Lynch",
            "content": "Arsenal come into the final game of the season against Everton at home with their hopes of winning the title for the first time in 20 years very much in the balance.\r\nWith Manchester City two points … [+5356 chars]",
            "description": "The Gunners need a win at home and hope for a Man City slip-up for their title dream to come true.",
            "publishedAt": "2024-05-19T12:03:05Z",
            "title": "Arsenal vs. Everton Livestream: How to Watch English Premier League Soccer From Anywhere - CNET",
            "url": "https://www.cnet.com/tech/services-and-software/arsenal-vs-everton-livestream-how-to-watch-english-premier-league-soccer-from-anywhere/",
            "urlToImage": "https://www.cnet.com/a/img/resize/47fe72d55cd7f1f105a65f5bc76f356701eeac01/hub/2024/05/18/8c2df860-58a6-41f8-9344-bf2c33f607af/gettyimages-2150177293.jpg?auto=webp&fit=crop&height=675&width=1200"
        },
        {
            "author": "Kevin Lynch",
            "content": "Crystal Palace will look to continue their sparkling close-season form on Sunday as they host an Aston Villa side basking in the afterglow of Champions League qualification.\r\nPalace fans will likely … [+5318 chars]",
            "description": "The Eagles look to maintain a strong finish to the season as they face Champions League-bound Villains.",
            ...
        },
        {
            "author": "Rob Price",
            "content": "Since its founding in 2020, WiFi Money has left a trail of lawsuits alleging fraud, bankruptcies, mental breakdowns, and financial devastation.Brandon Celi for BI\r\nAlex Moeller was having a great mon… [+24113 chars]",
            ...
        }
    ]
    
    // Message 2
    
    event:news
    data:[
        {
            "author": "Ryan Bittan",
            "content": "SALT LAKE CITY (ABC4) The defense for Kouri Richins the Kamas, Utah woman on trial for allegedly poisoning her husband with fentanyl, resulting in his death in March of 2022 has filed a motion to wit… [+1212 chars]",
            ...
        },
        {
            "author": "Lauren Sforza",
            "content": "Sen. John Fetterman (D-Penn.) responded Sunday to Rep. Alexandria Ocasio-Cortezs (D-N.Y.) criticism of his comments mocking the Jerry Springer-like chaos during a House hearing last week, where the p… [+2090 chars]",
            ...
        },
        {
            "author": "Perri Ormont Blumberg",
            "content": "For about 30 summers, Mindy Haar worked as the head lifeguard at a sleepaway camp in the Catskill Mountains. The teenage lifeguards under her charge wouldnt always remember to drink water, so she rem… [+10389 chars]",
            ...
        }
    ]
    
    // Message 3
    
    event:news
    data:[
        {
            "author": "Josh Dubow / AP",
            "content": "Jim Otto, the Hall of Fame center known as Mr. Raider for his durability through a litany of injuries, has died, the team confirmed Sunday night. He was 86.\r\nThe cause of death was not immediately kn… [+4229 chars]",
            ...
        },
        {
            "author": "Anna Nordberg",
            "content": "During the darkest depths of COVID, my husband and I had the same conversation over and over. I predicted the iron grip of Activities Culture would loosen after lockdown, and parents would become mor… [+17909 chars]",
            ...
        },
        {
            "author": "Elizabeth Merrill",
            "content": "Five years of football dominance in Kansas City inspired a spike in dogs named Kelce, and, for the creative types, some feline Catrick Mahomeses. But in Louisburg, Kansas, Shelly Trester and her fami… [+19763 chars]",
            ...
        }
    ]

    If you open the URL in browser and check in dev tools, you can see the news feed data being streamed to the client every second.


    sse-streaming


    Implementing SSE with Spring Web MVC

    
    private final ExecutorService nonBlockingService = Executors.newCachedThreadPool();
    
    @GetMapping(value = "/news-emitter")
    public SseEmitter newsEmitter() throws IOException {
        SseEmitter emitter = new SseEmitter();
        Resource resource = new ClassPathResource("news.json");
        Path path = resource.getFile().toPath();
        String json = Files.readString(path);
    
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        News news = objectMapper.readValue(json, News.class);
        nonBlockingService.execute(() -> {
            try {
                news.articles().stream().gather(Gatherers.windowFixed(3)).forEach(articles -> {
                    try {
                        emitter.send(articles);
                        Thread.sleep(1000);
                    } catch (Exception e) {
                        emitter.completeWithError(e);
                    }
    
                });
                emitter.complete();
            } catch (Exception ex) {
                emitter.completeWithError(ex);
            }
        });
        return emitter;
    }
    
    • As you can see code is little more complex compared to Web Flux. We need to use ExecutorService to send the news feed data to the client every second.
    • We use SseEmitter to send the news feed data to the client.
    • We are using new Stream Gatherers API to gather the news articles in a window of 3 and send it to the client.

    UI for News Feed Application

    Now let us create a simple HTML page to display the news feed data being streamed from the server.

    <!DOCTYPE html>
    <html>
    <head>
        <title>News Feed</title>
    </head>
    <body>
    <div id="news"></div>
    
    <script>
        var source = new EventSource('/news-emitter');
    
        source.onmessage = function(event) {
            var articles = JSON.parse(event.data);
            var newsDiv = document.getElementById('news');
    
            articles.forEach(function(article) {
                var articleDiv = document.createElement('div');
                articleDiv.className = 'article';
    
                var author = document.createElement('p');
                author.textContent = 'Author: ' + article.author;
                articleDiv.appendChild(author);
    
                var title = document.createElement('h2');
                title.textContent = article.title;
                articleDiv.appendChild(title);
    
                var description = document.createElement('p');
                description.textContent = article.description;
                articleDiv.appendChild(description);
    
                var image = document.createElement('img');
                image.src = article.urlToImage;
                articleDiv.appendChild(image);
    
                newsDiv.insertBefore(articleDiv, newsDiv.firstChild);
            });
        };
    </script>
    </body>
    </html>

    • Here we are using EventSource API to connect to the server and receive the news feed data.
    • We parse the news feed data and display it in the browser.

    If we keep this HTML file with name ss.html in resources/static folder, we can access it using URL http://localhost:8080/sse.html and see the news feed data being streamed to the client every second.


    sse-streaming


    Pros

    • Realtime updates from Server: SSE enables real-time updates from the server to the client, making it ideal for applications that require live data.
    • Simplicity: SSE uses the standard HTTP protocol for data transfer, making it simpler to implement compared to other protocols like WebSockets.
    • Automatic Reconnection: The EventSource API used in SSE has built-in automatic reconnection capabilities. If the connection is lost, it will automatically attempt to reconnect.
    • Built-in Message Ordering: SSE ensures that messages are received in the order they were sent, thanks to the built-in message ID feature.
    • Compatibility with Browsers and Firewalls: Since SSE uses HTTP, it is compatible with most browsers and firewalls, unlike WebSockets which may face issues with some firewalls.
    • Less Resource Consumption by Multiplexing: When used with HTTP/2, SSE can leverage multiplexing, allowing multiple HTTP requests and SSE streams to share a single TCP connection, reducing resource consumption.

    Cons

    • Unidirectional: SSE is designed for one-way communication from the server to the client. It is not suitable for scenarios requiring bidirectional communication, where WebSockets would be a better choice.
    • Connection Limitation if Using HTTP 1.1: When using HTTP/1.1, each SSE connection requires a separate TCP connection. This can lead to connection limitations, especially if multiple SSE connections are needed.
    • No Binary Data Support: SSE does not natively support binary data. While it is possible to encode binary data as text, this approach is inefficient.

    Example Use Cases

    • Live Feeds: Ideal for applications that need to push live updates, such as news feeds, social media updates, and live sports scores.
    • Notifications and Alerts: Useful for sending real-time notifications and alerts from the server to the client.
    • Real-time Dashboards: Suitable for applications that display real-time data, such as server stats or metrics.
    • Stock Prices: Can be used to push real-time stock price updates to clients.

    Conclusion

    Server-Sent Events (SSE) provide a simple and efficient way to implement real-time communication between the server and the client over HTTP. While SSE is not suitable for bidirectional communication, it is well-suited for scenarios where the server needs to send real-time updates to the client without the need for the client to request them.

    To stay updated with the latest updates in Java and Spring follow us on youtube, linked in and medium. You can find the code used in this blog here

    Video Version

    You can watch the video version of this blog on our youtube channel.