From zero to build the development scaffold, Spring Boot realizes a variety of postures of WebSocket

Posted by TANK on Thu, 27 Jan 2022 01:52:31 +0100

brief introduction

WebSocket is a two-way, full duplex and persistent connection between Web browser and server. Once a WebSocket connection is established, the connection will remain open until the client or server decides to close the connection.

A typical use case might be when an application involves multiple users communicating with each other, such as in a chat. We will build a simple chat client in our example.

Introduction to Http, Websocket, SockJs and Stomp

Http

HTTP hypertext transfer protocol, HTTP has several versions of 1.0, 1.1 and 2.0, from http1 Since 1, keep alive is enabled by default to maintain the continuity of the connection. In short, when a web page is opened, the TCP connection between the client and the server for transmitting HTTP data will not be closed. If the client accesses the web page on the server again, it will continue to use the established connection, which reduces the consumption of resources and optimizes performance, However, keep alive also has a time limit. There is another client that can only actively initiate a request to obtain the returned data, and can not actively receive the data pushed by the background. websocket came into being.

Websocket

It is one of the newly added features of html5. The purpose is to establish a full duplex communication mode between the browser and the server, solve the excessive resource consumption caused by http request response, and provide a new implementation mode for special scenario applications, such as chat, stock trading, games and other industries with high real-time requirements.

Both http and websocket are based on TCP (transmission control protocol). Websocket can be regarded as a supplement to http protocol.

SockJs

SockJS is a JavaScript library. In order to solve the problem that many browsers do not support WebSocket protocol, an alternative SockJS is designed. SockJS is a simulation of WebSocket technology. SockJS will correspond to the WebSocket API as much as possible, but if the WebSocket technology is not available, it will be automatically reduced to the polling mode. SockJS will preferentially select WebSocket for connection, but when the server or client does not support WebSocket, it will automatically connect in XHR stream, XDR stream, iFrame event source, iFrame HTML file, XHR polling, XDR polling, iFrame XHR polling and JSONP polling.

SockJS is a JavaScript library. In order to solve the problem that many browsers do not support WebSocket protocol, an alternative SockJS is designed. SockJS is a simulation of WebSocket technology. SockJS will correspond to the WebSocket API as much as possible, but if the WebSocket technology is not available, it will be automatically reduced to the polling mode.

The SockJS client first sends GET /info to get basic information from the server. After that, it must decide which transmission method to use. If possible, use WebSocket. If not, in most browsers, there is at least one HTTP streaming option. If not, use HTTP (long) polling.

All transport requests have the following URL structure:

http://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}
  • {server ID} is used to route requests in the cluster, but not for other purposes.
  • {session ID} is associated with HTTP requests belonging to SockJS sessions.
  • {transport} indicates the transport type (such as websocket, XHR streaming, and others).

Github address: https://github.com/sockjs

Stomp

STOMP is a Streaming Text Oriented Messaging Protocol. It is a part of the WebSocket communication standard and belongs to the business layer control protocol Wire protocol, Protocol specification . Based on the general publish subscribe semantics, it provides reliable message delivery through begin, commit, rollback transaction sequence and ack confirmation mechanism. Because the protocol is simple and easy to implement, almost all programming languages have the client implementation of STOMP.

STOMP protocol to add appropriate message semantics for the communication between browser and server.

STOMP provides a frame based line format layer on WebSocket to define the semantics of messages.

In terms of transmission protocol, it supports both STOMP Over WebSocket and STOMP Over SockJS.

The STOMP frame consists of a command, one or more header information, and a payload. For example, the following is a STOMP frame for sending data:

>>> SEND
destination:/app/marco
content-length:20

{"message":"Maeco!"}

Relationship among WebSocket, SockJs and STOMP

In short, WebSocket is the underlying protocol, SockJS is the alternative to WebSocket and the underlying protocol, and STOMP is the upper protocol based on WebSocket (SockJS).

1. HTTP protocol solves the details of the request initiated by the web browser and the response of the web server. Assuming that HTTP protocol does not exist, we can only use TCP socket to write web applications.

2. Using WebSocket (SockJS) directly is very similar to using TCP socket to write web applications. Because there is no high-level protocol, we need to define the semantics of messages sent between applications and ensure that both ends of the connection can follow these semantics;

3. Like HTTP adding request response model layer on TCP socket, STOMP provides a frame based line format layer on WebSocket to define message semantics;

Common implementation methods

maven dependency is introduced first

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

Simple Websocket

It has nothing to do with Spring and is implemented based on tomcat

