Reading target: This article is suitable for SpringBoot beginners and children interested in SpringBoot reading.

Background: In the development of enterprise-level WEB applications, in order to better user experience & improve response speed, some time-consuming and laborious requests (Excel import or export, complex calculation, etc.) are often processed asynchronously. An important problem arising from this is how to inform users of task status ***. Common methods can be roughly divided into 2 categories and 4 types:

  • HTTP Polling client pull
  • HTTP Long-Polling client pull
  • Server-Sent Events (SSE) server push
  • WebSocket server push

1. Polling

Is a very simple implementation. The client repeatedly requests new messages from the server through a scheduled task, and the server provides one or more messages in chronological order that have occurred since the last request.

The advantage of short polling is that it is easy to implement. This works well when there is very little data in both directions and the request interval is not very dense. For example, news comment messages can be updated every half minute, which is fine for users.

Its disadvantages are also very obvious, once we have a very high demand for real-time data, in order to ensure the timely delivery of messages, the request interval must be shortened, in this case, it will increase the waste of server resources, reduce service availability. Another disadvantage is that when the number of messages is small, there will be a large number of requests that do not work, which will lead to a waste of server resources.

2. Long-polling

The official definition of long polling is:

The server attempts to “hold open” (notimmediately reply to) each HTTP request, responding only when there are events to deliver. In this way, there is always a pending request to which the server can reply for the purpose of delivering events as they occur, thereby minimizing the latency in message delivery.

Compared with Polling, it can be found that the advantage of long-polling is to reduce useless requests by holding open HTTP requests.

The general steps are as follows:

  1. The client makes a request to the server and waits for a response.
  2. The server blocks the request and constantly checks for new messages. Returns immediately if a new message is generated during this period. Otherwise wait untilThe request timeout.
  3. When the clientA new message is obtainedorThe request timeoutTo process the message and initiate the next request.

One of the disadvantages of long-polling is also a waste of server resources, because it, like Polling, is a passive acquisition, requiring constant requests to the server. With high concurrency, server performance can be severely tested.

Note: Since the above two methods are relatively simple to implement, we will not do the code demonstration here. Next we will focus on Server-sent Events and WebSockets.

3. The Demo summary

Next we will use a *** download file *** case to demonstrate SSE and WebSocket message push, before that, let’s briefly talk about the structure of our project, the whole project based on SpringBoot build.

First we define an APIDownloadController for the front end to access

@RestController
public class DownloadController {
    private static final Logger log = getLogger(DownloadController.class);
    @Autowired
    private MockDownloadComponent downloadComponent;  

