当前位置:   article > 正文

基于Spring封装一个websocket工具类使用事件发布进行解耦和管理

基于Spring封装一个websocket工具类使用事件发布进行解耦和管理

最近工作中,需要将原先的Http请求换成WebSocket,故此需要使用到WebSocket与前端交互。故此这边需要研究一下WebSocket到底有何优点和不可替代性:

WebSocket优点:

WebSocket 协议提供了一种在客户端和服务器之间进行全双工通信的机制,这意味着客户端和服务器可以在任何时候互相发送消息,而不需要预先建立请求。与传统的 HTTP 轮询相比,WebSocket 有以下不可替代的优点:

1. 低延迟: WebSocket 提供了真正的实时通信能力,因为它允许服务器在数据可用时立即将其推送到客户端。这比 HTTP 轮询的“询问-回答”模式更高效,轮询模式可能会引入不必要的延迟。

2. 减少网络流量: 在 HTTP 轮询中,客户端需要定期发送请求以检查更新,即使没有更新也是如此。这会产生大量冗余的 HTTP 头部信息和请求响应。相比之下,WebSocket 在建立连接后,只需要非常少的控制开销就可以发送和接收消息。

3. 持久连接: WebSocket 使用单个持久连接进行通信,而不需要为每个消息或请求重新建立连接。这减少了频繁建立和关闭连接的开销,提高了效率。

4. 双向通信: WebSocket 支持全双工通信,客户端和服务器可以同时发送消息,而不需要等待对方的响应。这对于需要快速双向数据交换的应用程序来说是非常重要的。

5. 更好的服务器资源利用: 由于 WebSocket 连接是持久的,服务器可以更有效地管理资源,而不是在每个轮询请求中重新初始化资源。

6. 协议开销小: WebSocket 消息包含非常少的协议开销,相比之下,HTTP 协议的每个请求/响应都包含了完整的头部信息。

7. 支持二进制数据: WebSocket 不仅支持文本数据,还支持二进制数据,这使得它可以用于更广泛的应用场景,如游戏、视频流和其他需要高效二进制数据传输的应用。

8. 兼容性: 尽管是较新的技术,WebSocket 已经得到了现代浏览器的广泛支持,并且可以通过 Polyfills 在不支持的浏览器上使用。

时序图:

 

这个流程图展示了以下步骤:

  1. 握手阶段:客户端向服务器发送 WebSocket 连接请求,服务器响应并切换协议。
  2. 连接建立:WebSocket 连接建立后,客户端和服务器可以相互发送消息。
  3. 通信循环:客户端和服务器在建立的 WebSocket 连接上进行消息交换。
  4. 关闭握手:客户端或服务器发起关闭连接的请求,另一方响应,然后连接关闭。

因为以上优点这边将需要重新构建一套WebSocket工具类实现这边的要求:

工具类实现:

在 Spring 中封装 WebSocket 工具类通常涉及使用 Spring 提供的 WebSocket API。

WebSocketUtils