The WebSocket specification supports two online data formats -- text and binary. The API supports these two formats and adds the function of processing Java objects and health check messages (ping pong) defined in the specification:

  • Text: any text data (java.lang.String, primitives, or their equivalent wrapper classes)
  • Binary: by Java nio. Binary data represented by ByteBuffer or byte [] (byte array) (such as audio, image, etc.)
  • Java objects: API s can use native (Java object) representations in your code and use custom converters (encoders / decoders) to convert them to compatible online formats (text, binary) allowed by the WebSocket protocol
  • Ping-Pong: javax. WebSocket. A pongmessage is an acknowledgement sent by a WebSocket peer in response to a health check (ping) request

Server

Relevant notes are as follows:

  • @ServerEndpoint: if it is decorated with * @ ServerEndpoint, the container ensures the availability of the class as a WebSocket * server and listens to a specific URI space
  • @ClientEndpoint: a class decorated with this annotation is considered a WebSocket client
  • @OnOpen: when a new WebSocket connection is started, the container will call the Java method with * @ OnOpen *
  • @OnMessage: a Java method annotated with * @ OnMessage to receive information from the WebSocket * container when the message is sent to the endpoint
  • @OnError: call the method with * @ OnError * when there is a communication problem
  • @OnClose: used to decorate a Java method that is called by the container when the WebSocket connection is closed

We declare a Java class WebSocket * server endpoint by using the * @ ServerEndpoint annotation. We also specify the URI of the deployment endpoint. The URI is defined relative to the root of the server container and must begin with a forward slash:

Step 1: inject ServerEndpointExporter. This bean will automatically register the Websocket endpoint declared with @ ServerEndpoint annotation.

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

Step 2: handle the websocket class and add @ Component

@ServerEndpoint(value = "/chat/{username}")
@Component
public class ChatEndpoint {

    private Session session;
    private static Set<ChatEndpoint> chatEndpoints = new CopyOnWriteArraySet<>();
    private static HashMap<String, String> users = new HashMap<>();

    @OnOpen
    public void onOpen(
            Session session,
            @PathParam("username") String username) throws IOException {
        this.session = session;
        chatEndpoints.add(this);
        users.put(session.getId(), username);
        String message = username + "Connected!";
        broadcast(message);
    }

    @OnMessage
    public void onMessage(Session session, String message)
            throws IOException {
        broadcast(message);
    }

    @OnClose
    public void onClose(Session session) throws IOException {

        chatEndpoints.remove(this);
        broadcast(users.get(session.getId()) + "Disconnected!");
    }

    @OnError
    public void onError(Session session, Throwable throwable) {
        // Do error handling here
    }

    private static void broadcast(String message) {

        chatEndpoints.forEach(endpoint -> {
            synchronized (endpoint) {
                try {
                    endpoint.session.getBasicRemote().sendText(message);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

client

<script>
    if ('WebSocket' in window) {
        var url = 'ws://localhost:8080/chat/laker1';
        var sock = new WebSocket(url);      //Open WebSocket
    } else {
        alert("Your browser does not support WebSocket");
    }
    sock.onopen = function () {          //Handle connection opening events
        console.log('Opening');
        sock.send('start');
    };
    sock.onmessage = function (e) {      //process information
        e = e || event; 		   //Get events. This is written to be compatible with IE browser
        console.log(e.data);
    };
    sock.onclose = function () {         //Handle connection close events
        console.log('Closing');
    };
</script>

Spring WebSocket

Server

Processing class

public class ChatHandler extends AbstractWebSocketHandler {
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        System.out.println("Received message: " + message.getPayload());
        Thread.sleep(2000);
        session.sendMessage(new TextMessage("Polo!"));
    }
    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        System.out.println("Connection established!");
    }
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
        System.out.println("Connection closed. Status: " + status);
    }
}

Configure registration

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(chatHandler(), "/chat").setAllowedOrigins("*"); 
    }
    @Bean
    public ChatHandler chatHandler() {
        return new ChatHandler();
    }
}

client

<script>
    if ('WebSocket' in window) {
        var url = 'ws://localhost:8080/chat';
        var sock = new WebSocket(url);      //Open WebSocket
    } else {
        alert("Your browser does not support WebSocket");
    }
    sock.onopen = function () {          //Handle connection opening events
        console.log('Opening');
        sock.send('start');
    };
    sock.onmessage = function (e) {      //process information
        e = e || event; 		   //Get events. This is written to be compatible with IE browser
        console.log(e.data);
    };
    sock.onclose = function () {         //Handle connection close events
        console.log('Closing');
    };
</script>

SockJS

Server

The code above is websocketconfig Focus on enabling SockJS support on Java withSockJS();

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    registry.addHandler(chatHandler(), "/chat").withSockJS();
}

client

<script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
var url = 'http://localhost:8080/chat'; // The URL processed by sockjs is http: / / or https: / /, no longer ws: / / and wss://
var sock = new SockJS(url);
//var sock = new WebSocket(url);      // Open websocket

The operation effect is the same, but the way of client server communication has changed greatly.

Stomp

Server

