Springboot connects socket to realize real-time push and precautions

Business scene

Background: There are multiple companies, and there are many users under the company. Each user has multiple documents being processed.
Requirement: Every user who opens a browser can view the real-time progress of the document being processed in real time
. Idea: Receive the company id, user id, list of document ids that need to be pushed, and then enable the background thread for real-time push. After all processing is completed, the server will actively close the socket connection

What is WebSocket?

WebSocket is a protocol provided by HTML5 for full-duplex communication on a single TCP connection.

WebSocket makes data exchange between client and server easier, allowing the server to actively push data to the client. In the WebSocket API, the browser and the server only need to complete a handshake, and a persistent connection can be created directly between the two, and two-way data transmission can be performed.

In the WebSocket API, the browser and the server only need to do a handshake, and then a fast channel is formed between the browser and the server. Data can be directly transmitted between the two.

Enable WebSocket support

pom dependencies

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

Add the following configuration class

@Configuration
public class WebSocketConfig {
    
    

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

@EnableWebSocketIf it is not a dependency of the starter, you need to add annotations to the startup class

Simple usage example of WebSocket

@ServerEndpoint(value = "/websocket/document/{enterpriseId}/{userId}")
public class WebSocketServer {
    
    
	/**
	* 连接时触发
	*/
 	@OnOpen
    public void onOpen(@PathParam("enterpriseId") Long enterpriseId, @PathParam("userId") Long userId, Session session) {
    
    
    }

    /**
     * 收到客户端消息时触发
     */
    @OnMessage
    public void onMessage(@PathParam("enterpriseId") Long enterpriseId, @PathParam("userId") Long userId, Session session, String message) {
    
    
        // 发送指定消息
	    session.getBasicRemote().sendText("xxx");
    }

    /**
     * 异常时触发
     */
    @OnError
    public void onError(@PathParam("enterpriseId") Long enterpriseId, @PathParam("userId") Long userId, Session session, Throwable throwable) {
    
    
    }

    /**
     * 关闭连接时触发
     */
    @OnClose
    public void onClose(@PathParam("enterpriseId") Long enterpriseId, @PathParam("userId") Long userId, Session session) {
    
    
    }
}

Note 1: Automatic injection failure problem

Phenomenon:

  • @AutowiredWhen using @ServerEndpointto inject the properties of the marked class, it is displayed as null

reason:

  1. Every time a WebSocket connection is established, a new object will be generated, which is the object modified by @ServerEndpoint.
  2. From the perspective of the spring container, WebSocket is a multi-instance object rather than a singleton.

Solution: Inject class properties to achieve the effect of a singleton, as follows:

@Slf4j
@Component
@ServerEndpoint(value = "/websocket/document/{enterpriseId}/{userId}")
public class WebSocketServer {
    
    

    private static RedisTemplate<String, Object> redisTemplate;
    private static DocumentTransService documentTransService;
    private static ThreadPoolTaskExecutor executor;

    @Autowired
    public void setField(RedisTemplate<String, Object> redisTemplate,
                                 DocumentTransService documentTransService,
                                 @Qualifier("ws-pool") ThreadPoolTaskExecutor executor) {
    
    
        WebSocketServer.redisTemplate = redisTemplate;
        WebSocketServer.documentTransService = documentTransService;
        WebSocketServer.executor = executor;
    }
}

Note 2: IOException occurs frequently when closing the socket connection

Phenomenon:

  • When the front end quickly establishes and closes the socket connection, the back end will trigger an exceptionjava.io.IOException: 你的主机中的软件中止了一个已建立的连接

reason:

  1. Websocket uses nio, which is a new thread when establishing a connection, receiving a message, triggering an exception, and closing a connection, but it uses the same thread, you can Seesionview the following log

