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();
}
}
@EnableWebSocket
If 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:
@Autowired
When using@ServerEndpoint
to inject the properties of the marked class, it is displayed as null
reason:
- Every time a WebSocket connection is established, a new object will be generated, which is the object modified by @ServerEndpoint.
- 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 exception
java.io.IOException: 你的主机中的软件中止了一个已建立的连接
reason:
-
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
Seesion
view the following log2023-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
-
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 @onOpen
the thread where it is located be released normally
-
Create a
WebSocketSender
class to implementRunnable
:@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(); } } }
-
Create a
WebSocketUtil
utility class to manageWebSocketSender
:@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(); }
-
When establishing a connection, create it
WebSocketSender
, and remove it from the cache when it is abnormal or closedWebSocketSender
:/** * 连接时触发 */ @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
-
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) { } }
-
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 belowws://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 |