【SpringBoot WEB series 】SSE server send event details

The full name of SSE is Server Sent Event. Literally translated, SSE is the Server Sent Event. It is rarely used in general project development, and many people may not know what it does and what it is used for

The main points of this article are as follows:

  • SSE literacy, application scenario analysis
  • Use asynchronous request to implement SSE, deepen conceptual understanding
  • useSseEmitterImplement a simple push example

I. SSE literacy

Skip this section if the basic concepts of SSE are clear

1. Concept introduction

Server Sent Event (SSE) literally means Server Sent Event. As the name implies, the client can obtain events Sent by the Server

A common HTTP interaction is that the client initiates a request, the server responds, and the request is completed once. However, in THE SSE scenario, the client initiates a request, the connection is always maintained, and the server can return data to the client once it has data, which can be returned at multiple intervals

2. Characteristic analysis

The biggest feature of SSE can be simply planned as two

  • A long connection
  • The server can push information to the client

Those of you who know websocket probably know that it is also a long connection that can push messages, but there is one obvious difference

Sse is a single channel. Only the server can send messages to the client. And webscoket is a two-channel

So why make sse with webscoket? Since it exists, it must have its advantages

sse websocket
The HTTP protocol Standalone WebSocket protocol
Light weight and easy to use Relatively complicated
Reconnection is supported by default You need to disconnect and reconnect yourself
Text transmission Binary transmission
Supports user-defined message types to be sent

3. Application scenarios

From the characteristics of SSE, we can roughly determine its application scenarios. It can be used in most cases where polling is required to obtain the latest data of the server

For example, the real-time number of people who show the current website online, the legal currency exchange rate shows the current real-time exchange rate, the real-time transaction amount of e-commerce greatly promoted, and so on…

II. Manually implement sse

Sse itself has its own set of gameplay, which will be explained later. This section mainly focuses on the two characteristics of SSE: long connection + back-end push data. If we implement such an interface by ourselves, what can we do?

1. Project creation

To create a demonstration project with SpringBoot 2.2.1.release, the core XML dependencies are as follows

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.1. RELEASE</version>
    <relativePath/> <! -- lookup parent from repository -->
</parent>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
</properties>

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

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </pluginManagement>
</build>
<repositories>
    <repository>
        <id>spring-snapshots</id>
        <name>Spring Snapshots</name>
        <url>https://repo.spring.io/libs-snapshot-local</url>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </repository>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/libs-milestone-local</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
    <repository>
        <id>spring-releases</id>
        <name>Spring Releases</name>
        <url>https://repo.spring.io/libs-release-local</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>
Copy the code

2. Function realization

Http1.1 supports long connections. Add a Connection: keep-alive to the request header

Here we use asynchronous request to achieve SSE function, as for what is asynchronous request, recommend to check the blog: 【WEB series 】 asynchronous request knowledge point and use posture summary

Since the back end can return data irregularly, we need to be careful that we need to keep the connection, do not return data once and then disconnect; Content-type: text/event-stream; Charset =UTF-8 (what if it’s not a stream?)

// Create a new container and save the connection for output return
private Map<String, PrintWriter> responseMap = new ConcurrentHashMap<>();

// Send data to the client
private void writeData(String id, String msg, boolean over) throws IOException {
    PrintWriter writer = responseMap.get(id);
    if (writer == null) {
        return;
    }

    writer.println(msg);
    writer.flush();
    if(over) { responseMap.remove(id); }}/ / push
@ResponseBody
@GetMapping(path = "subscribe")
public WebAsyncTask<Void> subscribe(String id, HttpServletResponse response) {

    Callable<Void> callable = () -> {
        response.setHeader("Content-Type"."text/event-stream; charset=UTF-8");
        responseMap.put(id, response.getWriter());
        writeData(id, "Subscription successful".false);
        while (true) {
            Thread.sleep(1000);
            if(! responseMap.containsKey(id)) {break; }}return null;
    };

    WebAsyncTask is used to handle timeouts and errors and to specify the Excutor name to use
    WebAsyncTask<Void> webAsyncTask = new WebAsyncTask<>(30000, callable);
    // Note: onCompletion means completion, and this function will be executed regardless of whether you time out or throw an exception
    webAsyncTask.onCompletion(() -> System.out.println("Callback completed by program [normal execution]"));

    // These two returned contents will eventually be put into response ===========
    webAsyncTask.onTimeout(() -> {
        responseMap.remove(id);
        System.out.println("Over time!!");
        return null;
    });
    // note: this is new for Spring5
    webAsyncTask.onError(() -> {
        System.out.println("An exception!!");
        return null;
    });


    return webAsyncTask;
}
Copy the code

