【2023】前端加springboot实现Web Socket连接通讯(包括后端实现心跳检测)
- 前言
- 一、Web Socket 简绍
- 1 为什么用 websocket?
- 二、代码实现
- 1、前端(html)
- 1.1、无前端向后端发送消息
- 1.2、有前端向后端发送消息
- 2、后端具体代码(spring boot)
- 2.1、maven依赖
- 2.2、配置类
- 3、Web Socket连接工具类
- 2.3、Controller用于测试主动发送消息
- 2.4、定时任务,用于调用主动向客户端发送心跳
- 三、测试
- 1、 测试消息发送
- 1.1、前端日志
- 1.2、后端日志
- 2、测试客户端异常断开,服务器通过心跳检测自动剔除掉异常对话。
前言
写这个项目主要是有有个项目需要后端有数据实话返回前端,一开始采用前端轮询的方式,后面觉得及时性上有些不行,然后改为使用websocket ,具体实现demo以及测试流程发出来提供交流学习,
一、Web Socket 简绍
WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
1 为什么用 websocket?
换句话说,websocket 解决了什么问题?答案是,解决了两个主要问题:
- 只能客户端发送请求
- 一段时间内的频繁信息发送
假设现在需要设计一个实时预警系统的通知模块,那么作为工程师我们应该怎么设计通知的这个功能呢?因为这些系统的数据来源,一般他通过硬件设备采集到后台的,如果我们现在只有 http 协议,那么我们只能让客户端不断地轮询服务器,轮询的时间间隔越小越能接近实时的效果。可是,轮询的效率低,又浪费资源。针对这样的场景,websocket 应运而生。
特点:
(1)建立在 TCP 协议之上,服务器端的实现比较容易,是一个可靠的传输协议。
(2)与 HTTP协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据。
(5)没有同源限制,客户端可以与任意服务器通信。
(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
二、代码实现
1、前端(html)
1.1、无前端向后端发送消息
uid实际开发中应该使用唯一值作为当前对话的key
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
消息展示区:<br/>
<div id="textArea"></div>
</body>
<script>
var textArea = document.getElementById('textArea');
var websocket = null;
//如果浏览器支持websocket就建立一个websocket,否则提示浏览器不支持websocket
//uid应该要用唯一标识,为了测试方便看
if('websocket' in window){
websocketPage = new WebSocket('ws://localhost:8080/websocket/' + 99);
}else{
alert('浏览器不支持websocket!');
}
//建立websocket时自动调用
websocketPage.onopen = function (event) {
console.log('建立连接');
}
//关闭webscoket时自动调用
websocketPage.oncolse = function (event){
console.log('关闭连接');
}
//websocket接收到消息时调用
websocketPage.onmessage = function (event){
//将接收到的消息展示在消息展示区 (心跳响应回来的消息不显示)
if (event.data !== "conn_success"){
textArea.innerText += event.data;
textArea.innerHTML += "<br/>";
}
}
//websocket出错自动调用
websocketPage.onerror = function () {
alert('websocket出错');
}
//关闭窗口前关闭websocket连接
window.onbeforeunload = function (){
websocketPage.close();
}
</script>
</html>
1.2、有前端向后端发送消息
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Java后端WebSocket的Tomcat实现</title>
<script type="text/javascript" src="js/jquery.min.js"></script>
</head>
<body>
Welcome<br/><input id="text" type="text" />
<button onclick="send()">发送消息</button>
<hr/>
<button onclick="closeWebSocket()">关闭WebSocket连接</button>
<hr/>
<div id="message"></div>
</body>
<script type="text/javascript">
var websocket = null;
//判断当前浏览器是否支持WebSocket
if('WebSocket' in window) {
//改成你的地址
websocket = new WebSocket("ws://localhost:8080/websocket/100");
} else {
alert('当前浏览器 Not support websocket')
}
//连接发生错误的回调方法
websocket.onerror = function() {
setMessageInnerHTML("WebSocket连接发生错误");
};
//连接成功建立的回调方法
websocket.onopen = function() {
setMessageInnerHTML("WebSocket连接成功");
}
var U01data, Uidata, Usdata
//接收到消息的回调方法
websocket.onmessage = function(event) {
console.log(event);
if (event.data !== "conn_success"){
setMessageInnerHTML("接收消息:"+event.data);
// setMessageInnerHTML(event);
setechart()
}
}
//连接关闭的回调方法
websocket.onclose = function() {
setMessageInnerHTML("WebSocket连接关闭");
}
// //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function() {
closeWebSocket();
}
//将消息显示在网页上
function setMessageInnerHTML(innerHTML) {
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
//关闭WebSocket连接
function closeWebSocket() {
websocket.close();
}
//发送消息
function send() {
var message = document.getElementById('text').value;
websocket.send('{"msg":"' + message + '"}');
setMessageInnerHTML("--------------发送消息:"+message + "");
}
</script>
</html>
2、后端具体代码(spring boot)
2.1、maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
– yml没有东西,只有一个默认端口
2.2、配置类
需要加一个 WebSocket 端点暴露 的bean 和定时器注解
@EnableScheduling //定时器
@SpringBootApplication
public class WebSocketApplication {
public static void main(String[] args) {
SpringApplication.run(WebSocketApplication.class, args);
}
/**
* 服务器端点导出
* @author zhengfuping
* @date 2023/8/22
* @return ServerEndpointExporter
*/
@Bean
public ServerEndpointExporter getServerEndpointExporter(){
return new ServerEndpointExporter();
}
}
3、Web Socket连接工具类
@Slf4j
@Service
@ServerEndpoint("/websocket/{uid}")
public class WebSocketServer2 {
//连接建立时长
private static final long sessionTimeout = 60000;
// 用来存放每个客户端对应的WebSocketServer对象
private static Map<String, WebSocketServer2> webSocketMap = new ConcurrentHashMap<>();
// 与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
// 接收id
private String uid;
/**
* 连接建立成功调用的方法
* @author zhengfuping
* @date 2023/8/22
* @param session
* @param uid
*/
@OnOpen
public void onOpen(Session session , @PathParam("uid") String uid){
session.setMaxIdleTimeout(sessionTimeout);
this.session = session;
this.uid = uid;
if (webSocketMap.containsKey(uid)){
webSocketMap.remove(uid);
}
webSocketMap.put(uid,this);
log.info("websocket连接成功编号uid: " + uid + ",当前在线数: " + getOnlineClients());
try{
// 响应客户端实际业务数据!
sendMessage("conn_success");
}catch (Exception e){
log.error("websocket发送连接成功错误编号uid: " + uid + ",网络异常!!!");
}
}
/**
* 连接关闭调用的方法
* @author zhengfuping
* @date 2023/8/22
*/
@OnClose
public void onClose(){
try {
if (webSocketMap.containsKey(uid)){
webSocketMap.remove(uid);
}
log.info("websocket退出编号uid: " + uid + ",当前在线数为: " + getOnlineClients());
} catch (Exception e) {
log.error("websocket编号uid连接关闭错误: " + uid + ",原因: " + e.getMessage());
}
}
/**
* 收到客户端消息后调用的方法
* @param message 客户端发送过来的消息
* @param session
*/
@OnMessage
public void onMessage(String message, Session session) {
try {
WebSocketServer2.sendInfo(message);
log.info("websocket收到客户端编号uid消息: " + uid + ", 报文: " + message);
} catch (Exception e) {
log.error("websocket发送消息失败编号uid为: " + uid + ",报文: " + message);
}
}
/**
* 发生错误时调用
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("websocket编号uid错误: " + this.uid + "原因: " + error.getMessage());
error.printStackTrace();
}
/**
* 实现服务器主动推送
* @author yingfeng
* @date 2023/8/22 10:11
* @Param * @param null
* @return
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
/**
* 获取客户端在线数
* @author zhengfuping
* @date 2023/8/22 10:11
* @param
*/
public static synchronized int getOnlineClients() {
if (Objects.isNull(webSocketMap)) {
return 0;
} else {
return webSocketMap.size();
}
}
/**
* 单机使用,外部接口通过指定的客户id向该客户推送消息
* @param key
* @param message
* @return boolean
*/
public static boolean sendMessageByWayBillId(@NotNull String key, String message) {
WebSocketServer2 webSocketServer = webSocketMap.get(key);
if (Objects.nonNull(webSocketServer)) {
try {
webSocketServer.sendMessage(message);
log.info("websocket发送消息编号uid为: " + key + "发送消息: " + message);
return true;
} catch (Exception e) {
log.error("websocket发送消息失败编号uid为: " + key + "消息: " + message);
return false;
}
} else {
log.error("websocket未连接编号uid号为: " + key + "消息: " + message);
return false;
}
}
/**
* 群发自定义消息
* @author zhengfuping
* @date 2023/8/22 9:52
* @param message
*/
public static void sendInfo(String message) {
webSocketMap.forEach((k, v) -> {
WebSocketServer2 webSocketServer = webSocketMap.get(k);
try {
webSocketServer.sendMessage(message);
log.info("websocket群发消息编号uid为: " + k + ",消息: " + message);
} catch (IOException e) {
log.error("群发自定义消息失败: " + k + ",message: " + message);
}
});
}
/**
* 服务端群发消息-心跳包
* @author zhengfuping
* @date 2023/8/22 10:09
* @param message 推送数据
* @return int 连接数
*/
public static synchronized int sendPing(String message){
if (webSocketMap.size() == 0)
return 0;
StringBuffer uids = new StringBuffer();
AtomicInteger count = new AtomicInteger();
webSocketMap.forEach((uid,server)->{
count.getAndIncrement();
if (webSocketMap.containsKey(uid)){
WebSocketServer2 webSocketServer = webSocketMap.get(uid);
try {
if (Integer.valueOf(uid) ==101){
Integer i=1/0;
}
webSocketServer.sendMessage(message);
if (count.equals(webSocketMap.size() - 1)){
uids.append("uid");
return;
}
uids.append(uid).append(",");
} catch (Exception e) {
webSocketMap.remove(uid);
log.info("客户端心跳检测异常移除: " + uid + ",心跳发送失败,已移除!");
}
}else {
log.info("客户端心跳检测异常不存在: " + uid + ",不存在!");
}
});
log.info("客户端心跳检测结果: " + uids + "连接正在运行");
return webSocketMap.size();
}
/**
* 连接是否存在
* @param uid
* @return boolean
*/
public static boolean isConnected(String uid) {
if (Objects.nonNull(webSocketMap) && webSocketMap.containsKey(uid)) {
return true;
} else {
return false;
}
}
}
2.3、Controller用于测试主动发送消息
@RestController
@RequestMapping("/test")
public class WebSocketController{
/**
* 检验连接
* @date 2023/8/22
* @Param * @param webSocketId
* @return * @return String
*/
@GetMapping("/webSocketIsConnect/{webSocketId}")
public String webSocketIsConnect(@PathVariable("webSocketId") String webSocketId){
if (WebSocketServer2.isConnected(webSocketId)) {
return webSocketId+"正在连接";
}
return webSocketId+"连接断开!";
}
/**
* 单发 消息
* @author zhengfuping
* @date 2023/8/22 10:25
* @param webSocketId 指定 连接
* @param message 数据
* @param pwd 验证密码
* @return String
*/
@GetMapping("/sendMessageByWayBillId")
public String sendMessageByWayBillId(String webSocketId, String message, String pwd) {
boolean flag = false;
flag = WebSocketServer2.sendMessageByWayBillId(webSocketId, message);
if (flag) {
return "发送成功!";
}
return "发送失败!";
}
/**
* 群发
* @author zhengfuping
* @date 2023/8/22 10:26
* @param message
* @param pwd
*/
@GetMapping("/broadSendInfo")
public void sendInfo(String message, String pwd) {
WebSocketServer2.sendInfo(message);
}
}
2.4、定时任务,用于调用主动向客户端发送心跳
每10秒调用一次,主动检测,查看客户端连接是否异常断开,如果异常断开,则把该会话从集合中剔除掉,避免无限积压。
@Component
@Slf4j
public class WebSocketTask {
@Scheduled(cron = "0/10 * * * * ?")
public void clearOrders(){
int num = 0;
try {
num = WebSocketServer2.sendPing("conn_success");
} finally {
log.info("websocket心跳检测结果,共【" + num + "】个连接");
}
}
}
三、测试
1、 测试消息发送
1.1、前端日志
1.2、后端日志
2、测试客户端异常断开,服务器通过心跳检测自动剔除掉异常对话。
因为测试不方便,只能通过断点实现效果
-
前端需要把主动关闭会话的注释掉,不让主动关闭
-
先在连接关闭的地方和心跳检测地方打上断点,断点需要设置成Thread,要不然没法异步
-
然后关闭掉一个前端页面让他把会话关闭,就会进入该断点位置,通过断点让它停住不让他去正常关闭
4. 然后选择执行该心跳检测的断点代码
5. 进入心跳的循环给每个会话发送心跳检测,此时前端已经异常断开了
6. 因为前端已经关闭会话了,则发送心跳会失败,会直接进入catch块,然后把该会话从集合中剔除掉
最终日志