一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第18天,点击查看活动详情。
前言
一般在java开发中如果需要使用长连接,比较成熟的方案是加入netty框架,来实现这个连接功能。但是对于一般的小项目,可能只有一两个点需要长连接,对性能有没有较高的要求,那么可以使用最原始的websocket,用原生的方式来实现这个功能。在spring中为我们提供了一个websocket的实现方法。
环境搭建
首先我们需要引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
这个是websocket所需要的依赖。 然后我们需要创建一个配置类
@Configuration
public class WebSocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
这个是为了暴露一个websocket的服务器端点。
@Slf4j
@Component
@ServerEndpoint(value = "/websocket/{userId}")
public class SelfWebSocketServer
这里我们创建一个组件用于处理我们的websocket业务。@ServerEndpoint注解中声明了url参数用于暴露提供给前端的websocket地址。相当把当前的这个类定义为一个websocket服务器,客户端可以通过ws协议和它进行连接。
服务端业务处理
@Slf4j
@Component
@ServerEndpoint(value = "/websocket/{userId}")
public class SelfWebSocketServer {
private static int onlineCount = 0;
private static Map<String, Session> sessionMap = new ConcurrentHashMap<>();
private Session session;
private Long userId;
private static final String PING = "ping";
private static final String PONG = "pong";
public static synchronized Integer getOnlineCount(){
return SelfWebSocketServer.onlineCount;
}
private static synchronized void addOnlineCount(){
SelfWebSocketServer.onlineCount++;
}
private static synchronized void subOnlineCount(){
SelfWebSocketServer.onlineCount--;
}
@OnOpen
public void onOpen(Session session,@PathParam("userId") Long userId){
this.session = session;
this.userId = userId;
String user = String.valueOf(userId);
if (sessionMap.containsKey(user)){
sessionMap.put(user,session);
}else {
sessionMap.put(user,session);
addOnlineCount();
}
log.info("socket add userId:[{}],online:[{}]",userId,onlineCount);
//sendUserMessage("success", Collections.singletonList(userId));
}
@OnClose
public void onClose(@PathParam("userId") Long userId){
String user = String.valueOf(userId);
if(sessionMap.containsKey(user)) {
sessionMap.remove(user);
subOnlineCount();
log.info("socket remove userId:[{}],online:[{}]",user,onlineCount);
}
}
@OnMessage
public void onMessage(String message,@PathParam("userId")Long userId){
String user = String.valueOf(userId);
boolean containsKey = sessionMap.containsKey(user);
if (containsKey){
sendUserMessage(message.equals(PING)?PONG:message, Collections.singletonList(userId));
}
}
public void sendUserMessage(String message, List<Long> userIdList){
for (Long userId : userIdList) {
String user = String.valueOf(userId);
if (null != user && sessionMap.containsKey(user)){
Session session = sessionMap.get(user);
this.sendMessage(message,session);
log.info("send websocket message,userId[{}],message[{}]",user,message);
}else {
log.warn("userId or websocket session null,user:[{}]",user);
}
}
}
public void sendMessage(String message, Session session){
session.getAsyncRemote().sendText(message);
}
@OnError
public void onError(Session session,Throwable throwable){
log.error("websocket error, userId {[]},error,{[]}",this.userId,throwable.getMessage());
throwable.printStackTrace();
}
首先我们看到有四个注解分别是@OnOpen、@OnClose、@OnMessage、@OnError。 第一个OnOpen是用于websocket连接建立时候触发的,所有接入的连接我们都是用session来进行管理。然后我们用户每个用户的唯一标识id来和session绑定,那我们就可以通过这个id从存储中获取到session。
我们在这个建立连接的方法中添加这样的逻辑。使用一个map来管理会话,使用一个静态变量来统计在线人数。当会话中没有包含当前连接用户时,保存session,同时增加在线人数。如果会话中已经存在,则更新会话。
第二个OnClose是连接关闭时触发的,当我们的会话中包含这个用户时,移除它的session,同时减少在线人数。
第三个OnMessage是我们服务端和客户端通信的接口,主要是接受到客户端的消息,并且做出应答。因为如果服务端和客户端长时间没有数据交互,连接会关闭,因为一方会以为另一方已经挂掉了,所以断开连接。那么比较通用的做法是保持心跳连接,即在没有业务数据通信时,传输一些简单的标志保持心跳,也叫保活。 我这边定义是如果客户端发送了“ping”那么我就回复“pong”,如果是别的数据则按具体的数据做处理。
第四个是OnError,这个是连接异常的时候触发的,我们这里简单的打印错误信息即可。
客户端代码
这里还需要我们的客户端代码才可以完成一整个链路的调试。我们的客户端代码主要实现
- 能够和服务端连接通讯
- 能够做到心跳保持
- 能够做的断线重连
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>websocket</title>
</head>
<body>
<div>
<input id="userId" name="userId" type="text" placeholder="userId" />
</div>
<div>
<button onclick="openSocket()">开启socket</button>
</div>
<div>
<input id="content" />
<button onclick="sendMsg()">发送消息</button>
</div>
</body>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<script>
var socket;
const socketUrl = "ws://127.0.0.1:8082/web/websocket";
var heartBeatTime = 10;
var heartCheck = {
timeout: heartBeatTime * 1000,
timeoutObj: null,
serverTimeoutObj: null,
reset: function () {
clearTimeout(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
return this;
},
start: function () {
var self = this;
this.timeoutObj = setTimeout(function () {
socket.send("ping");
self.serverTimeoutObj = setTimeout(function () {
socket.close();
}, this.timeout);
}, this.timeout);
},
};
function openSocket() {
if (typeof WebSocket == "undefined") {
console.log("不支持websocket");
} else {
console.log("支持websocket");
var userId = document.getElementById("userId").value;
var webSocketUrl = socketUrl + "/" + userId;
console.log(socketUrl);
socket = new WebSocket(webSocketUrl);
initEventHandle();
}
}
function sendMsg() {
if (typeof WebSocket == "undefined") {
console.log("不支持websocket");
} else {
console.log("支持websocket");
var userId = document.getElementById("userId").value;
var content = document.getElementById("content").value;
var webSocketUrl = socketUrl + "/" + userId;
console.log(socketUrl);
socket.send(content);
}
}
function initEventHandle() {
socket.onclose = function () {
console.log("websocket已断开");
reconnect(socketUrl);
console.log("websocket断线重连");
};
socket.onerror = function (err) {
console.log(
"websocket发生异常:" +
err.code +
" " +
err.reason +
" " +
err.wasClean
);
reconnect(socketUrl);
console.log("websocket断线重连");
};
socket.onopen = function () {
socket.send("ping");
heartCheck.reset().start();
};
socket.onmessage = function (msg) {
console.log("接收到信息:" + msg);
heartCheck.reset().start();
};
}
function reconnect(socketUrl) {
if (reconnect.lockReconnect) return;
reconnect.lockReconnect = true;
setTimeout(function () {
openSocket();
reconnect.lockReconnect = false;
}, 3000);
}
</script>
</html>
简单的介绍一下上面的代码。定义了心跳时间时10s。 首先用户输入id,客户端建立一个ws协议的连接,初始化连接的监听。分别对监听事件做出处理。 比如这里监听了默认的几个事件。从上往下是:
关闭事件,当客户端监听到连接关闭,就开始执行断线重连的方法。只要连接没有成功,就每隔3s尝试建立一次连接。
异常事件,如果连接通信中抛出了异常,那么也调用断线重连的方法。
打开事件和服务端建立了连接,首先向服务端发送一条消息,然后开始心跳机制。首先重置setTimeout方法的计时时间,然后每隔3s发送一个心跳包。这里是因为setTimeout只能执行一次,执行完之后定时器的计时已经达到了约定的时间,如果要重复调用方法需要使用clearTimeout方法来重置定时器,或者不使用setTimeout改用setInterval方法
消息事件,接受到服务端的消息,也开始心跳发送心跳包。
可以看到建立连接之后发送了心跳包。
2022-04-20 23:46:42.598 INFO 12152 --- [nio-8082-exec-7] c.z.s.websocket.SelfWebSocketServer : send websocket message,userId[1],message[pong]
服务端接受了心跳包。
2022-04-20 23:47:24.111 INFO 12152 --- [io-8082-exec-10] c.z.s.websocket.SelfWebSocketServer : send websocket message,userId[1],message[hello]
发送消息,服务端可以接受到客户端的消息
断线后进行了重连
以上就是spring boot websocket简单又快速的实现了,非常适合小型项目的开发。