1. An overview of the

This article introduces webSocket related content, mainly including the following contents:

  • WebSocket birth background, operation mechanism and packet capture analysis
  • WebSocket application scenario, server, and browser version requirements
  • A simple message broker and message flowchart embedded in Spring
  • Integrate websocket in Spring Boot, and introduce the usage of STOMp and SOckJS
  • Introduces the HandshakeInterceptor and ChannelInterceptor interceptors, and demonstrates their use
  • Usage and differences between @sendto and @sendtouser

2. WebSocket birth background, operation mechanism and packet capture analysis

2.1. Background of Websocket birth

For applications that require real-time response and high concurrency, the traditional request-response Web is not very efficient. When dealing with such a business scenario, the following scenarios are commonly used:

  • Polling, this method is easy to waste bandwidth, low efficiency
  • Based on Flash, AdobeFlash realizes data exchange through its Socket, and then uses Flash to expose the corresponding interface for JavaScript invocation, so as to achieve the purpose of real-time transmission. But now that Flash is dead, it’s not working
  • MQTT, the Comet open source framework, these techniques do not work well in heavy traffic situations

In this context, WebSocket in HTML5 specification (known as Web TCP) is an efficient and energy-saving two-way communication mechanism to ensure real-time data transmission.

2.2. Operation mechanism of WebSocket

WebSocket is a new protocol for HTML5. It is built on TCP and realizes full duplex asynchronous communication between client and server.

It differs from HTTP in the following ways:

  • WebSocket is a two-way communication protocol. Both WebSocket server and Browser/Client Agent can actively send or receive data to each other.
  • A WebSocket requires a TCP – like client and server to communicate with each other only after the connection is successful.

Traditional HTTP request response client server interaction diagram

WebSocket request response client server interaction diagram

Comparing the above two figures, compared with the traditional HTTP request-response mode, which requires the client to establish a connection with the server, WebSocket once the WebSocket connection is established, subsequent data is transmitted in the form of frame sequence. Before the client disconnects from the WebSocket or the Server, the client and Server do not need to initiate a connection request again. In this way, the performance and real-time advantages of WebSocket are ensured

2.3. WebSocket packet capture analysis

Let’s take a look at the difference between WebSocket communication and traditional HTTP through the packets exchanged between the client and the server:

The WebSocket client connects to the server port and performs a handshake. The client sends data in a similar format: request:

  • The “Upgrade: websocket “parameter value indicates that this is a WebSocket type request
  • “Sec-websocket-key” is a base64 encoded ciphertext sent by the WebSocket client. The server must return an encrypted sec-websocket-accept reply. Otherwise, the client will throw an Error during WebSocket Handshake and close the connection.

The data format returned by the server is similar to the following:

  • The value of sec-websocket-accept is calculated by the server using the same key as the client and then returned to the client
  • HTTP/1.1 101″ : Switching Protocols indicate that the server accepts a WebSocket client connection. After such request-response processing, the WebSocket connection on the client server is successfully handshake, and TCP communication can then be performed

3. WebSocket application scenario, server, and browser version requirements

3.1. Websocket scenarios

Clients and servers need to exchange events with high frequency and low latency. They are sensitive to time delays and need to exchange a wide variety of messages at high frequencies

3.2. Version requirements for the server and browser

The WebSocket server is generally supported by the JEE JSR356 standard specification API from major application server vendors. Currently supported websocket versions :Tomcat 7.0.47+, Jetty 9.1+, GlassFish 4.1+, WebLogic 12.1.3+, and Undertow 1.0+ (and WildFly 8.0+).

Browser supported versions: View all webSocket supported connections:

4. Simple message broker and message flowchart embedded in Spring

4.1. Simple Broker

Spring has a built-in simple message broker. This broker processes subscription requests from clients, stores them in memory, and broadcasts the messages to connected clients with matching targets

4.2. Message flow diagram

The following diagram is a flowchart for using a simple message broker