WebSocket 工具类封装示例,它使用 Spring 的 WebSocketSession 来发送消息给客户端。

  1. 异常处理: 在发送消息时,如果发生异常,我们可以添加更详细的异常处理逻辑。
  2. 会话管理: 我们可以添加同步块或使用 ConcurrentHashMap 的原子操作来确保线程安全。
  3. 用户标识符管理: 提供一个更灵活的方式来管理用户标识符和会话之间的关系。
  4. 事件发布: 使用 Spring 事件发布机制来解耦和管理 WebSocket 事件。

  1. import org.springframework.beans.factory.annotation.Autowired;
  2. import org.springframework.context.ApplicationEvent;
  3. import org.springframework.context.ApplicationEventPublisher;
  4. import org.springframework.stereotype.Component;
  5. import org.springframework.web.socket.CloseStatus;
  6. import org.springframework.web.socket.TextMessage;
  7. import org.springframework.web.socket.WebSocketSession;
  8. import org.springframework.web.socket.handler.TextWebSocketHandler;
  9. import java.io.IOException;
  10. import java.util.Map;
  11. import java.util.concurrent.ConcurrentHashMap;
  12. /**
  13. * @Author derek_smart
  14. * @Date 202/5/11 10:05
  15. * @Description WebSocket 工具类
  16. */
  17. @Component
  18. public class WebSocketUtils extends TextWebSocketHandler {
  19. private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
  20. @Autowired
  21. private ApplicationEventPublisher eventPublisher;
  22. public void registerSession(String userIdentifier, WebSocketSession session) {
  23. sessions.put(userIdentifier, session);
  24. // Publish an event when a session is registered
  25. eventPublisher.publishEvent(new WebSocketSessionRegisteredEvent(this, session, userIdentifier));
  26. }
  27. public void removeSession(String userIdentifier) {
  28. WebSocketSession session = sessions.remove(userIdentifier);
  29. if (session != null) {
  30. // Publish an event when a session is removed
  31. eventPublisher.publishEvent(new WebSocketSessionRemovedEvent(this, session, userIdentifier));
  32. }
  33. }
  34. public void sendMessageToUser(String userIdentifier, String message) {
  35. WebSocketSession session = sessions.get(userIdentifier);
  36. if (session != null && session.isOpen()) {
  37. try {
  38. session.sendMessage(new TextMessage(message));
  39. } catch (IOException e) {
  40. // Handle the exception, e.g., logging or removing the session
  41. handleWebSocketException(session, e);
  42. }
  43. }
  44. }
  45. public void sendMessageToAllUsers(String message) {
  46. TextMessage textMessage = new TextMessage(message);
  47. sessions.forEach((userIdentifier, session) -> {
  48. if (session.isOpen()) {
  49. try {
  50. session.sendMessage(textMessage);
  51. } catch (IOException e) {
  52. // Handle the exception, e.g., logging or removing the session
  53. handleWebSocketException(session, e);
  54. }
  55. }
  56. });
  57. }
  58. private void handleWebSocketException(WebSocketSession session, IOException e) {
  59. // Log the exception
  60. // Attempt to close the session if it's still open
  61. if (session.isOpen()) {
  62. try {
  63. session.close();
  64. } catch (IOException ex) {
  65. // Log the exception during close
  66. }
  67. }
  68. // Remove the session from the map
  69. sessions.values().remove(session);
  70. // Further exception handling...
  71. }
  72. @Override
  73. public void afterConnectionEstablished(WebSocketSession session) throws Exception {
  74. // This method can be overridden to handle connection established event
  75. }
  76. @Override
  77. public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
  78. // This method can be overridden to handle connection closed event
  79. }
  80. // Additional methods like handleTextMessage can be overridden if needed
  81. // Custom events
  82. public static class WebSocketSessionRegisteredEvent extends ApplicationEvent {
  83. private final WebSocketSession session;
  84. private final String userIdentifier;
  85. /**
  86. * Create a new WebSocketSessionRegisteredEvent.
  87. * @param source the object on which the event initially occurred (never {@code null})
  88. * @param session the WebSocket session which has been registered
  89. * @param userIdentifier the identifier of the user for whom the session is registered
  90. */
  91. public WebSocketSessionRegisteredEvent(Object source, WebSocketSession session, String userIdentifier) {
  92. super(source);
  93. this.session = session;
  94. this.userIdentifier = userIdentifier;
  95. }
  96. public WebSocketSession getSession() {
  97. return session;
  98. }
  99. public String getUserIdentifier() {
  100. return userIdentifier;
  101. }
  102. }
  103. public static class WebSocketSessionRemovedEvent extends ApplicationEvent {
  104. private final WebSocketSession session;
  105. private final String userIdentifier;
  106. /**
  107. * Create a new WebSocketSessionRemovedEvent.
  108. * @param source the object on which the event initially occurred (never {@code null})
  109. * * @param session the WebSocket session which has been removed
  110. * * @param userIdentifier the identifier of the user for whom the session was removed
  111. * */
  112. public WebSocketSessionRemovedEvent(Object source, WebSocketSession session, String userIdentifier) {
  113. super(source);
  114. this.session = session;
  115. this.userIdentifier = userIdentifier;
  116. }
  117. public WebSocketSession getSession() {
  118. return session;
  119. }
  120. public String getUserIdentifier() {
  121. return userIdentifier;
  122. }
  123. }
  124. }

 

 