If you look at the implementation above, it’s basically the same logic as an asynchronous request. If you look at the logic in callable, there’s a while loop to keep a long connection going

Next, we add two interfaces to simulate a scenario where the back end sends a message to the client to close the connection

@ResponseBody
@GetMapping(path = "push")
public String pushData(String id, String content) throws IOException {
    writeData(id, content, false);
    return "over!";
}

@ResponseBody
@GetMapping(path = "over")
public String over(String id) throws IOException {
    writeData(id, "over".true);
    return "over!";
}
Copy the code

Let’s briefly demonstrate how this works

III. SseEmitter

Sse has its own specifications, but our implementation above does not manage this. The problem is that the front end requests data according to the gameplay of SSE, so it may not work properly

1. The sse specification

In the definition of HTML5, the server SSE generally needs to follow the following requirements

Request header

Enable long connection + stream transfer

Content-Type: text/event-stream; charset=UTF-8 Cache-Control: no-cache Connection: keep-aliveCopy the code

The data format

The message sent by the server consists of the following format:

field:value\n\n
Copy the code

Field has five possibilities

  • Empty: that is:The beginning, indicating a comment, can be interpreted as a heartbeat sent by the server to the client to ensure that the connection is not broken
  • Data: data
  • Event: indicates the event. Default value
  • Id: Data identifier is represented by the ID field, which is equivalent to the number of each piece of data
  • Retry: reconnection time

2. Implement

SpringBoot uses SseEmitter to support SSE, which can be said to be very simple, directly return the SseEmitter object; Let me rewrite the logic above

@RestController
@RequestMapping(path = "sse")
public class SseRest {
    private static Map<String, SseEmitter> sseCache = new ConcurrentHashMap<>();
    @GetMapping(path = "subscribe")
    public SseEmitter push(String id) {
        // The timeout is set to 1 hour
        SseEmitter sseEmitter = new SseEmitter(3600_000L);
        sseCache.put(id, sseEmitter);
        sseEmitter.onTimeout(() -> sseCache.remove(id));
        sseEmitter.onCompletion(() -> System.out.println("Done!!"));
        return sseEmitter;
    }

    @GetMapping(path = "push")
    public String push(String id, String content) throws IOException {
        SseEmitter sseEmitter = sseCache.get(id);
        if(sseEmitter ! =null) {
            sseEmitter.send(content);
        }
        return "over";
    }

    @GetMapping(path = "over")
    public String over(String id) {
        SseEmitter sseEmitter = sseCache.get(id);
        if(sseEmitter ! =null) {
            sseEmitter.complete();
            sseCache.remove(id);
        }
        return "over"; }}Copy the code

The above implementation uses several methods of SseEmitter, as explained below

  • send(): sends data if the incoming is a nonSseEventBuilderObject, then the pass parameter is encapsulated in data
  • complete(): indicates that the connection is disconnected after the execution is complete
  • onTimeout(): Indicates that the timeout callback is triggered
  • onCompletion(): Triggers a callback after completion

Also demonstrate access requests

The overall effect is similar to the previous one, and the output is prefixed. Next, let’s write a simple HTML consumer to demonstrate more features of the full SSE

<! doctypehtml>
<html lang="en">
<head>
    <title>Sse Test Documentation</title>
</head>
<body>
<div>Sse test</div>
<div id="result"></div>
</body>
</html>
<script>
    var source = new EventSource('http://localhost:8080/sse/subscribe? id=yihuihui');
    source.onmessage = function (event) {
        text = document.getElementById('result').innerText;
        text += '\n' + event.data;
        document.getElementById('result').innerText = text; }; <! -- Add an open callback --> source.onopen =function (event) {
        text = document.getElementById('result').innerText;
        text += '\n 开启: ';
        console.log(event);
        document.getElementById('result').innerText = text;
    };
