Re learn the server push technology of Springboot series

Re learn the server push technology of Springboot series

Technical description of mainstream server push

Demand and background

A few years ago, all requests were initiated by the browser, and the browser itself did not have the ability to accept requests. Therefore, some special requirements are implemented by ajax polling. For example:

  • The share price display page obtains the share price update in real time
  • Live text broadcast of the event to update the situation in real time
  • Start a task through the page. The front end wants to know the real-time running status of the task background

The usual approach is to frequently establish an http connection to the server at small intervals, ask for updates to the task status, and then refresh the page display status. But the consequence of doing so is to waste a lot of traffic, causing great pressure on the server.

Common technologies of server push

After html5 has been widely promoted, we can use the server to actively push data and the browser to receive data to solve the above problems. Let's introduce two server-side data push technologies

Full duplex communication: WebSocket

Full duplex, full duplex is two-way communication. If the http protocol is a call between "walkie talkies" (you say one thing and I say one thing, there is a return), then our websocket is a mobile phone (you can send and receive information at any time, that is, full duplex).

In essence, it is an additional tcp connection. The handshake uses http protocol when establishing and closing, and other data transmission does not use http protocol. It is more complex. It is more suitable for complex two-way real-time data communication. Customer service and chat rooms on web pages are generally developed using WebSocket protocol.

Server active push: SSE (Server Send Event)

The new html5 standard is used to push data from the server to the browser in real time. It is directly established on the current http connection. In essence, it is to maintain a long http connection and lightweight protocol. The client sends a request to the server. The server keeps the request connected until a new message is ready and returns the message to the client. This connection will be maintained unless it is actively closed.

  • Establish connection
  • Server - > browser (connection hold)
  • Close connection

A major feature of SSE is to reuse one connection to receive messages sent by the server (also known as event s), so as to avoid continuous polling requests to establish a connection, resulting in a shortage of service resources.

Comparison between websocket and SSE

However, IE and Edge browsers do not support SSE, so there are few current application scenarios for SSE. Although websocket is not compatible with many older browsers, it is generally better than SSE. In addition, some open-source JS front-end products, such as SockJS and Socket.IO, provide a better programming experience of websocket front-end JS on the browser side, which is better than that of the browser Compatibility.

Server push event SSE

Simulate network payment scenario

We should all have used the payment system, such as scanning code to pay after buying a product on Taobao. Let's see how to realize this process in combination with SSE.

  • The user scan code is paid to the payment system (Alipay).
  • After the payment is completed, inform the merchant system (Taobao seller system) that I have initiated the payment (establish SSE connection)
  • The payment system (Alipay) tells the merchant system (Taobao seller system) that the user really paid for it.
  • The merchant system (Taobao seller system) sends a message to the user: you have paid successfully, jump to the payment success page. (through SSE connection, the server informs the user of the client browser)

Note: in the operation of returning the final payment result, the event push from the server to the client can be realized by SSE

Application scenario

Starting from the characteristics of sse, we can roughly judge its application scenario. It is mostly available for case s that need to poll to obtain the latest data from the server

For example, display the real-time number of people online on the current website, display the current real-time exchange rate in French currency, promote the real-time transaction volume of e-commerce, and so on

sse specification

In the definition of html5, the server sse generally needs to comply with the following requirements

Request header

Enable long connection + stream transfer

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

data format

The message sent by the server consists of message, and its format is as follows:

field:value\n\n

There are five possibilities for field

empty: Namely:The beginning indicates a comment, which can be understood as the heartbeat sent by the server to the client to ensure that the connection is not interrupted
data: data
event: Event, default
id: For data identifier id Field, equivalent to the number of each piece of data
retry: Reconnection time

Simulation Implementation

If you can't understand the following code, look back at this figure

We write code to simulate the implementation of the four steps 2, 3 and 4 in the above sequence diagram.

Browser front end implementation

For the data sent by the server to the browser, the browser needs to use the EventSource object in JavaScript for processing. EventSource uses the standard event listener method, and only needs to add the corresponding event processing method on the object. EventSource provides three standard events