在这个工具类中,我们使用了 ConcurrentHashMap 来存储和管理 WebSocket 会话。每个会话都与一个用户标识符相关联,这允许我们向特定用户发送消息。 使用了 ApplicationEventPublisher 来发布会话注册和移除事件,这样可以让其他组件在需要时响应这些事件。

另外,我们让 WebSocketUtils 继承了 TextWebSocketHandler,这样它可以直接作为一个 WebSocket 处理器。这意味着你可以重写
afterConnectionEstablished 和 afterConnectionClosed 方法来处理连接建立和关闭的事件,而不是在一个单独的 WebSocketHandler 中处理它们。

通过这些优化,WebSocketUtils 工具类变得更加健壮和灵活,能够更好地集成到 Spring 应用程序中

工具类提供了以下方法:

  • registerSession: 当新的 WebSocket 连接打开时,将该会话添加到映射中。
  • removeSession: 当 WebSocket 连接关闭时,从映射中移除该会话。
  • sendMessageToUser: 向特定用户发送文本消息。
  • sendMessageToAllUsers: 向所有连接的用户发送文本消息。
  • getSessions: 返回当前所有的 WebSocket 会话。

WebSocketHandler

为了完整地实现一个 WebSocket 工具类,你还需要创建一个 WebSocketHandler 来处理 WebSocket 事件,如下所示:

  1. import org.springframework.web.socket.CloseStatus;
  2. import org.springframework.web.socket.TextMessage;
  3. import org.springframework.web.socket.WebSocketHandler;
  4. import org.springframework.web.socket.WebSocketMessage;
  5. import org.springframework.web.socket.WebSocketSession;
  6. /**
  7. * @Author derek_smart
  8. * @Date 202/5/11 10:05
  9. * @Description WebSocketHandler
  10. */
  11. public class MyWebSocketHandler implements WebSocketHandler {
  12. private final WebSocketUtils webSocketUtils;
  13. public MyWebSocketHandler(WebSocketUtils webSocketUtils) {
  14. this.webSocketUtils = webSocketUtils;
  15. }
  16. @Override
  17. public void afterConnectionEstablished(WebSocketSession session) throws Exception {
  18. String userIdentifier = retrieveUserIdentifier(session);
  19. webSocketUtils.registerSession(userIdentifier, session);
  20. }
  21. @Override
  22. public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
  23. // Handle incoming messages
  24. }
  25. @Override
  26. public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
  27. // Handle transport error
  28. }
  29. @Override
  30. public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
  31. String userIdentifier = retrieveUserIdentifier(session);
  32. webSocketUtils.removeSession(userIdentifier);
  33. }
  34. @Override
  35. public boolean supportsPartialMessages() {
  36. return false;
  37. }
  38. private String retrieveUserIdentifier(WebSocketSession session) {
  39. // Implement logic to retrieve the user identifier from the session
  40. return session.getId(); // For example, use the WebSocket session ID
  41. }
  42. }

在 MyWebSocketHandler 中,我们处理了 WebSocket 连接的建立和关闭事件,并且在这些事件发生时调用 WebSocketUtils 的方法注册或移除会话。

WebSocketConfig

要在 Spring 中配置 WebSocket,你需要在配置类中添加 WebSocketHandler 和 WebSocket 的映射。以下是一个简单的配置示例:

  1. import org.springframework.context.annotation.Configuration;
  2. import org.springframework.web.socket.config.annotation.EnableWebSocket;
  3. import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
  4. import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
  5. @Configuration
  6. @EnableWebSocket
  7. public class WebSocketConfig implements WebSocketConfigurer {
  8. private final MyWebSocketHandler myWebSocketHandler;
  9. public WebSocketConfig(WebSocketUtils webSocketUtils) {
  10. this.myWebSocketHandler = new MyWebSocketHandler(webSocketUtils);
  11. }
  12. @Override
  13. public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
  14. registry.addHandler(myWebSocketHandler, "/ws").setAllowedOrigins("*");
  15. }
  16. }

