Four ways of integrating websocket with spring boot

Four schemes of integrating websocket

1. Original notes

pom.xml

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

WebSocketConfig

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */
package cn.coder4j.study.example.websocket.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * @author buhao
 * @version WebSocketConfig.java, v 0.1 2019-10-18 15:45 buhao
 */
@Configuration
@EnableWebSocket
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpoint() {
        return new ServerEndpointExporter();
    }
}
Explain:

This configuration class is very simple. Only spring boot can scan the following notes about websocket.

WsServerEndpoint

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */
package cn.coder4j.study.example.websocket.ws;

import org.springframework.stereotype.Component;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * @author buhao
 * @version WsServerEndpoint.java, v 0.1 2019-10-18 16:06 buhao
 */
@ServerEndpoint("/myWs")
@Component
public class WsServerEndpoint {

    /**
     * Successful connection
     *
     * @param session
     */
    @OnOpen
    public void onOpen(Session session) {
        System.out.println("Successful connection");
    }

    /**
     * Connection closure
     *
     * @param session
     */
    @OnClose
    public void onClose(Session session) {
        System.out.println("Connection closure");
    }

    /**
     * Message received
     *
     * @param text
     */
    @OnMessage
    public String onMsg(String text) throws IOException {
        return "servet Send out:" + text;
    }
}
Explain

Here are a few notes to note. First, their packages are all under javax.websocket. It is not provided by spring, but comes with jdk. Here are their specific functions.

  1.  @ServerEndpoint
  2. Through this spring boot, you can know the path of the exposed WS application, which is similar to the @ RequestMapping we often use. For example, if your startup port is 8080 and the value of this annotation is WS, then we can connect your application through ws://127.0.0.1:8080/ws.
  3.  @OnOpen
  4. When websocket establishes a successful connection, this annotation modification method will be triggered. Note that it has a Session parameter.
  5. @OnClose
  6. When the connection established by websocket is disconnected, this annotation modification method will be triggered. Note that it has a Session parameter.
  7. @OnMessage
  8. When the client sends a message to the server, this annotation modification method will be triggered. It has a String input parameter indicating the value passed in by the client.
  9. @OnError
  10. When websocket creates a connection, an exception will trigger the annotation modification method. Note that it has a Session parameter.

Another point is how the server sends the message to the client. The server must send the message through the session class mentioned above, usually in the @ OnOpen method. When the connection is successful, the session is stored in the Map value. The key is the user ID corresponding to the session. When the message is to be sent, the session can be obtained through the key and then sent. Here, you can use * * session.getbasicrem Te_(). Sendtext () * * to send messages to clients.

2. Spring encapsulation

pom.xml

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

HttpAuthHandler

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */
package cn.coder4j.study.example.websocket.handler;

import cn.coder4j.study.example.websocket.config.WsSessionManager;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.time.LocalDateTime;

/**
 * @author buhao
 * @version MyWSHandler.java, v 0.1 2019-10-17 17:10 buhao
 */
@Component
public class HttpAuthHandler extends TextWebSocketHandler {

    /**
     * socket Build success event
     *
     * @param session
     * @throws Exception
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        Object token = session.getAttributes().get("token");
        if (token != null) {
            // The user is connected successfully and put into the online user cache
            WsSessionManager.add(token.toString(), session);
        } else {
            throw new RuntimeException("User login has expired!");
        }
    }

    /**
     * Receive message event
     *
     * @param session
     * @param message
     * @throws Exception
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // Get messages from clients
        String payload = message.getPayload();
        Object token = session.getAttributes().get("token");
        System.out.println("server Receive " + token + " Transmitted " + payload);
        session.sendMessage(new TextMessage("server Send to " + token + " news " + payload + " " + LocalDateTime.now().toString()));
    }

    /**
     * socket When disconnected
     *
     * @param session
     * @param status
     * @throws Exception
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        Object token = session.getAttributes().get("token");
        if (token != null) {
            // User exit, remove cache
            WsSessionManager.remove(token.toString());
        }
    }


}
Explain

By inheriting the TextWebSocketHandler class and overriding the corresponding methods, the events of websocket can be processed. Here, we can connect with those annotations of the native annotation.

  1. The afterConnectionEstablished method is triggered after the socket connection is successful. It is the same as the @ OnOpen function in the native annotation.
  2. The afterConnectionClosed method is triggered after the socket connection is closed. It is the same as the @ OnClose function in the native annotation.
  3. The handleTextMessage method is triggered when the client sends a message, the same as the @ OnMessage function in the native annotation.

    WsSessionManager

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */
package cn.coder4j.study.example.websocket.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.WebSocketSession;

import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author buhao
 * @version WsSessionManager.java, v 0.1 2019-10-22 10:24 buhao
 */
@Slf4j
public class WsSessionManager {
    /**
     * Where to save the connection session
     */
    private static ConcurrentHashMap<String, WebSocketSession> SESSION_POOL = new ConcurrentHashMap<>();