The three message channels in the figure are described as follows:

  • “ClientInboundChannel” – Used to transmit messages received from webSocket clients
  • “ClientOutboundChannel” – Used to transport messages sent to webSocket clients
  • “BrokerChannel” – Used to transport messages from server-side application code to message brokers

5. Integrate Websocket in Spring Boot and introduce the usage of STOMp and SOckJS

5.1. The pom. The XML

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

5.2. The POJO class

RequestMessage: a message that the browser requests from the server

public class RequestMessage {
    private String name;

// set/ get slightly}Copy the code

ResponseMessage: message returned by the server to the browser

public class ResponseMessage {
    private String responseMessage;

// set/ get slightly}Copy the code

5.3. BroadcastCtl

This class is the @Controller class

  • BroadcastIndex () method: uses @requestMapping to get to the page
  • Comments on the broadcast() method
    • MessageMapping: specifies the address to receive messages, similar to @requestMapping
    • SendTo By default messages will be sent to the same destination as the incoming message, but with a prefix attached to the destination (” /topic “by default).
@Controller public class BroadcastCtl { private static final Logger logger = LoggerFactory.getLogger(BroadcastCtl.class); Private AtomicInteger count = new AtomicInteger(0); /** * @messagemapping specifies the address to receive messages, similar to @requestMapping. In addition to annotations to methods, Can also be noted that class * @ SendTo default message will be sent to the same destination as the incoming message * news of the return value is by {@ link org. Springframework. Messaging. The converter. MessageConverter} * @param requestMessage * @return
     */
    @MessageMapping("/receive")
    @SendTo("/topic/getResponse")
    public ResponseMessage broadcast(RequestMessage requestMessage){
        logger.info("receive message = {}" , JSONObject.toJSONString(requestMessage));
        ResponseMessage responseMessage = new ResponseMessage();
        responseMessage.setResponseMessage("BroadcastCtl receive [" + count.incrementAndGet() + "] records");
        return responseMessage;
    }

    @RequestMapping(value="/broadcast/index")
    public String broadcastIndex(HttpServletRequest req){
        System.out.println(req.getRemoteHost());
        return "websocket/simple/ws-broadcast"; }}Copy the code

5.4. WebSocketMessageBrokerConfigurer

Configure the message broker, using the built-in message broker by default. Annotation on the class @ EnableWebSocketMessageBroker: this annotation said use STOMP protocol to transmit messages based on message broker, can use @ MessageMapping in @ the Controller class at this time

