前言

现在是距11月08日的第四周,这一个月来,我最大的成果就是这个WebSocket服务器,实现过程艰难坎坷,也算是人生中第一个上手的项目


学习计划

从菜鸟教程学习知识基础,覆盖一遍知识框架并建立索引

★ HTML + H5、CSS + CSS3
★ JavaScript、Vue、uni-app
★ Java、PHP、Python

★ JSON、Markdown、TCP/IP、HTTP
★ MySQL、Redis


学习进度

★ HTML
★ CSS

★ JSON
★ Markdown
★ TCP/IP
★ HTTP


学习历程

Java WebSocket服务器,使用STOMP协议,基于Spring boot框架

项目代码主要参考超级小豆丁
Java知识点主要参考菜鸟教程
Spring boot集成WebSocket的疑惑主要参考腾讯云社区

客户端与服务端建立WebSocket连接-服务端查询数据库并返回数据-客户端解析数据并显示

由于没有系统学习过Java(学校内容等于0),且不使用指导老师给的Java服务器,实现这个小项目艰难坎坷
好处是认识了Spring boot框架,知道了Maven,虽差距遥远,但完善性一步一步向老师的服务器靠齐

素材:人体穴位psd图,人体穴位信息Excel表
要求:将Excel表导入MySQL数据库,uni-app开发前端,基于老师的Java服务器开发后端
流程:客户端输入病症,服务器得到数据并查询数据库,客户端收到消息并高亮穴位

此篇文章仅作为笔记,代码并不一定规范,主要是为了实现功能
实际项目已经完成前后端对接,后端代码同步到github
后端之后会加入鉴权,以及重构部分类,使服务器有更强的通用性


项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
src>main>java>top.onektas.stompserver

> config
>> WebSocketConfig.java

> controller
>> MessageController.java
>> MysqlController.java

> model
>> MessageBody

> tools
>> ResultSetTranslator
1
2
3
4
5
src>main>java>resourses

> static
>> app-websocket.js
>> index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!-- SpringBoot WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>

<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.json/json -->
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20210307</version>
</dependency>

项目源码

项目地址Onektas的github

WebSocket配置,通过注解将类标记为一个WebSocket

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package top.onektas.stompserver.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

/**
* WebSocket 配置类
*
* @onektas
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

/**
* 配置 WebSocket 进入点,及开启使用 SockJS,这些配置主要用配置连接端点,用于 WebSocket 连接
*
* @param registry STOMP 端点
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 网页端连接端点,启用跨域,启用http/https
registry.addEndpoint("/onesocket").setAllowedOriginPatterns("*").withSockJS();
// uni-app端连接端点,启用跨域,启用ws/wss
registry.addEndpoint("/onesocket-app").setAllowedOriginPatterns("*");
}

/**
* 配置消息代理选项
*
* @param registry 消息代理注册配置
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 设置一个或者多个代理前缀,在 Controller 类中的方法里面发生的消息,会首先转发到代理从而发送到对应广播或者队列中。
registry.enableSimpleBroker("/topic");
// 配置客户端发送请求消息的一个或多个前缀,该前缀会筛选消息目标转发到 Controller 类中注解对应的方法里
registry.setApplicationDestinationPrefixes("/app");
}

}

接收客户端消息并广播到订阅URL,可在广播前处理消息,可指定发送对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package top.onektas.stompserver.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.stereotype.Controller;
import top.onektas.stompserver.model.MessageBody;

/**
* 消息 Controller 类
*
* @onektas
*/
@Controller
public class MessageController {

/**
* 消息发送工具对象
*/
@Autowired
private SimpMessageSendingOperations simpMessageSendingOperations;

/**
* 接受客户端消息
* <p>
* 广播发送消息,将消息发送到指定的目标地址
*/
@MessageMapping("/test")
public void sendTopicMessage(MessageBody messageBody) {
// 接受客户端消息并传入MysqlController类进行处理
String result = new MysqlController().mysqlConntroller(messageBody.getContent());
System.out.println(result);
messageBody.setContent(result);

// 将消息发送到 WebSocket 配置类中配置的代理中(/topic)进行消息转发
simpMessageSendingOperations.convertAndSend(messageBody.getDestination(), messageBody);
}

}

操作数据库,执行SQL语句并返回结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package top.onektas.stompserver.controller;

import top.onektas.stompserver.tools.ResultSetTranslator;

import java.sql.*;

/**
* Mysql查询类
*
* @onektas
*/
public class MysqlController {

// MySQL 8.0 以下版本 - JDBC 驱动名及数据库 URL
static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
static final String DB_URL = "jdbc:mysql://localhost:3306/work";

// MySQL 8.0 以上版本 - JDBC 驱动名及数据库 URL
//static final String JDBC_DRIVER = "com.mysql.cj.jdbc.Driver";
//static final String DB_URL = "jdbc:mysql://localhost:3306/work?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC";

// 数据库的用户名与密码,需要根据自己的设置
static final String USER = "root";
static final String PASS = "1234";

public String mysqlConntroller(String zhengzhuang) {
Connection conn = null;
Statement stmt = null;
String result = null;

try {
// 注册 JDBC 驱动
Class.forName(JDBC_DRIVER);

// 打开链接
System.out.println("连接数据库...");
conn = DriverManager.getConnection(DB_URL, USER, PASS);

// 执行查询
System.out.println("查询数据库数据...");
stmt = conn.createStatement();
String sql;
sql = "SELECT buwei,xuewei,bingzheng FROM xuewei WHERE bingzheng LIKE '%" + zhengzhuang + "%'";
ResultSet rs = stmt.executeQuery(sql);

// 将查询结果转换为String
result = new ResultSetTranslator().resultSetToJson(rs);

// 完成后关闭
rs.close();
stmt.close();
conn.close();
} catch (SQLException se) {
// 处理 JDBC 错误
se.printStackTrace();
} catch (Exception e) {
// 处理 Class.forName 错误
e.printStackTrace();
} finally {
// 关闭资源
try {
if (stmt != null) stmt.close();
} catch (SQLException se2) {
}// 什么都不做
try {
if (conn != null) conn.close();
} catch (SQLException se) {
se.printStackTrace();
}
}
return result;
}
}