    /**
     * Add session
     *
     * @param key
     */
    public static void add(String key, WebSocketSession session) {
        // Add session
        SESSION_POOL.put(key, session);
    }

    /**
     * Deleting a session will return the deleted session
     *
     * @param key
     * @return
     */
    public static WebSocketSession remove(String key) {
        // Delete session
        return SESSION_POOL.remove(key);
    }

    /**
     * Delete and sync close connection
     *
     * @param key
     */
    public static void removeAndClose(String key) {
        WebSocketSession session = remove(key);
        if (session != null) {
            try {
                // Close connection
                session.close();
            } catch (IOException e) {
                // todo: exception handling during shutdown
                e.printStackTrace();
            }
        }
    }

    /**
     * Get session
     *
     * @param key
     * @return
     */
    public static WebSocketSession get(String key) {
        // Get session
        return SESSION_POOL.get(key);
    }
}
Explain

Here, we simply implement a session pool through ConcurrentHashMap to save the sessions of the logged in web socket. As mentioned earlier, the server must pass this session to send messages to the client.

MyInterceptor

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */
package cn.coder4j.study.example.websocket.interceptor;

import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;

import java.util.HashMap;
import java.util.Map;

/**
 * @author buhao
 * @version MyInterceptor.java, v 0.1 2019-10-17 19:21 buhao
 */
@Component
public class MyInterceptor implements HandshakeInterceptor {

    /**
     * Before shaking hands
     *
     * @param request
     * @param response
     * @param wsHandler
     * @param attributes
     * @return
     * @throws Exception
     */
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        System.out.println("Shake hands start");
        // Get request parameters
        HashMap<String, String> paramMap = HttpUtil.decodeParamMap(request.getURI().getQuery(), "utf-8");
        String uid = paramMap.get("token");
        if (StrUtil.isNotBlank(uid)) {
            // Put in property field
            attributes.put("token", uid);
            System.out.println("user token " + uid + " Handshake successful!");
            return true;
        }
        System.out.println("User login failed");
        return false;
    }

    /**
     * After shaking hands
     *
     * @param request
     * @param response
     * @param wsHandler
     * @param exception
     */
    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
        System.out.println("Shake hands finish");
    }

}
Explain

The handshake interceptor is defined by implementing the HandshakeInterceptor interface. Note that the event here is different from that of the Handler above. Here is the event when the handshake is established, which is divided into pre handshake and post handshake. The event of the Handler is to establish the socket connection based on the successful handshake. Therefore, if the authentication is put in this step, the server resources will be saved most. It has two main methods: beforeHandshake and afterHandshake. As the name implies, one is triggered before handshake and the other after handshake.

WebSocketConfig

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */
package cn.coder4j.study.example.websocket.config;

import cn.coder4j.study.example.websocket.handler.HttpAuthHandler;
import cn.coder4j.study.example.websocket.interceptor.MyInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

/**
 * @author buhao
 * @version WebSocketConfig.java, v 0.1 2019-10-17 15:43 buhao
 */
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Autowired
    private HttpAuthHandler httpAuthHandler;
    @Autowired
    private MyInterceptor myInterceptor;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry
                .addHandler(httpAuthHandler, "myWS")
                .addInterceptors(myInterceptor)
                .setAllowedOrigins("*");
    }
}
Explain

Implement WebSocketConfigurer class and override the corresponding method to configure websocket. We mainly cover the registerWebSocketHandlers method. Configure by setting different parameters to WebSocketHandlerRegistry. The addHandler method adds the handler processing class of ws written above. The second parameter is the ws path you exposed. addInterceptors adds the handshake filter we wrote. **setAllowedOrigins("*") * * this is to turn off cross domain verification for local debugging. It is recommended to turn it on online.

3. TIO

pom.xml

 <dependency>
     <groupId>org.t-io</groupId>
     <artifactId>tio-websocket-spring-boot-starter</artifactId>
     <version>3.5.5.v20191010-RELEASE</version>
</dependency>