    2023-03-01 13:43:04.353  INFO 29700 --- [nio-8081-exec-5]  : client已连接 userId=1 session=org.apache.tomcat.websocket.WsSession@70c06a13 开始广播
    2023-03-01 13:43:05.794  INFO 29700 --- [io-8081-exec-10]  : client接收到信息 userId=1 session=org.apache.tomcat.websocket.WsSession@70c06a13 message=aaa
    2023-03-01 13:43:05.953  INFO 29700 --- [nio-8081-exec-7]  : client接收到信息 userId=1 session=org.apache.tomcat.websocket.WsSession@70c06a13 message=aaa
    2023-03-01 13:43:06.086  INFO 29700 --- [nio-8081-exec-8]  : client接收到信息 userId=1 session=org.apache.tomcat.websocket.WsSession@70c06a13 message=aaa
    2023-03-01 13:43:06.247  INFO 29700 --- [nio-8081-exec-2]  : client接收到信息 userId=1 session=org.apache.tomcat.websocket.WsSession@70c06a13 message=aaa
    2023-03-01 13:43:06.358  INFO 29700 --- [nio-8081-exec-1]  : client接收到信息 userId=1 session=org.apache.tomcat.websocket.WsSession@70c06a13 message=aaa
    2023-03-01 13:43:06.501  INFO 29700 --- [nio-8081-exec-3]  : client接收到信息 userId=1 session=org.apache.tomcat.websocket.WsSession@70c06a13 message=aaa
    2023-03-01 13:43:06.624  INFO 29700 --- [nio-8081-exec-6]  : client接收到信息 userId=1 session=org.apache.tomcat.websocket.WsSession@70c06a13 message=aaa
    2023-03-01 13:43:06.766  INFO 29700 --- [nio-8081-exec-5]  : client接收到信息 userId=1 session=org.apache.tomcat.websocket.WsSession@70c06a13 message=aaa
    2023-03-01 13:43:06.933  INFO 29700 --- [nio-8081-exec-9]  : client接收到信息 userId=1 session=org.apache.tomcat.websocket.WsSession@70c06a13 message=aaa
    2023-03-01 13:44:29.906  INFO 29700 --- [nio-8081-exec-2]  : client连接关闭 userId=1 session=org.apache.tomcat.websocket.WsSession@70c06a13
    
  2. If the user does not release the thread when establishing a connection, but uses the thread that established the connection to send a message, then an io exception will be triggered when the connection is closed

    That is to say, when establishing a connection, if the above-mentioned thread [nio-8081-exec-5] is used to push messages to the foreground while (true), then the thread establishing the connection will not be released, and when the socket connection is closed, it will Will trigger io exception

Solution: When the connection is established, use the background thread to push the message, and let @onOpenthe thread where it is located be released normally

  1. Create a WebSocketSenderclass to implement Runnable:

    @Slf4j
    public class WebSocketSender implements Runnable {
          
          
    
        private static final CloseReason reason = new CloseReason(CloseReason.CloseCodes.GOING_AWAY, "全部推送完毕,服务端主动关闭socket连接");
        private final AtomicBoolean atomicBoolean = new AtomicBoolean(true);
        private final Session session;
        private final Long enterpriseId;
        private final Long userId;
    
        public WebSocketSender(Session session, Long enterpriseId, Long userId) {
          
          
            this.session = session;
            this.enterpriseId = enterpriseId;
            this.userId = userId;
        }
    
        public void stop() {
          
          
            atomicBoolean.set(false);
            if (session.isOpen()) {
          
          
                try {
          
          
                    session.close(reason);
                } catch (IOException e) {
          
          
                    e.printStackTrace();
                }
            }
        }
    
        @Override
        public void run() {
          
          
            log.info("send ws...userId={} session={}", userId, session);
            int i = 0;
            while (atomicBoolean.get()) {
          
          
                if (!atomicBoolean.get()) {
          
          
                    break;
                }
                if (session.isOpen()) {
          
          
                    try {
          
          
                        session.getBasicRemote().sendText("xxx");
                    } catch (IOException e) {
          
          
                        e.printStackTrace();
                    }
                }
                i++;
                // 服务端主动关闭socket
                if (i == 10) stop();
            }
        }
    }
    
  2. Create a WebSocketUtilutility class to manage WebSocketSender:

    @Slf4j
    public class WebSocketUtil {
          
          
    
        private static final Map<Session, WebSocketSender> senders = new ConcurrentHashMap<>();
    
        public static void putSender(Session session, WebSocketSender webSocketSender) {
          
          
            senders.putIfAbsent(session, webSocketSender);
        }
    
        public static void removeAndCloseSender(Session session) {
          
          
            final WebSocketSender sender = senders.remove(session);
            if (sender != null)
                sender.stop();
        }
    
  3. When establishing a connection, create it WebSocketSender , and remove it from the cache when it is abnormal or closed WebSocketSender :