In addition to using standard event handling methods, you can also use the addEventListener method to listen to events.

var es = new EventSource('Event source name') ;  //Establish a connection with the event source
//Standard event handling methods include onopen and onerror
es.onmessage = function(e) {
};
//You can listen to custom event names
es.addEventListener('Custom event name', function(e) {
});

ssetest.html (user payment page of merchant system)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>SSE</title>
</head>
<body>
<div id = "message">

</div>
<script>
    if (window.EventSource) { //Determine whether the browser supports SSE
        //Step 2: take the initiative to establish a long connection, indicating that the user has initiated payment
        let source = new EventSource(
            'http://localhost/dhy/orderpay?payid=1');
        let innerHTML = '';

        //Listen for events sent from the server: open
        source.onopen = function(e) {
            console.log("Connection establishment")
            innerHTML += "onopen: Ready to start receiving server data" + "<br/>"; //Payment results
            document.getElementById("message").innerHTML = innerHTML;
        };
        //Listen for events sent from the server: message
        source.onmessage = function(e) {
            console.log("The message sent by the server is: "+e)
            innerHTML += "onmessage:" + e.data + "<br/>"; //Payment results
            document.getElementById("message").innerHTML = innerHTML;
        };
        //Customize the finish event and actively close the EventSource
        source.addEventListener('finish', function(e) {
            console.log("Events sent by the server: "+e)
            source.close();
            innerHTML += "After receiving the payment result, notify the server to close EventSource" +  "<br/>";
            document.getElementById("message").innerHTML = innerHTML;
        }, false);
        //Listen for events from the server: error
        source.onerror = function(e) {
            console.log("Exception from server: "+e)
            if (e.readyState === EventSource.CLOSED) {
                innerHTML += "sse Connection closed" +  "<br/>";
            } else {
                console.log(e);
            }
        };
    } else {
        console.log("Your browser does not support SSE");
    }
</script>

</body>
</html>

Server implementation

Controller code (merchant system server code)

@RestController
public class SSEControler {
    //After creation, save SseEmitter to ConcurrentHashMap according to the order id
    //Normally, it should be stored in the database to generate database orders. Here we just simulate it
    public static final ConcurrentHashMap<Long, SseEmitter> sseEmitters
            = new ConcurrentHashMap<>();

    //Step 2: accept the user to establish a long connection, indicating that the user has paid, and the order can be generated after payment (unconfirmed status)
    @GetMapping("/orderpay")
    public SseEmitter orderpay(@RequestParam Long payid) throws IOException {
        System.out.println("=======orderpay Method execution========");
        //Set the default timeout of 3 seconds. After the timeout, the server actively closes the connection.
        //Timeout refers to the time interval when the server does not send data to the client
        SseEmitter emitter = new SseEmitter(3 * 1000L);
        sseEmitters.put(payid,emitter);
        emitter.onTimeout(() -> sseEmitters.remove(payid));
        emitter.send(SseEmitter.event().reconnectTime(1000).data("Connection succeeded"));
        //Triggered by the callback interface after execution
        emitter.onCompletion(() -> System.out.println("Done!!!"));
        return emitter;
    }

    //Step 3: accept the payment result notification from the payment system, indicating that the user has paid successfully
    @GetMapping("/payback")
    public void payback (@RequestParam Long payid){
        System.out.println("=======payback Method execution========");
        //Take out the SSE connection
        SseEmitter emitter = sseEmitters.get(payid);
        try {
            //Step 4: the server informs the browser that the user has paid successfully
            emitter.send("User payment succeeded"); //The front-end message event is triggered.
            //Trigger the finish event customized by the front end
            emitter.send(SseEmitter.event().name("finish").id("6666").data("ha-ha"));
        } catch (IOException e) {
            emitter.completeWithError(e);   //Departure front-end onerror event
        }
    }
}

Introduction to SseEmitter api

  • send(): send data. If a non SseEventBuilder object is passed in, the passed parameters will be encapsulated in data
  • complete(): indicates that the connection will be disconnected after execution
  • onTimeout(): triggered by timeout callback
  • onCompletion(): callback trigger after completion