  • The addEndpoint method in registerStompEndpoints() adds the STOMP endpoint. This HTTP URL is the address to be accessed by WebSocket or SockJS clients; WithSockJS: Specifies that the endpoint uses the SockJS protocol
  • Set up the simple message broker in the configureMessageBroker() method and configure messages to be sent to the Broker only if the address matches the configured prefix
@configuration // This annotation indicates that STOMP is used to transport messages based on the message broker. At this point you can @ Controller class using the @ MessageMapping @ EnableWebSocketMessageBroker public class WebSocketMessageBrokerConfigurer extends AbstractWebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) AddEndpoint: Adds a Stomp endpoint. * withSockJS: Specifies the endpoint to use the SockJS protocol */ registry.addendpoint ("/websocket-simple")
                .setAllowedOrigins("*"// Add.withsockjs () to allow cross-domain access; } @override public void configureMessageBroker(MessageBrokerRegistry Registry) {/** * Start the simple Broker, Message sending address in accordance with the prefix to configure message is sent to the broker * / registry. The enableSimpleBroker ("/topic"."/queue"); } @Override public void configureClientInboundChannel(ChannelRegistration registration) { super.configureClientInboundChannel(registration); }}Copy the code

5.5. Configure FRONT-END STOMP and SOCKJS

Stomp WebSocket implements duplex asynchronous communication capabilities using sockets. However, if developing programs directly using the WebSocket protocol is cumbersome, we can use its subprotocol Stomp

SockJS SockJS is the implementation of webSocket protocol, added to the compatibility of browsers do not support Websocket when supporting the support of SockJS transport protocol has three classes: WebSocket, HTTP Streaming, and HTTP Long Polling. Websocket is used by default. If the browser does not support WebSocket, use the latter two methods. SockJS uses “Get /info” to Get basic information from the server. The client then decides which transport mode to use. If the browser uses WebSocket, use webSocket. If not, use Http Streaming, and if not, finally use Http Long Polling

Ws-broadcast.jsp front-end page

Introduce the related stomp.js, sockjs.js, jquery.js

<! -- jquery --> <script src="/websocket/jquery.js"></script> <! -- Stomp client script --> <script SRC ="/websocket/stomp.js"></script> <! -- SockJS client script --> <script SRC ="/websocket/sockjs.js"></script>
Copy the code

Front-end access to Websocket, important code description is as follows:

  • Var socket = new SockJS(‘/websocket-simple’) : The websocket connection address, this value is equal to the WebSocketMessageBrokerConfigurer registry. AddEndpoint (“/websocket – simple “). WithSockJS configuration address ()
  • StompClient. Subscribe (‘/topic/the method getResponse ‘, function (respnose) {… }): destination address of the client subscription message: this value is the same as the value configured in the @sendto (“/topic/getResponse”) annotation in BroadcastCtl
  • stompClient.send(“/receive”, {}, JSON.stringify({ ‘name’: name })): Destination address for sending messages: The server uses BroadcastCtl’s @messagemapping (“/receive”) annotation to process incoming messages
<body onload="disconnect()">
<div>
    <div>
        <button id="connect" onclick="connect();"</button> <button id="disconnect" disabled="disabled" onclick="disconnect();"</button> </div> <div id="conversationDiv"<label> Enter your name </label><inputtype="text" id="name" />
        <button id="sendName" onclick="sendName();"> send </button> <p id="response"></p>
    </div>
</div>

<script type="text/javascript">
    var stompClient = null;

    function setConnected(connected) {
        document.getElementById('connect').disabled = connected;
        document.getElementById('disconnect').disabled = ! connected; document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden';
        $('#response').html();
    }

    function connect() {/ / websocket connection address, this value is equal to the registry in WebSocketMessageBrokerConfigurer. AddEndpoint ("/websocket-simple"Var socket = new SockJS()'/websocket-simple'); 
        stompClient = Stomp.over(socket);
        stompClient.connect({}, function(frame) {
            setConnected(true);
            console.log('Connected: '+ frame); // The destination address of the client subscription message: this value is BroadcastCtl."/topic/getResponse"Subscribe (stompClient.subscribe)'/topic/getResponse'.function(respnose){ 
                showResponse(JSON.parse(respnose.body).responseMessage);
            });
        });
    }


    function disconnect() {
        if(stompClient ! = null) { stompClient.disconnect(); }setConnected(false);
        console.log("Disconnected");
    }

    function sendName() {
        var name = $('#name').val(); // The server uses BroadcastCtl to send messages to @messagemapping"/receive") annotated methods to handle incoming messages stompClient.send("/receive", {}, JSON.stringify({ 'name': name }));
    }

    function showResponse(message) {
        var response = $("#response");
        response.html(message + "\r\n" + response.html());
    }
</script>
</body>

Copy the code

5.6. Test

Start the service WebSocketApplication in open multiple tags, perform the request: http://127.0.0.1:8080//broadcast/index click “connect”, then “send” many times, the results are as follows: The websocket executes successfully and sends all returned values to all subscribers

6. Introduce the HandshakeInterceptor and ChannelInterceptor interceptors, and demonstrate their usage

We can configure interceptors for WebSockets. By default, there are two types:

  • HandshakeInterceptor: Intercepts webSocket handshake requests. Is executed when the server and client are shaking hands
  • ChannelInterceptor: intercepts Message. This value can be viewed and modified before and after the Message pair is sent to the MessageChannel, or before and after the MessageChannel receives the MessageChannel object

6.1. HandShkeInceptor

Intercepts webSocket handshake requests. Implement the interface HandshakeInterceptor or inherit the class DefaultHandshakeHandler

HttpSessionHandshakeInterceptor: About the operation of the httpSession, this interceptor is used to manage things, after shaking hands and we can request information, such as the token, or whether the user can connect session, so that it can prevent illegal user OriginHandshakeInterceptor: Check the validity of the Origin header field

Custom HandshakeInterceptor:

@Component
public class MyHandShakeInterceptor implements HandshakeInterceptor {
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        System.out.println(this.getClass().getCanonicalName() + "HTTP protocol conversion websoket protocol before, before handshake."+request.getURI()); // Before converting the HTTP protocol to websoket, you can use the session information to check whether the user login is validreturn true; } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) {// After the handshake succeeds, system.out.println (this.getClass().getCanonicalName() +"After a successful handshake..."); }}Copy the code

6.2. ChannelInterceptor

ChannelInterceptor: You can view and modify this value before and after the Message object is sent to the MessageChannel, or before and after the MessageChannel receives the MessageChannel

Used in the interceptor StompHeaderAccessor or SimpMessageHeaderAccessor access news

Custom ChannelInterceptorAdapter

@Component
public class MyChannelInterceptorAdapter extends ChannelInterceptorAdapter {

    @Autowired
    private SimpMessagingTemplate simpMessagingTemplate;

    @Override
    public boolean preReceive(MessageChannel channel) {
        System.out.println(this.getClass().getCanonicalName() + " preReceive");
        returnsuper.preReceive(channel); } @Override public Message<? > preSend(Message<? > message, MessageChannel channel) { System.out.println(this.getClass().getCanonicalName() +" preSend");
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
        StompCommand command= accessor.getCommand(); // Detect user subscriptions (prevent users from subscribing to illegal channels)if (StompCommand.SUBSCRIBE.equals(command)) {
            System.out.println(this.getClass().getCanonicalName() + "User subscription destination ="+ accessor.getDestination()); // If the channel subscribed by the user is invalid, return null and the front-end user cannot receive the channel informationreturn super.preSend(message, channel);
        } else {
            returnsuper.preSend(message, channel); } } @Override public void afterSendCompletion(Message<? > message, MessageChannel channel, boolean sent, Exception ex) { System.out.println(this.getClass().getCanonicalName() +" afterSendCompletion");
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
        StompCommand command = accessor.getCommand();
        if (StompCommand.SUBSCRIBE.equals(command)){
            System.out.println(this.getClass().getCanonicalName() + "Subscription message sent successfully");
            this.simpMessagingTemplate.convertAndSend("/topic/getResponse"."Message sent successfully"); } // If the user disconnectsif (StompCommand.DISCONNECT.equals(command)){
            System.out.println(this.getClass().getCanonicalName() + "User disconnected successfully");
                simpMessagingTemplate.convertAndSend("/topic/getResponse"."{' MSG ':' user disconnected successfully '}"); } super.afterSendCompletion(message, channel, sent, ex); }}Copy the code

6.3. In WebSocketMessageBrokerConfigurer configured interceptor

  • Add a custom HandShkeInceptor intercept in the registerStompEndpoints() method via Registry.addInterceptors (myHandShakeInterceptor)
  • In configureClientInboundChannel () method of registration. SetInterceptors (myChannelInterceptorAdapter) add ChannelInterceptor interceptors