    @GetMapping("/api/download/{type}")
    public String download(@PathVariable String type, HttpServletRequest request) {  // (A)
        HttpSession session = request.getSession();
        String sessionid = session.getId();
        log.info("sessionid=[{}]", sessionid);
        downloadComponent.mockDownload(type, sessionid);  // (B)
        return "success"; // (C)}}Copy the code
  • (A) typeParameter is used to distinguish which push method is used, where issse.ws.stompThese three types.
  • (B) MockDownloadComponentUsed to asynchronously simulate the process of downloading a file.
  • (C) Because the download process is asynchronous, the method is not blocked and immediately returned to the clientsuccessUsed to indicateDownload begins.

In DownloadController we call the mockDownload() method of MockDownloadComponent to simulate the real download logic.

@Component
public class MockDownloadComponent {
    private static final Logger log = LoggerFactory.getLogger(DownloadController.class);

    @Async // (A)
    public void mockDownload(String type, String sessionid) {
        for (int i = 0; i < 100; i++) {
            try {
                TimeUnit.MILLISECONDS.sleep(100); // (B)

                int percent = i + 1;
                String content = String.format("{\"username\":\"%s\",\"percent\":%d}", sessionid, percent); // (C)
                log.info("username={}'s file has been finished [{}]% ", sessionid, percent);

                switch (type) { // (D)
                    case "sse":
                        SseNotificationController.usesSsePush(sessionid, content);
                        break;
                    case "ws":
                        WebSocketNotificationHandler.usesWSPush(sessionid, content);
                        break;
                    case "stomp":
                        this.usesStompPush(sessionid, content);
                        break;
                    default:
                        throw new UnsupportedOperationException(""); }}catch(InterruptedException e) { e.printStackTrace(); }}}}Copy the code
  • (A) We use@AsyncTo make itasynchronous.
  • (B) Simulated download time.
  • (C) The format of the message is{"username":"abc","percent":1}.
  • (D) According to differenttypeSelect the notification push mode.

4. Server-Sent Events

SSE is a set of API specifications defined by W3C, which enables servers to push data to Web pages over HTTP. It has the following features:

  • Unidirectional half-duplex: Only the server pushes messages to the client
  • Http-based: Data is encoded as “text/event-stream” content and transmitted using HTTP streaming mechanisms
  • Data format unrestricted: Messages are just a set of messages that follow the specification definitionkey-valueFormat &UTF-8Encode text data streams we can in messagespayloadCan be used inJSONorXMLOr custom data formats.
  • HTTP long connection: The actual delivery of the message is done over a long-standing HTTP connection, consuming fewer resources
  • Easy to use API

Browser support:

Note: Internet Explorer can support SSE through third-party JS libraries

4.1 Using SSE in SpringBoot

Since Spring 4.2 SSE specifications have been supported, we just need to return the SseEmitter object in the Controller.

Note: Spring Webflux is provided in Spring 5 to make SSE more convenient, but to be closer to our actual project, this text only demonstrates using Spring MVC SSE.

On the server side we define a SseNotificationController for and client processing and save the SSE connection. The endpoint is/API/SSE-notification.

@RestController
public class SseNotificationController {

    public static final Map<String, SseEmitter> SSE_HOLDER = new ConcurrentHashMap<>(); // (A)

    @GetMapping("/api/sse-notification")
    public SseEmitter files(HttpServletRequest request) {
        long millis = TimeUnit.SECONDS.toMillis(60);
        SseEmitter sseEmitter = new SseEmitter(millis); // (B)

        HttpSession session = request.getSession();
        String sessionid = session.getId();

        SSE_HOLDER.put(sessionid, sseEmitter); 
        return sseEmitter;
    }

    /** * Use the sessionId to obtain the corresponding client to push messages */
    public static void usesSsePush(String sessionid, String content) {  // (C)
        SseEmitter emitter = SseNotificationController.SSE_HOLDER.get(sessionid);
        if (Objects.nonNull(emitter)) {
            try {
                emitter.send(content);
            } catch (IOException | IllegalStateException e) {
                log.warn("sse send error", e); SseNotificationController.SSE_HOLDER.remove(sessionid); }}}}Copy the code
  • (A) SSE_HOLDERAll clients are savedSseEmitterIs used to notify corresponding clients.
  • (B) Create one based on the specified timeoutSseEmitterObject, which is a class provided by SpringMVC to manipulate SSE.
  • (C) usesSsePush()Sends messages to corresponding clients based on the sessionId. Sending is just a callSseEmitterthesend()Method can.

Now that the server is complete, we use Vue to write the client download.html for testing. The core code is as follows:

usesSSENotification: function () { var tt = this; var url = "/api/sse-notification"; var sseClient = new EventSource(url); // (A) sseClient.onopen = function () {... }; // (B) sseClient.onmessage = function (msg) { // (C) var jsonStr = msg.data; console.log('message', jsonStr); var obj = JSON.parse(jsonStr); var percent = obj.percent; Tt.ssemsg += 'SSE '+ percent + "%\r\n"; if (percent === 100) { sseClient.close(); // (D) } }; sseClient.onerror = function () { console.log("EventSource failed."); }; }Copy the code
  • (A) Open A new SSE Connection and access it/api/sse-notification.
  • (B) Callback when the connection is successful.
  • (C) Callback when there is a new message.
  • (D) Close the connection when the download progress reaches 100%.

Effect demonstration:

4. WebSocket

WebSocket is similar to the standard TCP connection. It is a communication mode defined by IETF (RFC 6455) for real-time full-duplex communication over TCP, which means it has more powerful functions and is often used in stock ticker and chat applications.

Compared to SSE, it can not only communicate bidirectional, but can even handle binary content such as audio/video.

Note: With WebSocket, in high concurrency, the server will have many long connections. This is a performance challenge for both network proxy layer components and WebSocket servers, and we need to consider its load balancing solution. At the same time, connection security and other issues can not be ignored.

4.1 Spring WebSocket (Low-level API)

Spring 4 provides a new Spring-WebSocket module for various WebSocket engines that is compatible with the Java WebSocket API standard (JSR-356) and provides additional enhancements.

Note: Direct use of the WebSocket API for applications can be significantly more difficult to develop, so Spring provides us with STOMP over WebSocket’s higher level API to use WebSocket. This article will demonstrate the low level API and higher level API respectively.

If you want to use WebSocket in SpringBoot, you first need to introduce the spring-boot-starter-websocket dependency

     <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
Copy the code

You can then configure the information, which we will demonstrate using the Low Level API.

First you need to customize a WebSocketNotificationHandler used to handle the WebSocket connection and message processing. We just need to implement WebSocketHandler or subclass TextWebSocketHandler BinaryWebSocketHandler.

public class WebSocketNotificationHandler extends TextWebSocketHandler {

    private static final Logger log = getLogger(WebSocketNotificationHandler.class);

    public static final Map<String, WebSocketSession> WS_HOLDER= new ConcurrentHashMap<>();  // (A)


    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {   // (B)
        String httpSessionId = (String) session.getAttributes().get(HttpSessionHandshakeInterceptor.HTTP_SESSION_ID_ATTR_NAME);
        WS_HOLDER.put(httpSessionId, session);
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        log.info("handleTextMessage={}", message.getPayload()); 
    }

    public static void usesWSPush(String sessionid, String content) {    // (C)
        WebSocketSession wssession = WebSocketNotificationHandler.WS_HOLDER.get(sessionid);
        if (Objects.nonNull(wssession)) {
            TextMessage textMessage = new TextMessage(content);
            try {
                wssession.sendMessage(textMessage);
            } catch(IOException | IllegalStateException e) { WebSocketNotificationHandler.SESSIONS.remove(sessionid); }}}}Copy the code
  • (A) WS_HOLDERUsed to save the clientWebSocket Session
  • (B)afterConnectionEstablished()Method, when the connection is established, presssessionIdwillWebSocket SessionSave toWS_HOLDERIs used to push messages to clients.
  • (C) according tosessionIdTo obtain the correspondingWebSocket SessionAnd callWebSocket SessionthesendMessage(textMessage)Method to send a message to the client.

Use @enablewebsocket to EnableWebSocket and implement WebSocketConfigurer to configure WebSocket.

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {

        WebSocketNotificationHandler notificationHandler = new WebSocketNotificationHandler(); 
        
        registry.addHandler(notificationHandler, "/ws-notification") // (A)
                .addInterceptors(new HttpSessionHandshakeInterceptor())  // (B)
                .withSockJS();  // (C)}}Copy the code
  • (A) will be our customWebSocketNotificationHandlerRegistered toWebSocketHandlerRegistry.
  • (B) HttpSessionHandshakeInterceptorIs a built-in interceptor for passing HTTP session attributes to a WebSocket session. Of course you can passHandshakeInterceptorInterface implements its own interceptor.
  • (C) Enable support for SockJS. The goal of SockJS is to let applications use the WebSocket API. When it is found that the browser does not support it, the non-WebSocket alternative can be used without changing any code and simulate WebSocket as much as possible. More information about SockJS can be found at github.com/sockjs/sock…

Now that the server side is basically done, let’s improve the client side download.html, its core method is as follows:

usesWSNotification: function () {
                var tt = this;
                var url = "http://localhost:8080/ws-notification";
                var sock = new SockJS(url);   // (A)
                sock.onopen = function () {
                    console.log('open');
                    sock.send('test');
                };

                sock.onmessage = function (msg) {   // (B)
                    var jsonStr = msg.data;

                    console.log('message', jsonStr);

                    var obj = JSON.parse(jsonStr);
                    var percent = obj.percent;
                    tt.wsMsg += 'WS notifies you: Download completed ' + percent + "%\r\n";
                    if (percent === 100) { sock.close(); }}; sock.onclose =function () { 
                    console.log('ws close');
                };
            }
Copy the code
  • (A) First you need to introduce the SockJS Client into the project and create A SockJS object based on the specified URL.
  • (B) when there is new informationcallbackWe can process our messages in this method.

Effect demonstration:

4.2 STOMP over WebSocket (Advanced API)

Although WebSocket defines two types of messages, text and binary, the content of the message is not defined. In order to facilitate the processing of messages, we hope that the Client and Server need to agree on some protocol to facilitate the processing of messages. So, are there any wheels that have been built? The answer must be yes. This is STOMP.

STOMP is a simple text-oriented messaging protocol that is essentially a protocol for message queues, parallel to AMQP and JMS. It just happens to be simple enough to define the message physique of WS. Although STOMP is a text-oriented protocol, the content of a message can also be binary data. STOMP can also use any reliable two-way flow network protocol, such as TCP and WebSocket, and many server message queues already support STOMP, such as RabbitMQ, ActiveMQ, etc.

Its structure is a frame-based protocol, where a frame consists of a command, an optional set of headers, and an optional Body.

COMMAND
header1:value1
header2:value2

Body^@
Copy the code

Clients can SEND or SUBSCRIBE messages using SEND or SUBSCRIBE commands. A publish-subscribe mechanism similar to MQ is formed by documenting who should receive and process messages through the destination label.

STOMP’s advantages are also clear, namely:

  1. There is no need to create a custom message format
  2. We can use the existing stomp.js client
  3. Message routing and broadcast can be realized
  4. Mature third party message broker middleware such as RabbitMQ, ActiveMQ etc can be used

Most importantly, Spring STOMP provides us with a programming model that works like Spring MVC, reducing our learning costs.

Let’s tweak our DEMO to use Spring STOMP to implement message push. In this case, we’ll use the SimpleBroker pattern. Our application will have a built-in STOMP Broker to store all information in memory.

The specific code is as follows:

@Configuration
@EnableWebSocketMessageBroker  // (A)
public class WebSocketBrokerConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {

        registry.addEndpoint("/ws-stomp-notification")
                .addInterceptors(httpSessionHandshakeInterceptor())   // (B)
                .setHandshakeHandler(httpSessionHandshakeHandler())  // (C)
                .withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setApplicationDestinationPrefixes("/app")  // (D)
                .enableSimpleBroker("/topic"."/queue");  // (E)
    }

    @Bean
    public HttpSessionHandshakeInterceptor httpSessionHandshakeInterceptor(a) {
        return new HttpSessionHandshakeInterceptor();
    }

    @Bean
    public HttpSessionHandshakeHandler httpSessionHandshakeHandler(a) {
        return newHttpSessionHandshakeHandler(); }}Copy the code
  • (A) use@EnableWebSocketMessageBrokerAnnotations enable STOMP support
  • (B) Create an interceptor that passes HTTP session attributes to a WebSocket session.
  • (C) Configure a customHttpSessionHandshakeHandler, its main function is to identify the connection by the sessionId tag.
  • (D) Set the message processor route prefix when the message’sdestinationhas/appAt the beginning, the message is routed to the corresponding message processing method on the server side. *** (meaningless in this case) ***
  • (E) Set the path prefix for client subscription messages

HttpSessionHandshakeHandler code is as follows:

public class HttpSessionHandshakeHandler extends DefaultHandshakeHandler {

    @Override
    protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
        String sessionId = (String) attributes.get(HttpSessionHandshakeInterceptor.HTTP_SESSION_ID_ATTR_NAME);
        returnnew HttpSessionPrincipal(sessionId); }}Copy the code

When we need to send a message to the client, all we need to do is inject the SimpMessagingTemplate object. Does that sound familiar? ! Yes, this Template pattern is the same as the RestTemplate JDBCTemplate we use everyday. We simply call the convertAndSendToUser() method of SimpMessagingTemplate to send the message to the corresponding user.

  private void usesStompPush(String sessionid, String content) {
        String destination = "/queue/download-notification";
        messagingTemplate.convertAndSendToUser(sessionid, destination, content);
    }
Copy the code

On the browser side, clients can use stomp.js and sockjs-client to connect as follows:

usesStompNotification: function () {
                var tt = this;
                var url = "http://localhost:8080/ws-stomp-notification"; // public topic // var notificationTopic ="/topic/download-notification"; // Point-to-point broadcast var notificationTopic ="/user/queue/download-notification"; // (A)

                var socket = new SockJS(url);
                var stompClient = Stomp.over(socket);

                stompClient.connect({}, function (frame) {
                    console.log("STOMP connection successful");

                    stompClient.subscribe(notificationTopic, function (msg) {   // (B)
                        var jsonStr = msg.body;

                        var obj = JSON.parse(jsonStr);
                        var percent = obj.percent;
                        tt.stompMsg += 'STOMP notifies you: Download completed ' + percent + "%\r\n";
                        if (percent === 100) {
                            stompClient.disconnect()
                        }

                    });

                }, function (error) {
                    console.log("STOMP protocol error " + error)
                })
            }
Copy the code
  • (A) If we want to receive messages for A specific user, we need to/user/Is prefixed by Spring STOMP/user/The message is given as a prefixUserDestinationMessageHandlerProcess it and send it to a specific user, of course/user/Yes, it can go throughWebSocketBrokerConfigFor the sake of simplicity, we use the default configuration here, so our topic URL is/user/queue/download-notification.
  • (B) settingstompClientMessage processing Callback performs message processing.

Effect demonstration:

5 concludes

In this paper, we simply explain several commonly used message push schemes, and through a download case focus on demonstrating SSE and WebSocket two server push mode of message push. Of course, there are a lot of details are not explained in the article, I suggest you download the source code reference.

Compared with these modes, Xiaobao thinks that if our demand is only to push messages to the client, SSE will be more cost-effective, followed by long-polling. Using WebSocket is a bit of an overkill and adds more complexity to our system than it’s worth, so it’s not recommended. Polling is the simplest and most compatible, but its efficiency is too low, so it is not recommended to use. Of course, if you have other opinions, welcome to leave a message to discuss and exchange.

Article example source: github.com/leven-space…

If you think this article is useful, please leave your small 💗💗, I am a Java pupil, welcome to ridicule message.