Access test

Simulation test step 2

User access http://localhost:8888/ssetest.html Page. The js code of ssetest.html page will be executed automatically,

let source = new EventSource( 'https://localhost:8888/orderpay?payid=1');

So as to simulate the user to inform the "merchant system" after the browser initiates payment: the user has initiated payment.

Simulation test step 3

Simulation of payment system (Alipay) with PostMan and interface to merchant system https://localhost:8888/payback?payid=1 Send a request, simulate the "payment system" to request from the merchant system developed by us, and inform the user that the payment is successful.

Simulation test step 4

The merchant system informs the user's browser that you have successfully paid (server data push). Automatically print out the information of "payment success" in the browser.

Because it is the first time to receive the data push from the server, the first line of text onopen in the figure is printed

Because the send message from the server is received, the second line of text onmessage in the figure is printed

The server triggered a custom finish event after data send, so the third line of text in the figure was printed

Global processing of connection timeout exception

@ExceptionHandler(AsyncRequestTimeoutException.class)
@ResponseBody
public String handleAsyncRequestTimeoutException(AsyncRequestTimeoutException e) {
    return SseEmitter.event().data("timeout!!").build().stream()
            .map(d -> d.getData().toString())
            .collect(Collectors.joining());
}

SSE technical recommended reference articles

[SringBoot WEB series] detailed explanation of events sent by SSE server

[SpringBoot WEB series] detailed explanation of events sent by SSE server

SSE Technology Details: a new HTML5 server push event technology

Two way real-time communication websocket

Integrate websocket

<!-- introduce websocket rely on -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

Enable websocket function

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

Compatible with HTTPS protocol

  • The ws protocol of WebSocket is implemented based on HTTP protocol
  • The wss protocol of WebSocket is implemented based on HTTPS protocol

Once the https protocol is used in your project, your websocket must use the WSS protocol. How to make the Spring Boot project support WSS protocol?

Refer to my previous article Configure HTTPS for Web container , add the following code on the basis of Tomcat customizer configuration in that section to support wss protocol.

@Bean
public TomcatContextCustomizer tomcatContextCustomizer() {

    return new TomcatContextCustomizer() {
        @Override
        public void customize(Context context) {
            context.addServletContainerInitializer(new WsSci(), null);
        }

    };
}

Fundamentals of WebSocket programming

Connection establishment

The front-end js sends a wss connection establishment request to the back-end

If the http protocol is used, it can be changed to ws

socket = new WebSocket("wss://localhost:8888/ws/asset");

The SpringBoot server WebSocket service receiving class is defined as follows:

@Component
@Slf4j
@ServerEndpoint(value = "/ws/asset")
public class WebSocketServer {  

Full duplex data interaction

Both front-end and back-end

  • Listen to the onopen event and handle the connection establishment event
  • Listen to the onmessage event and process the message data sent by the other party
  • The onclose event listens and handles connection closure
  • Listen to the onerror event and handle exceptions during interaction

Data transmission

Data exchange between browser and server

Front end JS

socket.send(message);

The back-end Java sends a message to a javax.websocket.Session user.

/** 
 * Send a message. Practice shows that the session changes every time the browser refreshes. 
 * @param session  session
 * @param message  news
 */  
private static void sendMessage(Session session, String message) throws IOException{
    session.getBasicRemote().sendText(String.format("%s (From Server,Session ID=%s)",message,session.getId()));
}  

One user sends mass messages to other users

The server sends messages to all online javax.websocket.Session users.

/** 
 * Mass messaging 
 * @param message  news
 */  
public static void broadCastInfo(String message) throws IOException {
    for (Session session : SessionSet) {  
        if(session.isOpen()){  
            sendMessage(session, message);  
        }  
    }  
}

Implementing chat software with websocket

WebSocketServer is the core code of this section, websocket server code