@configuration // This annotation indicates that STOMP is used to transport messages based on the message broker. At this point you can @ Controller class using the @ MessageMapping @ EnableWebSocketMessageBroker public class WebSocketMessageBrokerConfigurer extends AbstractWebSocketMessageBrokerConfigurer { @Autowired private MyHandShakeInterceptor myHandShakeInterceptor; @Autowired private MyChannelInterceptorAdapter myChannelInterceptorAdapter; Override public void registerStompEndpoints(StompEndpointRegistry Registry) {/** * Register Stomp endpoint ** addEndpoint: Add the STOMP endpoint. * withSockJS: Specifies the endpoint to use the SockJS protocol */ registry.addendpoint ("/websocket-simple")
                .setAllowedOrigins("*"// Add cross-domain access //.setAllowedOrigins("http://mydomain.com"); .addInterceptors(myHandShakeInterceptor) // Add custom interception.withsockjs (); } @Override public void configureClientInboundChannel(ChannelRegistration registration) { ChannelRegistration channelRegistration = registration.setInterceptors(myChannelInterceptorAdapter); super.configureClientInboundChannel(registration); }}Copy the code

6.4. Test:

Test it in the same way as in the last example, which is omitted here

7. Usage and differences between @sendto and @sendtouser

Above, @sendto will push the message to all connections that subscribe to this message, i.e. Subscribe/publish mode. SendToUser only pushes messages to a specific subscriber, in peer-to-peer mode

SendTo: Sends the received message to the specified routing destination. All subscribers can receive the message, which belongs to broadcast. @ SendToUser: message destination have UserDestinationMessageHandler, message routing and arrival will be anything corresponding to the destination, in addition the annotations and broadcast properties, indicate whether the radio. When the same user logs in to multiple sessions, whether they can all be received. True/false values.

7.1. BroadcastSingleCtl

The BroadcastCtl class above is mostly similar, with only the local broadcast() methods listed below: the @sendtouser annotation is used here

@Controller public class BroadcastSingleCtl { private static final Logger logger = LoggerFactory.getLogger(BroadcastSingleCtl.class); Private AtomicInteger count = new AtomicInteger(0); // @messagemapping specifies the address to receive messages, similar to @requestMapping. In addition to annotating methods, you can also annotate classes @messagemapping ("/receive-single") /** * You can also use SendToUser, which can be directed to a specific user * use @sendtouser instead of @sendto */ @sendtouser ("/topic/getResponse"Public ResponseMessage broadcast(RequestMessage RequestMessage){... . } @RequestMapping(value="/broadcast-single/index")
    public String broadcastIndex() {return "websocket/simple/ws-broadcast-single";
    }

Copy the code

7.2. In WebSocketMessageBrokerConfigurer configuration

@Configuration @MessageMapping @EnableWebSocketMessageBroker public class WebSocketMessageBrokerConfigurer extends AbstractWebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) {... . registry.addEndpoint("/websocket-simple-single").withSockJS(); }... .}Copy the code

7.3. Ws – broadcast – single. The JSP page

Ws – broadcast – single. JSP page: Similar to ws-broadcast.jsp, only the differences are listed here. The biggest difference is that the destination of stompClient.subscribe is prefixed with /user, This is followed by the configured value in the @sendtouser (“/topic/getResponse”) annotation

<script type="text/javascript"> var stompClient = null; ...function connect() {/ / websocket connection address, this value is equal to the registry in WebSocketMessageBrokerConfigurer. AddEndpoint ("/websocket-simple-single"Var socket = new SockJS()'/websocket-simple-single'); //1
        stompClient = Stomp.over(socket);
        stompClient.connect({}, function(frame) {
            setConnected(true);
            console.log('Connected: '+ frame); // Destination address of a client subscription message: this value is equal to @sendtouser ("/topic/getResponse"The value configured in the annotation. This is the address of the request which must be prefixed with /user stompClient.subscribe('/user/topic/getResponse'.function(respnose){ //2
                showResponse(JSON.parse(respnose.body).responseMessage);
            });
        });
    }


    function disconnect() {
        if(stompClient ! = null) { stompClient.disconnect(); }setConnected(false);
        console.log("Disconnected");
    }

    function sendName() {
        var name = $('#name').val(); //// Client message sending purpose: The server uses BroadcastCtl @messagemapping ("/receive-single") annotated methods to handle incoming messages stompClient.send("/receive-single", {}, JSON.stringify({ 'name': name })); }... </script>Copy the code

7.4. Test

Start the service WebSocketApplication perform the request: http://127.0.0.1:8080//broadcast-single/index click “connect”, in two pages each send two messages, the results are as follows: Websocket execution succeeds, and all return values return only the sender, not all subscribers

Code 8.

Please use tag V0.19 as much as possible. Do not use master because master changes all the time. There is no guarantee that the code in this article will always be the same as the code on Github