Spring 系列 (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 开发环境的搭建,可以参考 “ ”。


2. 创建 Spring Boot 基础项目


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

    创建步骤:

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

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


3. 导入 WebSocket、FastJson 依赖包

    修改 pom.xml

 1         <project ... >
 2             ...
 3             <dependencies>
 4                 ...
 5 
 6                 
 7                 <dependency>
 8                     <groupId>org.springframework.bootgroupId>
 9                     <artifactId>spring-boot-starter-websocketartifactId>
10                 dependency>
11                 
12                 <dependency>
13                     <groupId>com.alibabagroupId>
14                     <artifactId>fastjsonartifactId>
15                     <version>1.2.79version>
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 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 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 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}">Clienttitle>
  5             <script language="javascript" th:src="@{/lib/jquery/jquery-3.6.0.min.js}">script>
  6         head>
  7         <body>
  8 
  9             <h4>WebSocket - Clienth4>
 10             <p> 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">Connectbutton>
 16                 <button type="button" id="btn_close" class="btn btn-default btn-sm" style="display: none;">Closebutton>
 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">Sendbutton>
 23             p>
 24 
 25             <p> 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("
WSOpen: Connected
"); 105 } 106 107 function funcWSClose(e) { 108 console.log("funcWSClose(): ", e); 109 110 $("#result_area").append("
WSClose: Close
"); 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("
WSError: Error
"); 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("
WSMessage:
" + dataObj['message'] + "
"); 131 } else if (dataObj['ret'] == "finish") { 132 console.log("funcWSMessage(): ", dataObj['description']) 133 $("#result_area").append("
WSMessage:
" + dataObj['description'] + "
"); 134 globalSocket.close(3009) 135 } else if (dataObj['ret'] == "error") { 136 console.log("funcWSMessage(): ", dataObj['description']); 137 $("#result_area").append("
WSMessage:
" + dataObj['description'] + "
"); 138 } else { 139 $("#result_area").append("
WSMessage: Invalid data format
"); 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)。