一、前言
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。这意味着它是一种持久性连接,且服务端可以发消息给客户端。这便容易实现即时通讯通知的功能,本文将介绍 WebSocket 在 SpringBoot 中的用法,以及一个简单的网上聊天的 demo。
二、集成步骤
2.1 引入依赖
<!-- 集成websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2.2 开启WebSocket支持
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
2.3 配置服务端
使用注解 @ServerEndpoint 来定义一个 WebSocket 服务端。
@ServerEndpoint("/test")
@Component
@Slf4j
public class MyWebSocketServer {
/**
* 存放所有在线的客户端userId->session
*/
private static Map<String, Session> clients = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session) {
log.info("onOpen前存活: {}", clients.keySet());
Map<String, List<String>> requestParameterMap = session.getRequestParameterMap();
String userId = requestParameterMap.get("fromId").get(0);
log.info("用户上线了, userId为:{}, sessionId为:{}", userId, session.getId());
//将新用户存入在线的组
clients.put(userId, session);
log.info("onOpen后存活: {}", clients.keySet());
}
/**
* 客户端关闭
* @param session session
*/
@OnClose
public void onClose(Session session) {
log.info("onClose前存活: {}", clients.keySet());
Map<String, List<String>> requestParameterMap = session.getRequestParameterMap();
String userId = requestParameterMap.get("fromId").get(0);
log.info("有用户断开了, userId为:{}, sessionId为:{}", userId, session.getId());
//将掉线的用户移除在线的组里
clients.remove(userId);
log.info("onClose后存活: {}", clients.keySet());
}
/**
* 发生错误
* @param throwable e
*/
@OnError
public void onError(Throwable throwable) {
log.error("onError: {}, 当前存活:{}", throwable.getMessage(), clients.keySet());
}
/**
* 收到客户端发来消息
*/
@OnMessage
public void onMessage(Session session, String dataStr) {
log.info("服务端收到消息体: {}", dataStr);
JSONObject data = JSON.parseObject(dataStr);
// type不为空且等于heartbeat代表心跳消息,必须转发到目标自身以避免客户端断线
String type = data.getString("type");
if (!StringUtils.isEmpty(type) && "heartbeat".equals(type)) {
log.info("心跳消息: {}", dataStr);
session.getAsyncRemote().sendText(dataStr);
} else {
String toId = data.getString("toId");
Session toSession = clients.get(toId);
// 目标对象是否已下线,若ignore此元素,会造成自身session被关闭
if (toSession != null) {
toSession.getAsyncRemote().sendText(dataStr);
} else {
log.warn("对方{}已下线", toId);
data.put("fromId", toId);
data.put("state", "fail");
data.put("message", String.format("<发送失败>: 对方%s已下线", toId));
session.getAsyncRemote().sendText(data.toJSONString());
}
}
}
}
定义了一个成员变量 ConcurrentHashMap,用来保存所有的 WebSocket 会话连接;定义了4个注解的方法,@OnOpen、@OnClose、@OnError、@OnMessage 分别代表有客户端连接到、有客户端关闭、发生错误和服务端接收到消息的处理逻辑。
服务端发送消息通过目标 Session 来操作。服务端的流程大概如此,接下来看看客户端的架构。
2.4 配置客户端
首先,客户端(浏览器)必须要支持 WebSocket 协议;其次,创建 WebSocket 对象,同服务端一样,定义4个阶段对应的函数实现;最后,客户端发送消息,可通过创建的 WebSocket 对象直接 send 即可。
function openSocket() {
if (typeof(WebSocket) == "undefined") {
console.log("您的浏览器不支持WebSocket");
} else {
console.log("您的浏览器支持WebSocket");
//实现化WebSocket对象,指定要连接的服务器地址与端口,建立连接
var curId = $(".curAccount").attr("id");
var socketUrl = window.location.origin.replace("https","ws").replace("http","ws") + "/test?fromId=" + curId;
console.log(socketUrl);
if (socket != null) {
socket.close();
socket = null;
}
socket = new WebSocket(socketUrl);
//打开事件
socket.onopen = function() {
console.log("websocket已打开");
};
//获得消息事件
socket.onmessage = function(event) {
// 发现消息进入 开始处理前端触发逻辑
let dataStr = event.data;
let data = JSON.parse(dataStr);
// TODO 消息处理
};
//关闭事件
socket.onclose = function(event) {
console.log(event);
console.log("websocket已关闭");
};
//发生了错误事件
socket.onerror = function() {
console.log("websocket发生了错误");
}
}
}
openSocket();
function sendMsg() {
if (typeof(WebSocket) == "undefined") {
console.log("您的浏览器不支持WebSocket");
} else {
console.log("您的浏览器支持WebSocket");
let toId = $(".userRow.activeFriend").attr("data-id");
let msg = $(".msgarea").val();
console.log(toId + "->" + msg);
let data = {};
data["fromId"] = $(".curAccount").attr("id");
data["toId"] = toId;
data["message"] = msg;
socket.send(JSON.stringify(data));
}
}
2.5 心跳检测
通常情况下,WebSocket 连接创建后,如果一段时间内没有任何活动,服务器端会对连接进行超时处理。
以下是我在测试过程中的日志情况,可以发现一个连接5分钟没有活动后,会被服务端做超时关闭处理:
[INFO ] 2020-03-11 11:24:34,020 com.szh.wechat.controller.MyWebSocketServer.onOpen(MyWebSocketServer.java:35)
用户上线了, userId为:66666666, sessionId为:5
[INFO ] 2020-03-11 11:29:34,023 com.szh.wechat.controller.MyWebSocketServer.onClose(MyWebSocketServer.java:49)
有用户断开了, userId为:66666666, sessionId为:5
所以,我们需要让客户端每隔一段时间向服务端发送一次心跳活动,以免被服务端关闭,js代码如下:
//心跳检测
var heartCheck = {
timeout: 30000,
timeoutObj: null,
serverTimeoutObj: null,
start: function() {
console.log('start heartCheck');
var self = this;
this.timeoutObj && clearTimeout(this.timeoutObj);
this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
this.timeoutObj = setTimeout(function() {
console.log('heartCheck doing...');
//发送测试信息,后端收到后,返回一个消息,
let data = {};
data["type"] = "heartbeat";
data["fromId"] = currentUser.id;
data["message"] = "心跳检测: " + data["fromId"];
socket.send(JSON.stringify(data));
self.serverTimeoutObj = setTimeout(function() {
socket.close();
}, self.timeout);
}, this.timeout);
}
};
最后,在合适的地方,如onOpen、onMessage处添加 heartCheck.start() 即可续签心跳。
三、项目分享
【github地址】:GitHub - oaHeZgnoS/wechat_web: web简易版微信qq。springboot、html、jQuery、websocket
【相关技术】: SpringBoot、WebSocket、H5、JQuery等。
【项目截图】:如下