目录
前言
近日心血来潮想做一个开源项目,目标是做一款可以适配多端、功能完备的模板工程,包含后台管理系统和前台系统,开发者基于此项目进行裁剪和扩展来完成自己的功能开发。
本项目为前后端分离开发,后端基于Java21
和SpringBoot3
开发,前端提供了vue、angular、react、uniapp、微信小程序等多种脚手架工程。
本文主要介绍项目中如何集成WebSocket实现服务器端与客户端的双向通信。
相关技术简介
什么是WebSocket
WebSocket 是一种网络通信协议。RFC6455 定义了它的通信标准。
http是一种无状态,无连接,单向的应用层协议,它采用了请求/响应模型,通信请求只能由客户端发起,服务端对请求做出应答处理。这样的弊端显然是很大的,只要服务端状态连续变化,客户端就必须实时响应,都是通过javascript与ajax进行轮询,这样显然是非常麻烦的,同时轮询的效率低,非常的浪费资源(http一直打开,一直重复的连接)。
于是就有了WebSocket,它是一种全面双工通讯的网络技术,任意一方都可以建立连接将数据推向另一方,WebSocket只需要建立一次连接,就可以一直保持。
WebSocket的原理
- WebSocket约定了一个通信的规范,通过一个握手的机制,客户端和服务器之间能建立一个类似tcp的连接,从而方便它们之间的通信
- 在WebSocket出现之前,web交互一般是基于http协议的短连接或者长连接
- WebSocket是一种全新的协议,不属于http无状态协议,协议名为"ws"
WebSocket与HTTP协议的关系
WebSocket优点
- 减少请求费时费资源:是真正的全双工方式,建立连接后,服务器与客户端时完全对等的,可以相互请求,减少了不必要的网络请求时间损耗和网络流量;
- 更持久:WebSocket协议通过第一个request建立TCP连接后,只要不主动关闭,就能一直保持连接状态交换数据;
- 服务端可以主动向客户端发送消息;
WebSocket应用场景
社交聊天、弹幕、多玩家游戏、协同编辑、股票基金实时报价、体育实况更新、视频会议/聊天、基于位置的应用、在线教育、智能家居等需要高实时的场景都可以使用WebSocket技术实现。
实现方式
本项目后端基于Java 21
和SpringBoot3
开发,前端基于Vue3实现。
添加maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>3.2.0</version>
</dependency>
添加WebSocket配置类,定义ServerEndpointExporter Bean
@Configuration
@EnableWebSocket
public class WebSocketConfig {
/**
* 注入ServerEndpointExporter,
* 这个bean会自动注册使用了@ServerEndpoint注解声明的WebSocket Endpoint
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
定义WebSocket Endpoint
/**
* 消息提醒计数WebSocket
*/
@ServerEndpoint("/ws/test/{userId}")
@Component
@Slf4j
public class TestWebSocketServer {
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;
/**
* 用户ID
*/
private Long userId;
/**
* concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
* 虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。
*/
private static final CopyOnWriteArraySet<MessageCountWebSocketServer> webSockets = new CopyOnWriteArraySet<>();
/**
* 用来存在线连接用户信息
*/
private static final ConcurrentHashMap<Long, Session> sessionPool = new ConcurrentHashMap<>();
/**
* 链接成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam(value = "userId") Long userId) {
this.session = session;
this.userId = userId;
webSockets.add(this);
sessionPool.put(userId, session);
log.info("建立与UserID:{}的消息提醒计数连接", userId);
}
/**
* 链接关闭调用的方法
*/
@OnClose
public void onClose() {
webSockets.remove(this);
sessionPool.remove(this.userId);
log.info("关闭与UserID:{}的消息提醒计数连接", userId);
}
/**
* 收到客户端消息后调用的方法
*/
@OnMessage
public void onMessage(String message) {
log.info("接收到UserID:{}的消息{}", userId, message);
}
/**
* 发送错误时的处理
*/
@OnError
public void one rror(Session session, Throwable error) {
log.error("发送到UserID:{}的消息传输失败", userId, error);
}
/**
* 广播消息
*
* @param message
*/
public void sendAllMessage(String message) {
for (MessageCountWebSocketServer socketServer : webSockets) {
if (socketServer.session.isOpen()) {
socketServer.session.getAsyncRemote().sendText(message);
}
}
}
/**
* 单人单播消息
*
* @param userId
* @param message
*/
public void sendOneMessage(Long userId, String message) {
Session session = sessionPool.get(userId);
if (session != null && session.isOpen()) {
session.getAsyncRemote().sendText(message);
}
}
/**
* 多人单播消息
*
* @param userIds
* @param message
*/
public void sendMoreMessage(Long[] userIds, String message) {
for (Long userId : userIds) {
Session session = sessionPool.get(userId);
if (session != null && session.isOpen()) {
session.getAsyncRemote().sendText(message);
}
}
}
}
前端创建WebSocket对象
以下代码基于Vue3的组合式API编写。
<script setup>
import { onMounted, onBeforeMount } from 'vue';
/**
* @type {WebSocket}
*/
let websocket = null;
onMounted(async () => {
initTestWebSocket();
});
onBeforeMount(async()=>{
websocket && websocket.close();
});
const initTestWebSocket = async () => {
const userId = '当前用户ID';
console.log("尝试建立websockect连接");
websocket = new WebSocket(`/ws/test/${userId}`);
websocket.onopen = function (event) {
console.log("建立连接");
}
websocket.onclose = function (event) {
console.log('连接关闭')
//尝试重连websocket
reconnectMessageWebSocket();
}
//建立通信后,监听到后端的数据传递
websocket.onmessage = function (event) {
// 打印后端传来的数据
console.log(event.data);
// 调用WebSocket对象的send方法可向后端发送数据
// websocket.send("test data");
}
websocket.onerror = function () {
console.log("数据发送失败");
}
// 窗口关闭前关闭WebSocket连接
window.onbeforeunload = function () {
websocket.close();
}
};
// 重连
const reconnectMessageWebSocket = () => {
console.log("正在重连");
// 进行重连
setTimeout(() => {
initTestWebSocket();
}, 1000);
}
</script>
总结
本文介绍了WebSocket的相关概念,以及如何基于Java21、SpringBoot3和Vue3使用WebSocket,在使用过程中也遇到了一些问题。
- 执行
mvn package
或mvn test
命令时报错
请参阅 Java21 + SpringBoot3使用spring-websocket时执行mvn package报错。
- 如果后端使用
Spring Security
、Shiro
或Sa-Token
等技术,需要考虑使用@ServerEndpoint
注解所配置url的权限控制问题。
我也会及时的更新后续实践中所遇到的问题,希望与诸位看官一起进步。
如有错误,还望批评指正。