application.xml

tio:
  websocket:
    server:
      port: 8989
Explain

Only the start port of ws is configured here, and there are many other configurations, which can be found through the link at the end

MyHandler

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */
package cn.coder4j.study.example.websocket.handler;

import org.springframework.stereotype.Component;
import org.tio.core.ChannelContext;
import org.tio.http.common.HttpRequest;
import org.tio.http.common.HttpResponse;
import org.tio.websocket.common.WsRequest;
import org.tio.websocket.server.handler.IWsMsgHandler;

/**
 * @author buhao
 * @version MyHandler.java, v 0.1 2019-10-21 14:39 buhao
 */
@Component
public class MyHandler implements IWsMsgHandler {
    /**
     * Handshake
     *
     * @param httpRequest
     * @param httpResponse
     * @param channelContext
     * @return
     * @throws Exception
     */
    @Override
    public HttpResponse handshake(HttpRequest httpRequest, HttpResponse httpResponse, ChannelContext channelContext) throws Exception {
        return httpResponse;
    }

    /**
     * Handshake success
     *
     * @param httpRequest
     * @param httpResponse
     * @param channelContext
     * @throws Exception
     */
    @Override
    public void onAfterHandshaked(HttpRequest httpRequest, HttpResponse httpResponse, ChannelContext channelContext) throws Exception {
        System.out.println("Handshake success");
    }

    /**
     * Receive binary
     *
     * @param wsRequest
     * @param bytes
     * @param channelContext
     * @return
     * @throws Exception
     */
    @Override
    public Object onBytes(WsRequest wsRequest, byte[] bytes, ChannelContext channelContext) throws Exception {
        return null;
    }

    /**
     * Disconnect
     *
     * @param wsRequest
     * @param bytes
     * @param channelContext
     * @return
     * @throws Exception
     */
    @Override
    public Object onClose(WsRequest wsRequest, byte[] bytes, ChannelContext channelContext) throws Exception {
        System.out.println("Close connection");
        return null;
    }

    /**
     * receive messages
     *
     * @param wsRequest
     * @param s
     * @param channelContext
     * @return
     * @throws Exception
     */
    @Override
    public Object onText(WsRequest wsRequest, String s, ChannelContext channelContext) throws Exception {
        System.out.println("Receive text message:" + s);
        return "success";
    }
}
Explain

The handler in the above example is very similar to that in the previous example. It also implements the interface override method to process events. The implemented interface is IWsMsgHandler. Its method functions are as follows

  1. handshake
  2. Trigger when shaking hands
  3. onAfterHandshaked
  4. Triggered after successful handshake
  5. onBytes
  6. Client sends binary message trigger
  7. onClose
  8. Triggered when the client closes the connection
  9. onText
  10. Client sends text message trigger

StudyWebsocketExampleApplication

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */

package cn.coder4j.study.example.websocket;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.tio.websocket.starter.EnableTioWebSocketServer;

@SpringBootApplication
@EnableTioWebSocketServer
public class StudyWebsocketExampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(StudyWebsocketExampleApplication.class, args);
    }
}
Explain

The name of this class is not important. It is actually your spring boot class. Just remember to add the @ EnableTioWebSocketServer annotation.

STOMP

pom.xml

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

WebSocketConfig

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */
package cn.coder4j.study.example.websocket.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

/**
 * @author buhao
 * @version WebSocketConfig.java, v 0.1 2019-10-21 16:32 buhao
 */
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // Configure the address the client is trying to connect to
        registry.addEndpoint("/ws").setAllowedOrigins("*").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // Set up broadcast node
        registry.enableSimpleBroker("/topic", "/user");
        // The / app prefix is required for the client to send messages to the server
        registry.setApplicationDestinationPrefixes("/app");
        // Specifies the prefix / user that the user sends (one-to-one)/
        registry.setUserDestinationPrefix("/user/");
    }
}
Explain
  1. Through the implementation of WebSocketMessageBrokerConfigurer interface and the addition of @ EnableWebSocketMessageBroker, the configuration and annotation scanning of stomp can be performed.
  2. The register stopendpoints method is overridden to set the path of exposed stopp, and other cross domain and client settings.
  3. Override the configureMessageBroker method to configure nodes.
  4. Among them, the broadcasting node configured by enablesimplebarker is the node where the server sends messages and the client subscribes to receive messages.
  5. Override the setApplicationDestinationPrefixes method to set the node where the client sends messages to the server.
  6. Override the setUserDestinationPrefix method to set the one-to-one communication node.

    WSController

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */
package cn.coder4j.study.example.websocket.controller;