WebSocketEventListener

我们扩展了 ApplicationEvent 类,这是所有 Spring 事件的基类。我们添加了两个字段:session 和 userIdentifier,以及相应的构造函数和访问器方法。这样,当事件被发布时,监听器可以访问到事件的详细信息。

要发布这个事件,你需要注入 ApplicationEventPublisher 到你的 WebSocketUtils 类中,并在会话注册时调用 publishEvent 方法: 要发布这个事件,你需要在会话移除时调用 ApplicationEventPublisher 的 publishEvent 方法

  1. import org.springframework.context.event.EventListener;
  2. import org.springframework.stereotype.Component;
  3. import org.springframework.web.socket.WebSocketSession;
  4. @Component
  5. public class WebSocketEventListener {
  6. @EventListener
  7. public void handleWebSocketSessionRegistered(WebSocketUtils.WebSocketSessionRegisteredEvent event) {
  8. // 处理会话注册事件
  9. WebSocketSession session = event.getSession();
  10. String userIdentifier = event.getUserIdentifier();
  11. // 你可以在这里添加你的逻辑,例如发送欢迎消息或记录会话信息
  12. }
  13. @EventListener
  14. public void handleWebSocketSessionRemoved(WebSocketUtils.WebSocketSessionRemovedEvent event) {
  15. // 处理会话移除事件
  16. WebSocketSession session = event.getSession();
  17. String userIdentifier = event.getUserIdentifier();
  18. // 你可以在这里添加你的逻辑,例如更新用户状态或释放资源
  19. }
  20. }

类图:

 

在这个类图中,我们展示了以下类及其关系:

  • WebSocketUtils: 包含了会话管理和消息发送的方法。
  • WebSocketSessionRegisteredEvent: 一个自定义事件类,用于表示 WebSocket 会话注册事件。
  • WebSocketSessionRemovedEvent: 一个自定义事件类,用于表示 WebSocket 会话移除事件。
  • WebSocketEventListener: 一个监听器类,它监听并处理 WebSocket 会话相关的事件。
  • WebSocketController: 一个控制器类,用于处理 HTTP 请求并使用 WebSocketUtils 类的方法。
  • ChatWebSocketHandler: 一个 WebSocket 处理器类,用于处理 WebSocket 事件。
  • WebSocketConfig: 配置类,用于注册 WebSocket 处理器。

测试类实现:

WebSocketController

构建一个简单的聊天应用程序,我们将使用 WebSocketUtils 来管理 WebSocket 会话并向用户发送消息。 在这个示例中,我们将创建一个控制器来处理发送消息的请求,并使用 WebSocketUtils 类中的方法来实际发送消息。 首先,确保 WebSocketUtils 类已经被定义并包含了之前讨论的方法。 接下来,我们将创建一个 WebSocketController 来处理发送消息的请求:

  1. import org.springframework.beans.factory.annotation.Autowired;
  2. import org.springframework.http.ResponseEntity;
  3. import org.springframework.web.bind.annotation.*;
  4. /**
  5. * @Author derek_smart
  6. * @Date 202/5/11 10:09
  7. * @Description WebSocket 测试类
  8. */
  9. @RestController
  10. @RequestMapping("/chat")
  11. public class WebSocketController {
  12. private final WebSocketUtils webSocketUtils;
  13. @Autowired
  14. public WebSocketController(WebSocketUtils webSocketUtils) {
  15. this.webSocketUtils = webSocketUtils;
  16. }
  17. // 发送消息给特定用户
  18. @PostMapping("/send-to-user")
  19. public ResponseEntity<?> sendMessageToUser(@RequestParam String userIdentifier, @RequestParam String message) {
  20. try {
  21. webSocketUtils.sendMessageToUser(userIdentifier, message);
  22. return ResponseEntity.ok().build();
  23. } catch (IOException e) {
  24. // 日志记录异常,返回错误响应
  25. return ResponseEntity.status(500).body("Failed to send message to user.");
  26. }
  27. }
  28. // 发送广播消息给所有用户
  29. @PostMapping("/broadcast")
  30. public ResponseEntity<?> broadcastMessage(@RequestParam String message) {
  31. webSocketUtils.sendMessageToAllUsers(message);
  32. return ResponseEntity.ok().build();
  33. }
  34. }