格式化数据,STOMP发送的数据类型必须为String,格式一般为JSON
客户端收到的消息并不是String,一般先转成JSON,JSON可以存放多种数据类型,根据需要进行二次转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package top.onektas.stompserver.model;

import lombok.Data;

/**
* 消息实体类
*
* @onektas
*/
@Data
public class MessageBody {
/* 消息内容 */
private String content;
/* 广播转发的目标地址(告知 STOMP 代理转发到哪个地方) */
private String destination;
}

将MysqlController类得到的Result数据转换成JSON,但返回String类型,以便STOMP发送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package top.onektas.stompserver.tools;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;

/**
* ResultSet值转换String
*
* @onektas
*/
public class ResultSetTranslator {

public String resultSetToJson(ResultSet rs) throws SQLException, JSONException {
// json数组
JSONArray array = new JSONArray();
// 获取列数
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();

// 遍历ResultSet中的每条数据
while (rs.next()) {
JSONObject jsonObj = new JSONObject();

// 遍历每一列
for (int i = 1; i <= columnCount; i++) {
String columnName = metaData.getColumnLabel(i);
String value = rs.getString(columnName);
jsonObj.put(columnName, value);
}
array.put(jsonObj);
}
return array.toString();
}
}

静态文件

客户端示例

客户端连接WebSocket,并解析收到的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// 设置 STOMP 客户端
var stompClient = null;
// 设置 WebSocket 进入端点
var SOCKET_ENDPOINT = "/onesocket";
// 设置订阅消息的请求前缀
var SUBSCRIBE_PREFIX = "/topic"
// 设置订阅消息的请求地址
var SUBSCRIBE = "";
// 设置服务器端点,访问服务器中哪个接口
var SEND_ENDPOINT = "/app/test";

/* 进行连接 */
function connect() {
// 设置 SOCKET
var socket = new SockJS(SOCKET_ENDPOINT);
// 配置 STOMP 客户端
stompClient = Stomp.over(socket);
// STOMP 客户端连接
stompClient.connect({}, function (frame) {
alert("连接成功");
});
}

/* 订阅信息 */
function subscribeSocket() {
// 设置订阅地址
SUBSCRIBE = SUBSCRIBE_PREFIX + $("#subscribe").val();
// 输出订阅地址
alert("设置订阅地址为:" + SUBSCRIBE);
// 执行订阅消息
stompClient.subscribe(SUBSCRIBE, function (responseBody) {
// 转换消息为JSON
var receiveMessage = JSON.parse(responseBody.body);
console.log(receiveMessage);

// 转换消息主体为JSON(content的内容为JSON格式的String类型,所以需要二次转换)
var Message = receiveMessage.content;
Message = JSON.parse(Message);
console.log(Message);

// 输出内容到表格
var i = "";
for (i in Message) {
$("#information").append("<tr><td>" + Message[i].xuewei + "</td></tr>");
}
;
});
}

/* 断开连接 */
function disconnect() {
stompClient.disconnect(function () {
alert("断开连接");
});
}

/* 发送消息并指定目标地址(这里设置的目标地址为自身订阅消息的地址,当然也可以设置为其它地址) */
function sendMessageNoParameter() {
// 设置发送的内容
var sendContent = $("#content").val();
// 设置待发送的消息内容
var message = '{"destination": "' + SUBSCRIBE + '", "content": "' + sendContent + '"}';
// 发送消息
stompClient.send(SEND_ENDPOINT, {}, message);
}

前台页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<!DOCTYPE html>
<html>
<head>
<title>Hello WebSocket</title>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/sockjs-client/1.4.0/sockjs.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script src="app-websocket.js"></script>
</head>
<body>
<div id="main-content" class="container" style="margin-top: 10px;">
<div class="row">
<form class="navbar-form" style="margin-left:0px">
<div class="col-md-12">
<div class="form-group">
<label>WebSocket 连接:</label>
<button class="btn btn-primary" type="button" onclick="connect();">进行连接</button>
<button class="btn btn-danger" type="button" onclick="disconnect();">断开连接</button>
</div>
<label>订阅地址:</label>
<div class="form-group">
<input type="text" id="subscribe" class="form-control" placeholder="订阅地址">
</div>
<button class="btn btn-warning" onclick="subscribeSocket();" type="button">订阅</button>
</div>
</form>
</div>
</br>
<div class="row">
<div class="form-group">
<label for="content">发送的消息内容:</label>
<input type="text" id="content" class="form-control" placeholder="消息内容">
</div>
<button class="btn btn-info" onclick="sendMessageNoParameter();" type="button">发送</button>
</div>
</br>
<div class="row">
<div class="col-md-12">
<h5 class="page-header" style="font-weight:bold">接收到的消息:</h5>
<table class="table table-striped">
<tbody id="information"></tbody>
</table>
</div>
</div>
</div>
</body>
</html>