</script>
Copy the code

Place the above HTML file under the resources/static directory of your project; Then modify the previous SseRest

@Controller
@RequestMapping(path = "sse")
public class SseRest {
    @GetMapping(path = "")
    public String index(a) {
        return "index.html";
    }

    @ResponseBody
    @GetMapping(path = "subscribe", produces = {MediaType.TEXT_EVENT_STREAM_VALUE})
    public SseEmitter push(String id) {
        // Set the timeout period to 3s to demonstrate automatic client reconnection
        SseEmitter sseEmitter = new SseEmitter(1_000L);
        // Set the front-end retry time to 1s
        sseEmitter.send(SseEmitter.event().reconnectTime(1000).data("Connection successful"));
        sseCache.put(id, sseEmitter);
        System.out.println("add " + id);
        sseEmitter.onTimeout(() -> {
            System.out.println(id + "Timeout");
            sseCache.remove(id);
        });
        sseEmitter.onCompletion(() -> System.out.println("Done!!"));
        returnsseEmitter; }}Copy the code

We set the timeout time to be short to test the automatic reconnection of the client. As follows, the log enabled is increasing

Second, set SseEmitter to a longer timeout and try pushing data

Note the above demonstration that when the backend terminates the long connection, the client automatically reconnects without writing additional retry logic, just like magic

3. Summary

This article introduces SSE and its advantages compared with Websocket.

Please note that although this article introduces two kinds of SSE, the first kind of asynchronous request to achieve, if you need to complete SSE specification requirements, you need to do some adaptation, if you need to understand sse underlying implementation principle, you can refer to; In actual business development, SseEmitter is recommended

IV. The other

0. Project

Series of blog posts

  • 200329-SpringBoot series of web tutorials on asynchronous request knowledge and use posture summary
  • 200105-SpringBoot Web Tutorial Customization returns Http-Code n postures
  • 191222- Custom RequestCondition in SpringBoot Tutorial Web
  • 191206-SpringBoot Web Listener four registration postures
  • 191122- Four postures for Servlet registration in SpringBoot Tutorial Web part
  • 191120- Enabling GZIP data compression in the Web part of the SpringBoot series
  • 191018-SpringBoot Web tutorial Filter Usage Guide extension
  • 191016-SpringBoot Tutorial web guide to Filter usage
  • 191012- Custom Exception Handling HandlerExceptionResolver in the Web part of the SpringBoot tutorial series
  • 191010- Global exception handling in SpringBoot Tutorial Web
  • 190930- Configuration of 404 and 500 exception pages in SpringBoot Tutorial Web
  • 190929- Redirection of the SpringBoot Tutorial Web chapter
  • 190913-SpringBoot tutorial Web returns text, web pages, and image manipulation postures
  • 190905-SpringBoot Series tutorial Web Chinese garble problem solving
  • 190831- How to Customize the Parameter Parser in SpringBoot Tutorial Web
  • 190828- Summary of Post request parameter resolution postures in the Web part of the SpringBoot tutorial series
  • 190824-SpringBoot Tutorial Web chapter summary of Get Request parameter parsing posture
  • 190822- Beetl Environment Setup in SpringBoot Tutorial Web
  • 190820- Thymeleaf Setup for SpringBoot Tutorial Web
  • 190816- Freemaker Environment setup in SpringBoot Tutorial Web
  • 190421-SpringBoot Advanced WEB WebSocket instructions
  • 190327-Spring-resttemplate urlencode parameter parsing exception
  • 190317-Spring MVC web application building based on Java Config without XML configuration
  • 190316-Spring MVC web Application Building based on XML configuration
  • 190213- The temporary upload location XXX is not valid

The source code

  • Project: github.com/liuyueyi/sp…
  • Project source: github.com/liuyueyi/sp…

1. An ashy Blog

As far as the letter is not as good, the above content is purely one’s opinion, due to the limited personal ability, it is inevitable that there are omissions and mistakes, if you find bugs or have better suggestions, welcome criticism and correction, don’t hesitate to appreciate

Below a gray personal blog, record all the study and work of the blog, welcome everyone to go to stroll

  • A grey Blog Personal Blog blog.hhui.top
  • A Grey Blog-Spring feature Blog Spring.hhui.top