  • @ServerEndpoint(value = "/ ws/asset") indicates the interface service address of websocket
  • @OnOpen annotated method, which is called when the connection is established successfully
  • @OnClose annotated method, which is the method called for connection closure
  • The way to annotate @OnMessage is the way to call after receiving the client message.
  • @The method annotated with OnError is the method called when an exception occurs
@Component
@Slf4j
@ServerEndpoint(value = "/ws/asset")
public class WebSocketServer {  

    //Used to count the number of connected clients
    private static final AtomicInteger OnlineCount = new AtomicInteger(0);
    // The thread safe Set of the concurrent package is used to store the Session object corresponding to each client.  
    private static CopyOnWriteArraySet<Session> SessionSet = new CopyOnWriteArraySet<>();
    
    /** 
     * Method successfully called for connection establishment 
     */  
    @OnOpen
    public void onOpen(Session session) throws IOException {
        SessionSet.add(session);   
        int cnt = OnlineCount.incrementAndGet(); // Online number plus 1  
        log.info("There are connections joined. The current number of connections is:{}", cnt);
    }

    /**
     * Method of calling after receiving client message
     * @param message Messages sent by the client
     */
    @OnMessage
    public void onMessage(String message, Session session) throws IOException {
        log.info("Message from client:{}",message);
        sendMessage(session, "Echo Message content:"+message);
        // broadCastInfo(message);  Mass messaging
    }


    /** 
     * Method called for connection closure 
     */  
    @OnClose
    public void onClose(Session session) {  
        SessionSet.remove(session);  
        int cnt = OnlineCount.decrementAndGet();  
        log.info("There are connections closed. The current number of connections is:{}", cnt);  
    }  

    /** 
     * An error occurred
     */  
    @OnError
    public void onError(Session session, Throwable error) {  
        log.error("An error occurred:{},Session ID:  {}",error.getMessage(),session.getId());
    }  
  
    /** 
     * Send a message. Practice shows that the session changes every time the browser refreshes. 
     * @param session  session
     * @param message  news
     */  
    private static void sendMessage(Session session, String message) throws IOException {

        session.getBasicRemote().sendText(String.format("%s (From Server,Session ID=%s)",message,session.getId()));

    }  
  
    /** 
     * Mass messaging 
     * @param message  news
     */  
    public static void broadCastInfo(String message) throws IOException {
        for (Session session : SessionSet) {  
            if(session.isOpen()){  
                sendMessage(session, message);
            }  
        }  
    }
      
} 

Client code, do several experiments, naturally understand the meaning of the code. Don't look at the code first, look at the effect of the experiment through the browser first, so as to better understand the function of the code. public/wstest.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>websocket test</title>
    <style type="text/css">
        h3,h4{
            text-align:center;
        }
    </style>
</head>
<body>

<h3>Please enter the message to be sent to the server:</h3><br/>

<label for="text">Enter sending information</label><input id="text" type="text" />
<button onclick="sendToServer()">Send server message</button>
<button onclick="closeWebSocket()">Close connection</button>
<br>
information:
<span id="message">

</span>
<script type="text/javascript">
    var socket;
    if (typeof (WebSocket) == "undefined") {
        console.log("Sorry: your browser does not support WebSocket");
    } else {
        socket = new WebSocket("wss://localhost:8888/ws/asset");
        //Connection open event
        socket.onopen = function() {
            console.log("Socket Opened");
        };
        //Message received event
        socket.onmessage = function(msg) {
            document.getElementById('message').innerHTML += msg.data + '<br/>';
        };
        //Connection close event
        socket.onclose = function() {
            console.log("Socket Closed");
        };
        //An error event occurred
        socket.onerror = function() {
            alert("Socket An error has occurred");
        };

        //Close the connection when the window closes
        window.unload=function() {
            socket.close();
        };
    }

    //Close connection
    function closeWebSocket(){
        socket.close();
    }

    //Send message to server
    function sendToServer(){
        var message = document.getElementById('text').value;
        socket.send(message);
    }
</script>

</body>
</html>

test

Once the connection is closed, the message will not take effect

Refreshing the browser will cause the current long connection to close

Added by robmarston on Tue, 07 Dec 2021 14:30:59 +0200