import cn.coder4j.study.example.websocket.model.RequestMessage;
import cn.coder4j.study.example.websocket.model.ResponseMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @author buhao
 * @version WSController.java, v 0.1 2019-10-21 17:22 buhao
 */
@Controller
public class WSController {

    @Autowired
    private SimpMessagingTemplate simpMessagingTemplate;

    @MessageMapping("/hello")
    @SendTo("/topic/hello")
    public ResponseMessage hello(RequestMessage requestMessage) {
        System.out.println("Receive message:" + requestMessage);
        return new ResponseMessage("The server receives your:" + requestMessage);
    }

    @GetMapping("/sendMsgByUser")
    public @ResponseBody
    Object sendMsgByUser(String token, String msg) {
        simpMessagingTemplate.convertAndSendToUser(token, "/msg", msg);
        return "success";
    }

    @GetMapping("/sendMsgByAll")
    public @ResponseBody
    Object sendMsgByAll(String msg) {
        simpMessagingTemplate.convertAndSend("/topic", msg);
        return "success";
    }

    @GetMapping("/test")
    public String test() {
        return "test-stomp.html";
    }
}
Explain
  1. Expose the node path through @ MessageMapping, which is similar to @ RequestMapping. Note that although hello is written here, the real address of our client call is * * / app/hello. Because we have configured registry.setApplicationDestinationPrefixes("/app") * * * in the above config.
  2. @The SendTo annotation will send the content of the return value to the client who has subscribed to / topic/hello. Similarly, there is another @ SendToUser, which is only sent to the client for one-to-one communication. These two annotations are generally responsive when answering. If the server actively sends a message, you can use the convertAndSend method of the simpMessagingTemplate class. Pay attention to simpMessagingTemplate.convertAndSendToUser(token, "/msg", msg). Contact the registration.setuserdestinationprefix ("/ user /") configured above. Here, the client subscribes to / user/{token}/msg. Don't get it wrong.

    Session sharing issues

    One of the problems mentioned repeatedly above is that if the server wants to actively send messages to the client, it must use session. As we all know, session is not cross jvm. If there are multiple servers, in the case of http requests, we can share and solve this problem by putting the session into the cache middleware, which can be solved by several configurations of spring session. But Web socket can't. His session cannot be serialized. Of course, the purpose of this design is not to embarrass you, but because of the difference between http and web socket requests.
    At present, the simplest solution found on the Internet is to subscribe to broadcast through redis. The main code is similar to the second method. You need to put a map locally to save the requested session. That is to say, each server will save the session connected to it locally. Then the place where the message is sent needs to be modified. It is not sent directly as it is now, but through the subscription mechanism of redis. When the server wants to send a message, you broadcast the message through redis. All subscribed servers will receive the message and try to send it locally. Finally, it is certain that only the one with the corresponding user session can be sent out.

    How to choose

  3. If you are using tio, the integration of tio is recommended. Because it has implemented many functions, including the above-mentioned session sharing through redis, just add a few configurations. But tio is semi open source, and documents need to be charged. If not, forget him.
  4. If your business requirements are flexible, the first two are recommended, and the second is Spring encapsulation.
  5. If it is only a simple two-way communication between servers, it is recommended to use stomp as it is easier to standardize.

    Other

  6. websocket online verification

After writing the server-side code, you want to debug, but you don't know what to do with the front-end code. Here This is an online websocket client, which has enough functions for us to debug.

  1. stomp verification

This one can't be found in the online version, but there are many demo s that can be downloaded locally for debugging, or can be found through the link in the following article.

  1. In addition, due to the limited space, not all the codes can be put, but all the test codes can be uploaded to gitlab to ensure normal operation. Here Find

Reference link

  1. SpringBoot system - integrated WebSocket real-time communication
  2. The story of WebSocket (2) -- how to use STOMP to quickly build WebSocket broadcast message mode in Spring
  3. Spring boot integrates WebSocket [based on pure H5] for point-to-point [one-to-one] and broadcast [one to many] real-time push
  4. Spring Framework reference document (WebSocket STOMP)
  5. Summary of using WebSocket in Spring Boot (1): details of several implementation methods
  6. Spring Boot series - WebSocket easy to use
  7. tio-websocket-spring-boot-starter

Keywords: Java Session socket Spring

Added by Riparian on Wed, 23 Oct 2019 21:29:53 +0300