    	/**
    	* 连接时触发
    	*/
    	@OnOpen
        public void onOpen(@PathParam("enterpriseId") Long enterpriseId, @PathParam("userId") Long userId, Session session) {
          
          
            session.setMaxIdleTimeout(1000 * 60 * 30);
            log.info("client已连接 userId={} enterpriseId={} session={} 开始广播", userId, enterpriseId, session);
            WebSocketSender webSocketSender = new WebSocketSender(session, enterpriseId, userId);
            WebSocketUtil.putSender(session, sender);
            executor.execute(webSocketSender);
        }
        /**
         * 异常时触发
         */
        @OnError
        public void onError(@PathParam("enterpriseId") Long enterpriseId, @PathParam("userId") Long userId, Session session, Throwable throwable) {
          
          
            log.error("client连接异常 userId={} enterpriseId={} session={} message={}", userId, enterpriseId, session, throwable.getMessage());
            WebSocketUtil.removeAndCloseSender(session);
            throwable.printStackTrace();
        }
    
        /**
         * 关闭连接时触发
         */
        @OnClose
        public void onClose(@PathParam("enterpriseId") Long enterpriseId, @PathParam("userId") Long userId, Session session) {
          
          
            WebSocketUtil.removeAndCloseSender(session);
            log.info("client连接关闭 userId={} enterpriseId={} session={}", userId, enterpriseId, session);
        }
    

Note 3: How to receive url site and url parameters

  1. Receive url site parameters, obtained through @PathParam annotation

    @ServerEndpoint(value = "/websocket/document/{enterpriseId}/{userId}")
    public class WebSocketServer {
          
          
     	@OnOpen
        public void onOpen(@PathParam("enterpriseId") Long enterpriseId, @PathParam("userId") Long userId, Session session) {
          
          
        }
    }
    
  2. The url is passed as a parameter, obtained through the session.getQueryString() method:

    For example, if you want to get a list of ids when establishing a websocket connection, you can splice them after the url, and then use session.getQueryString()the method to get them. You can refer to the getDocumentIdSet() method below

    ws://192.168.111.67:65000/document-webSocket/websocket/document/1/1?idList=307,308,309,111
    
    public static Set<Long> getDocumentIdSet(Session session) {
          
          
        final String queryString = session.getQueryString();
        if (queryString==null||!queryString.contains("=")) return Collections.emptySet();
        final String[] strings = queryString.split("=");
        if (strings.length != 2) return Collections.emptySet();
        final String[] ids = strings[1].split(",");
        if (ids.length == 0) return Collections.emptySet();
        return Arrays.stream(ids).map(Long::parseLong).collect(Collectors.toSet());
    }
    

Note 4: How to receive header information

Just check this article: How springboot gets the header header information of websocket

Note 5: Use the status code to actively close the websocket

close status code Abbreviation reason
1000 normal shutdown The connection successfully fulfills the purpose for which it was created.
1001 leave The endpoint disappeared, either because of a server failure or because the browser left the page where the connection was opened.
1002 protocol error The endpoint is terminating the connection due to a protocol error.
1003 unsupported data The connection was terminated because the endpoint received an unacceptable data type. (e.g. a plain text endpoint receiving binary data
1004 temporarily reserved reserve. A meaning may be defined in the future.
1005 stateless reception reserve. Indicates that a status code was not provided, even if a status code is required
1006 abnormal shutdown reserve. Indicates that the connection was closed abnormally (i.e. no close frame was sent) when a status code is expected
1007 invalid frame payload data The endpoint is terminating the connection because a received message contained inconsistent data (for example, non-UTF-8 data in a text message)
1008 strategic conflict The endpoint is terminating the connection because it received a message that violated its policy. This is a generic status code and is used when codes 1003 and 1009 do not apply
1009 message too big The endpoint is terminating the connection because the received data frame was too large
1010 Mandatory extension The client is terminating the connection because it expected the server to negotiate one or more extensions, but the server did not
1011 internal error The server is terminating the connection because it encountered an unexpected condition and was unable to complete the request
1012 service restart The server is terminating the connection because it is restarting
1013 try again later The server is terminating the connection due to a temporary condition, e.g. it is overloaded and some clients are being dropped
1014 bad gateway The server is acting as a gateway or proxy and received an invalid response from an upstream server. This is similar to 502 HTTP status code
1015 TLS handshake reserve. Indicates that the connection was closed because the TLS handshake could not be performed (for example, the server certificate could not be verified)
1016–2999 Used to be defined by future versions of the WebSocket protocol specification, and by extensions to the specification
3000–3999 Used by libraries, frameworks and applications. These status codes are registered directly with IANA. The interpretation of these codes is not defined by the WebSocket protocol
4000–4999 For private use only, so registration is not possible. Previous protocols between WebSocket applications can use these codes. The interpretation of these codes is not defined by the WebSocket protocol

Guess you like

Origin blog.csdn.net/weixin_43702146/article/details/129277371