Configure registration

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketStompConfig extends AbstractWebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //Enable SockJS for / chatroom path
        registry.addEndpoint("/chatroom").setAllowedOrigins("*").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        //This indicates that messages can be sent to clients in the three domains of topic, queue and users. Server -- > client
        registry.enableSimpleBroker("/topic", "/queue", "/users");
        //When the client sends a request to the server, it needs to be prefixed with / app. Client -- > server
        registry.setApplicationDestinationPrefixes("/app");
        //Send a one-to-one message to the specified user with the prefix / users /.
        registry.setUserDestinationPrefix("/users/");
    }
}

WebSocketStompConfig overloads the registerstampendpoints() method and registers / chat as a STOMP endpoint.

This path is different from the previous destination path for receiving and sending messages. This is an endpoint that clients connect to before subscribing or publishing messages to the destination.

Websocketstopconfig also configures a simple message broker by overloading the configureMessageBroker() method. This method is optional. If it is not overloaded, a simple memory message agent will be automatically configured to process messages prefixed with "/ topic".

Process STOMP messages from clients

@Controller
public class WebSocketCharConroller {
    //The destination is "/ app/chat". ("/ app" prefix is implied because we configure it as the destination prefix of the application)
    @MessageMapping("/chat")
    public void chat(String msg) {
        System.out.println("Received message:" + msg);
    }

    @SubscribeMapping("/subscribe")
    public String handleSubscribe() {
        return "i am ok";
    }
}

@The MessageMapping annotation indicates that the chat () method can handle messages arriving at the specified destination. In this case, the destination is "/ app/chat". ("/ app" prefix is implied because we configure it as the destination prefix of the application)

@SubscribeMapping annotation is similar to @ MessageMapping annotation. When a STOMP subscription message is received, the method with @ SubscribeMapping annotation will be triggered.

client

<script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.js"></script>
<script>
    var url = 'http://localhost:8080/chatroom';
    var sock = new SockJS(url);  //Create a SockJS connection.
    var stomp = Stomp.over(sock);//Create a STOMP client instance. In fact, SockJS is encapsulated so that the STOMP message can be sent on the WebSocket connection.
    var payload = JSON.stringify({'message': 'Marco!'});
    stomp.connect('guest', 'guest', function (frame) {
        stomp.send("/app/chat", {}, payload);
        stomp.subscribe('/app/subscribe', function (message) {

        });
    });
</script>

Frequently asked questions

1. Does JavaScript have native methods that support sending ping/pong messages?

When connecting to the WebSocket, I found that the WebSocket was disconnected shortly after it was connected. In order to maintain the connection for a long time, I thought of the ping/pong protocol.

The browser provides a native WebSocket constructor to create a WebSocket instance. The instance only provides a send method, and the send method can only be used to send the content of Payload Data in the above protocol. The browser will automatically generate a complete frame data according to the send parameters. Therefore, it is impossible to control the frame content other than Payload Data on the browser side, that is, opcode cannot be customized. Therefore, the ping/pong protocol defined in the WebSocket specification cannot be implemented.

For details, see:

2.SockJS heartbeat

The SockJS protocol requires the server to send a heartbeat message to prevent the proxy from concluding that the connection is suspended. Spring SockJS is configured with a property called heartbeat time, which you can use to customize the frequency. By default, the heartbeat is sent after 25 seconds. Assuming that no other messages are sent on the connection, this 25 second value meets the following requirements for public Internet applications IETF recommendations.

When using STOMP on WebSocket and SockJS, if the STOMP client and server negotiate to exchange heartbeat, the SockJS heartbeat is disabled.

Spring SockJS support also allows you to configure the TaskScheduler to schedule heartbeat tasks. The task scheduler is supported by the thread pool. Its default setting is based on the number of available processors. You should consider customizing the settings according to your specific needs.

registry.addHandler(chatHandler(), "/chat").setAllowedOrigins("*").withSockJS().setHeartbeatTime(25000)

Other configurations
    .withSockJS()
            .setStreamBytesLimit(512 * 1024) 
            .setHttpMessageCacheSize(1000) 
            .setDisconnectDelay(30 * 1000); 
  • Set the streamBytesLimit property to 512KB (the default is 128KB = > 128 * 1024).
  • Set the httpMessageCacheSize property to 1000 (the default is 100).
  • Set the disconnectDelay property to 30 seconds (the default is 5 seconds = > 5 * 1000).

3. Configure WebSocket engine

Each underlying WebSocket engine exposes configuration properties that control runtime characteristics, such as message buffer size, idle timeout, etc.

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Bean
    public ServletServerContainerFactoryBean createWebSocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        container.setMaxTextMessageBufferSize(8192);
        container.setMaxBinaryMessageBufferSize(8192);
        return container;
    }
}

reference resources:

Topics: Java Spring Boot