【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
- use
SseEmitter
Implement 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 nonSseEventBuilder
Object, then the pass parameter is encapsulated in datacomplete()
: indicates that the connection is disconnected after the execution is completeonTimeout()
: Indicates that the timeout callback is triggeredonCompletion()
: 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