Springboot 系列 (10) - Springboot+WebSocket(一) | 使用 Spring 封装的 WebSocket 实现 JSON 消息实例

 

WebSocket 是HTML5一种新的协议,实现了浏览器与服务器全双工通信。其本质是先通过HTTP/HTTPS协议进行握手后创建一个用于交换数据的TCP连接,服务端与客户端通过此TCP连接进行实时通信。

Spring 框架下可以使用以下方式来实现 WebSocket 功能:

    (1) Spring 封装的 WebSocket;
    (2) STOMP 协议,STOMP 协议是 WebSocket 的一个子协议;
    (3) WebSocket 原生注解;

    以上三种方式,都包含在 spring-boot-starter-websocket 包里,前两种使用起来方便,这里暂时不花时间在第三种方式上。

本文使用 Spring 封装的 WebSocket 来实现发送 JSON 消息实例。


1. 开发环境

    Windows版本:Windows 10 Home (20H2)   
    IntelliJ IDEA (https://www.jetbrains.com/idea/download/):Community Edition for Windows 2020.1.4
    Apache Maven (https://maven.apache.org/):3.8.1

    注:Spring 开发环境的搭建,可以参考 “ Spring基础知识(1)- Spring简介、Spring体系结构和开发环境配置 ”。


2. 创建 Spring Boot 基础项目


    项目实例名称:SpringbootExample10
    Spring Boot 版本:2.6.6

    创建步骤:

        (1) 创建 Maven 项目实例 SpringbootExample10;
        (2) Spring Boot Web 配置;
        (3) 导入 Thymeleaf 依赖包;
        (4) 配置 jQuery;
        
    具体操作请参考 “Springboot 系列 (2) - 在 Spring Boot 项目里使用 Thymeleaf、JQuery+Bootstrap 和国际化” 里的项目实例 SpringbootExample02,文末包含如何使用 spring-boot-maven-plugin 插件运行打包的内容。

    SpringbootExample10 和 SpringbootExample02 相比,SpringbootExample10 不配置 Bootstrap、模版文件(templates/*.html)和国际化。


3. 导入 WebSocket、FastJson 依赖包

    修改 pom.xml

 1         <project ... >
 2             ...
 3             <dependencies>
 4                 ...
 5 
 6                 <!-- WebSocket -->
 7                 <dependency>
 8                     <groupId>org.springframework.boot</groupId>
 9                     <artifactId>spring-boot-starter-websocket</artifactId>
10                 </dependency>
11                 <!-- JSON -->
12                 <dependency>
13                     <groupId>com.alibaba</groupId>
14                     <artifactId>fastjson</artifactId>
15                     <version>1.2.79</version>
16                 </dependency>
17 
18                 ...
19             </dependencies>
20 
21             ...
22         </project>


    在IDE中项目列表 -> SpringbootExample10 -> 点击鼠标右键 -> Maven -> Reload Project


4. 配置 Spring 封装的 WebSocket

    1) 创建 src/main/java/com/example/ws/WSConfig.java 文件

 1         package com.example.ws;
 2 
 3         import org.springframework.beans.factory.annotation.Autowired;
 4         import org.springframework.context.annotation.Configuration;
 5         import org.springframework.web.socket.config.annotation.EnableWebSocket;
 6         import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
 7         import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
 8 
 9         @Configuration
10         @EnableWebSocket
11         public class WSConfig implements WebSocketConfigurer {
12             @Autowired
13             private WSTextHandler wsTextHandler;
14             @Autowired
15             private WSInterceptor wsInterceptor;
16 
17             @Override
18             public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
19                 registry.addHandler(wsTextHandler, "/websocket")
20                         .addInterceptors(wsInterceptor)
21                         .setAllowedOrigins("*"); // Allow cross site
22             }
23 
24         }


    2) 创建 src/main/java/com/example/ws/WSInterceptor.java 文件

 1         package com.example.ws;
 2 
 3         import java.util.Map;
 4 
 5         import org.springframework.stereotype.Component;
 6         import org.springframework.http.server.ServerHttpRequest;
 7         import org.springframework.http.server.ServerHttpResponse;
 8         import org.springframework.web.socket.WebSocketHandler;
 9         import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
10 
11         @Component
12         public class WSInterceptor extends HttpSessionHandshakeInterceptor {
13             @Override
14             public boolean beforeHandshake(ServerHttpRequest request,
15                                         ServerHttpResponse response,
16                                         WebSocketHandler wsHandler,
17                                         Map<String, Object> attributes) throws Exception {
18                 return super.beforeHandshake(request, response, wsHandler, attributes);
19             }
20 
21             @Override
22             public void afterHandshake(ServerHttpRequest request,
23                                     ServerHttpResponse response,
24                                     WebSocketHandler wsHandler,
25                                     Exception ex) {
26                 super.afterHandshake(request, response, wsHandler, ex);
27             }
28 
29         }


    3) 创建 src/main/java/com/example/ws/WSTextHandler.java 文件

 1         package com.example.ws;
 2 
 3         import java.util.List;
 4         import java.util.Map;
 5         import java.util.HashMap;
 6         import java.util.concurrent.CopyOnWriteArrayList;
 7 
 8         import org.springframework.stereotype.Component;
 9         import org.springframework.web.socket.CloseStatus;
10         import org.springframework.web.socket.TextMessage;
11         import org.springframework.web.socket.WebSocketMessage;
12         import org.springframework.web.socket.WebSocketSession;
13         import org.springframework.web.socket.handler.TextWebSocketHandler;
14 
15         import com.alibaba.fastjson.JSON;
16         import com.alibaba.fastjson.JSONObject;
17 
18         @Component
19         public class WSTextHandler extends TextWebSocketHandler {
20 
21             private List<WebSocketSession> clientSessions = new CopyOnWriteArrayList<>();
22 
23             @Override
24             public void afterConnectionEstablished(WebSocketSession session) throws Exception {
25                 System.out.println("WSTextHandler -> afterConnectionEstablished(): session.getId() = " + session.getId());
26 
27                 clientSessions.add(session);
28             }
29 
30             @Override
31             public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
32                 System.out.println("WSTextHandler -> afterConnectionClosed(): session.getId() = "
33                                 + session.getId() + ", status.getCode() = " + status.getCode());
34 
35                 clientSessions.remove(session);
36             }
37 
38             @Override
39             public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
40                 System.out.println("WSTextHandler -> handleMessage() -> message = "
41                                     + message.getPayload().toString());
42 
43                 Map<String, Object> retMap = new HashMap<>();
44 
45                 JSONObject recvJson = (JSONObject) JSON.parse(message.getPayload().toString());
46                 String opt = recvJson.getString("operation");
47 
48                 if ("command".equals(opt)) {
49 
50                     String msg = recvJson.getString("message");
51 
52                     if (msg == null || msg.isEmpty()) {
53                         retMap.put("ret", "error");
54                         retMap.put("description", "Invalid command format");
55                         session.sendMessage(new TextMessage(JSON.toJSON(retMap).toString()));
56                         return;
57                     }
58 
59                     retMap.put("ret", "data");
60                     retMap.put("message", msg + " (Reply from server)");
61                     session.sendMessage(new TextMessage(JSON.toJSON(retMap).toString()));
62 
63                 } else if ("close".equals(opt)) {
64 
65                     retMap.put("ret", "finish");
66                     retMap.put("description", "Finish directly");
67                     session.sendMessage(new TextMessage(JSON.toJSON(retMap).toString()));
68 
69                 } else {
70 
71                     retMap.put("ret", "error");
72                     retMap.put("description", "Invalid data format");
73                     session.sendMessage(new TextMessage(JSON.toJSON(retMap).toString()));
74 
75                 }
76 
77             }
78         }


5. 测试实例 (Web 模式)

    1) 创建 src/main/resources/templates/client.html 文件

  1         <html lang="en" xmlns:th="http://www.thymeleaf.org">
  2         <head>
  3             <meta charset="UTF-8">
  4             <title th:text="${var}">Client</title>
  5             <script language="javascript" th:src="@{/lib/jquery/jquery-3.6.0.min.js}"></script>
  6         </head>
  7         <body>
  8 
  9             <h4>WebSocket - Client</h4>
 10             <p>&nbsp;</p>
 11 
 12             <p>
 13                 <label><strong>WebSocket url:</strong></label><br>
 14                 <input type="text" name="ws_url" id="ws_url" th:value="'ws://'+${#request.getServerName()}+':'+${#request.getServerPort()}+@{/websocket}" value="ws://localhost:9090/websocket" style="width: 50%; height: 32px;" /><br><br>
 15                 <button type="button" id="btn_connect" class="btn btn-default btn-sm">Connect</button>
 16                 <button type="button" id="btn_close" class="btn btn-default btn-sm" style="display: none;">Close</button>
 17             </p>
 18 
 19             <p id="message_area" style="display: none;">
 20                 <label><strong>Message:</strong></label><br>
 21                 <input type="text" name="message" id="message" style="width: 50%; height: 32px;" /><br><br>
 22                 <button type="button" id="btn_send" class="btn btn-default btn-sm">Send</button>
 23             </p>
 24 
 25             <p>&nbsp;</p>
 26 
 27             <div id="result_area" style="padding: 15px; width: 50%;  font-size: 12px; min-height: 120px;">
 28             </div>
 29 
 30             <script type="text/javascript">
 31                 var globalSocket = null;
 32 
 33                 $(document).ready(function() {
 34                     $("#btn_connect").click(function(e) {
 35                         connectWebSocket();
 36                     });
 37 
 38                     $("#btn_close").click(function(e) {
 39                         closeWebSocket();
 40                     });
 41 
 42                     $("#btn_send").click(function(e) {
 43                     if (globalSocket != null) {
 44                             var msg = $("#message").val();
 45                             if (msg == '') {
 46                                 alert("Please enter message");
 47                                 $("#message").focus();
 48                                 return;
 49                             }
 50 
 51                             var data = {
 52                                 "operation": "command",
 53                                 "message": msg
 54                             }
 55                             globalSocket.send(JSON.stringify(data));
 56                     }
 57                     });
 58                 });
 59 
 60                 function connectWebSocket() {
 61                     var wsUrl = $("#ws_url").val();
 62                     if (wsUrl == '') {
 63                         alert("Please enter url");
 64                         $("#ws_url").focus();
 65                         return;
 66                     }
 67 
 68                     if (globalSocket == null) {
 69                         $("#result_area").html('');
 70                         $("#btn_execute").attr("disabled", "disabled");
 71 
 72                         createWebSocket(wsUrl);
 73                     }
 74                 }
 75 
 76                 function createWebSocket(url) {
 77                     if (globalSocket != null || url == '')
 78                         return;
 79 
 80                     console.log("createWebSocket(): url = ", url);
 81                     globalSocket = new WebSocket(url);
 82                     globalSocket.onopen = funcWSOpen;
 83                     globalSocket.onclose = funcWSClose;
 84                     globalSocket.onerror = funcWSError;
 85                     globalSocket.onmessage = funcWSMessage;
 86                 }
 87 
 88                 function closeWebSocket() {
 89                     if (globalSocket != null) {
 90                         console.log("closeWebSocket(): send close");
 91                         globalSocket.send(JSON.stringify({ "operation": "close"}));
 92                         $("#btn_close").attr("disabled", "disabled");
 93                         $("#message_area").css("display", "none");
 94                     }
 95                 }
 96 
 97                 function funcWSOpen(e) {
 98                     console.log("funcWSOpen(): ", e);
 99 
100                     $("#message_area").css("display", "");
101                     $("#btn_close").removeAttr("disabled");
102                     $("#btn_close").css("display", "");
103 
104                     $("#result_area").append("<br>WSOpen: Connected<br>");
105                 }
106 
107                 function funcWSClose(e) {
108                     console.log("funcWSClose(): ", e);
109 
110                     $("#result_area").append("<br>WSClose: Close<br>");
111                     $("#btn_execute").removeAttr("disabled");
112                     $("#btn_close").css("display", "none");
113                     globalSocket = null;
114                 }
115 
116                 function funcWSError(e) {
117                     console.error("funcWSError(): ", e);
118 
119                     $("#result_area").append("<br>WSError: Error<br>");
120                     $("#btn_execute").removeAttr("disabled");
121                     $("#btn_close").css("display", "none");
122                     globalSocket = null;
123                 }
124 
125                 function funcWSMessage(e) {
126                     console.log("funcWSMessage(): e.data = ", e.data);
127 
128                     var dataObj = JSON.parse(e.data);
129                     if (dataObj['ret'] == "data") {
130                         $("#result_area").append("<br>WSMessage: " + dataObj['message'] + "<br>");
131                     } else if (dataObj['ret'] == "finish") {
132                         console.log("funcWSMessage(): ", dataObj['description'])
133                         $("#result_area").append("<br>WSMessage: " + dataObj['description'] + "<br>");
134                         globalSocket.close(3009)
135                     } else if (dataObj['ret'] == "error") {
136                         console.log("funcWSMessage(): ", dataObj['description']);
137                         $("#result_area").append("<br>WSMessage: " + dataObj['description'] + "<br>");
138                     } else {
139                         $("#result_area").append("<br>WSMessage: Invalid data format<br>");
140                     }
141                 }
142             </script>
143         </body>
144         </html>


    2) 修改 src/main/java/com/example/controller/IndexController.java 文件

 1         package com.example.controller;
 2 
 3         import org.springframework.stereotype.Controller;
 4         import org.springframework.web.bind.annotation.RequestMapping;
 5         import org.springframework.web.bind.annotation.ResponseBody;
 6 
 7         @Controller
 8         public class IndexController {
 9             @ResponseBody
10             @RequestMapping("/test")
11             public String test() {
12                 return "Test Page";
13             }
14 
15             @RequestMapping("/client")
16             public String client() {
17                 return "client";
18             }
19 
20         }


    访问 http://localhost:9090/client,JAR 方式运行时,页面上 WebSocket url 是 ws://localhost:9090/websocket,点击 Connect 按钮测试。
    
    注:在 Tomcat 容器里以 WAR 方式运行时,页面上 WebSocket url 是 ws://localhost:8080/SpringbootExample10/websocket (Tomcat 默认端口是 8080)。


--------------------------------------

示例代码:https://gitee.com/slksm/public-codes/tree/master/demos/springboot-series/SpringbootExample10

 

posted @ 2022-05-20 20:38  垄山小站  阅读(975)  评论(0)    收藏  举报