在 WebSocketController 中,我们提供了两个端点:一个用于向特定用户发送消息,另一个用于广播消息给所有连接的用户。

 

ChatWebSocketHandler

现在,让我们创建一个 WebSocketHandler 来处理 WebSocket 事件:

  1. import org.springframework.web.socket.CloseStatus;
  2. import org.springframework.web.socket.TextMessage;
  3. import org.springframework.web.socket.WebSocketSession;
  4. import org.springframework.web.socket.handler.TextWebSocketHandler;
  5. public class ChatWebSocketHandler extends TextWebSocketHandler {
  6. private final WebSocketUtils webSocketUtils;
  7. public ChatWebSocketHandler(WebSocketUtils webSocketUtils) {
  8. this.webSocketUtils = webSocketUtils;
  9. }
  10. @Override
  11. public void afterConnectionEstablished(WebSocketSession session) throws Exception {
  12. // 使用用户的唯一标识符注册会话
  13. String userIdentifier = retrieveUserIdentifier(session);
  14. webSocketUtils.registerSession(userIdentifier, session);
  15. }
  16. @Override
  17. protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
  18. // 处理接收到的消息,例如在聊天室中广播
  19. String userIdentifier = retrieveUserIdentifier(session);
  20. webSocketUtils.sendMessageToAllUsers("User " + userIdentifier + " says: " + message.getPayload());
  21. }
  22. @Override
  23. public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
  24. // 移除会话
  25. String userIdentifier = retrieveUserIdentifier(session);
  26. webSocketUtils.removeSession(userIdentifier);
  27. }
  28. private String retrieveUserIdentifier(WebSocketSession session) {
  29. // 根据实际情况提取用户标识符,这里假设使用 WebSocketSession 的 ID
  30. return session.getId();
  31. }
  32. }

在 ChatWebSocketHandler 中,我们处理了连接建立和关闭事件,并在这些事件发生时调用 WebSocketUtils 的方法注册或移除会话。我们还实现了 handleTextMessage 方法来处理接收到的文本消息。

WebSocketConfig

最后,我们需要配置 WebSocket 端点:

  1. import org.springframework.context.annotation.Configuration;
  2. import org.springframework.web.socket.config.annotation.EnableWebSocket;
  3. import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
  4. import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
  5. @Configuration
  6. @EnableWebSocket
  7. public class WebSocketConfig implements WebSocketConfigurer {
  8. private final ChatWebSocketHandler chatWebSocketHandler;
  9. public WebSocketConfig(WebSocketUtils webSocketUtils) {
  10. this.chatWebSocketHandler = new ChatWebSocketHandler(webSocketUtils);
  11. }
  12. @Override
  13. public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
  14. registry.addHandler(chatWebSocketHandler, "/ws/chat").setAllowedOrigins("*");
  15. }
  16. }

在 WebSocketConfig 配置类中,我们注册了 ChatWebSocketHandler 到 /ws/chat 路径。这样,客户端就可以通过这个路径来建立 WebSocket 连接。

这个示例展示了如何使用 WebSocketUtils 工具类来管理 WebSocket 会话,并通过 REST 控制器端点发送消息。客户端可以连接到 WebSocket 端点,并使用提供的 REST 端点发送和接收消息。

总结:

总的来说,WebSocket 在需要快速、实时、双向通信的应用中提供了显著的优势,例如在线游戏、聊天应用、实时数据监控和协作工具。然而,不是所有的场景都需要 WebSocket 的能力,对于不需要实时通信的应用,传统的 HTTP 请求或 HTTP 轮询可能仍然是合适的。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/594756
推荐阅读
相关标签
  

闽ICP备14008679号