SpringBoot+Websocket 打造实时聊天、实时监听JVM负载
WebSocket
介绍
WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端
使用场景
- 弹幕
- 网页聊天系统
- 实时监控
- 股票行情推送
单播
点对点,私信私聊方式
广播
游戏公告,发布订阅
多播(也叫组播)
多人聊天,发布订阅
socketjs
介绍
- 是一个浏览器JavaScript库,提供了一个类似WebSocket的对象。
- 提供了一个连贯的跨浏览器的JavaScriptAPI,在浏览器和Web服务器之间创建了一个低延迟,全双工,跨域的通信通道
- 在底层SockJS首先尝试使用本地WebSocket。如果失败了,它可以使用各种浏览器特定的传输协议,并通过类似WebSocket的抽象方式呈现它们
- SockJS旨在适用于所有现代浏览器和不支持WebSocket协议的环境。
git地址
https://github.com/sockjs/sockjs-client
stompjs
介绍
STOMP Simple (or Streaming) Text Orientated Messaging Protocol 它定义了可互操作的连线格式,以便任何可用的STOMP客户端都可以与任何STOMP消息代理进行通信,以在语言和平台之间提供简单而广泛的消息互操作性(归纳一句话:是一个简单的面向文本的消息传递协议。)
官网
http://jmesnil.net/stomp-websocket/doc/
https://stomp-js.github.io/stomp-websocket/codo/class/Client.html
SpringBoot第一个案例(游戏公告通知)
项目结构

添加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.24</version> </dependency>
WebSocket配置类
package com.ybchen.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配置类 * * @author: chenyanbin 2022-07-02 21:21 */ @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { /** * 注册端点,发布或者订阅消息的时候,需要连接此端点 * * @param registry */ @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry //添加端点 .addEndpoint("/endpoint-websocket") //设置允许访问的源(所有) .setAllowedOrigins("*") //开始sockjs支持 .withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { //设置消息代理(服务端推送给客户端的路径前缀) registry.enableSimpleBroker("/topic", "/chat"); //设置消息发布订阅的头(客户端发送数据给服务器的一个前缀) registry.setApplicationDestinationPrefixes("/app"); } }
Model
package com.ybchen.model; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import java.util.Date; /** * @author: chenyanbin 2022-07-02 21:14 */ @Data public class InMessage { /** * 从哪里来 */ private String from; /** * 到哪里去 */ private String to; /** * 内容 */ private String content; /** * 时间 */ @JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") private Date time; }
package com.ybchen.model; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import java.util.Date; /** * @author: chenyanbin 2022-07-02 21:16 */ @Data public class OutMessage { /** * 从哪里来 */ private String from; /** * 内容 */ private String content; /** * 时间 */ @JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") private Date time; }
Controller
package com.ybchen.controller.v1; import com.ybchen.model.InMessage; import com.ybchen.model.OutMessage; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.stereotype.Controller; /** * 游戏公告-控制层 * * @author: chenyanbin 2022-07-02 21:17 */ @Controller @Slf4j public class GameInfoController { //消息路由,别人发送的消息会到这里来 @MessageMapping("/v1/chat") //发送哪里去 @SendTo("/topic/game_chat") public OutMessage gameInfo(InMessage message) { log.info("接收信息:{}", message); OutMessage outMessage = new OutMessage(); BeanUtils.copyProperties(message, outMessage); return outMessage; } }
游戏公告演示

前端页面
<!DOCTYPE HTML> <html> <head> <title>游戏公告客户端</title> <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.js"></script> </head> <body> 内容 <input id="text" type="text" style="width:500px"/> <button onclick="connect()">建立连接</button> <button onclick="disconnect()">释放连接</button> <div id="message"></div> </body> <script type="text/javascript"> var stompClient = null; //建立连接 function connect() { var socket = new SockJS('http://127.0.0.1:9999/endpoint-websocket'); //连接上端点(基站) stompClient = Stomp.over(socket); //用stom进行包装,规范协议 stompClient.connect({}, function (frame) { console.log('Connected: ' + frame); stompClient.subscribe('/topic/game_chat', function (result) { console.info("result="+result) showContent(JSON.parse(result.body)); }); }); alert("连接成功"); } //显示内容 function showContent(body) { console.log(body); document.getElementById('message').innerHTML += body.content+","+body.time + '<br/>'; } //释放连接 function disconnect(){ if (stompClient !== null) { stompClient.disconnect(); alert("释放连接成功"); }else{ alert("释放连接失败"); } } </script> </html>
<!DOCTYPE HTML> <html> <head> <title>游戏公告服务端</title> <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.js"></script> </head> <body> 内容 <input id="text" type="text" style="width:500px"/> <button onclick="send()">发送消息</button> <button onclick="connect()">建立连接</button> <button onclick="disconnect()">释放连接</button> <div id="message"></div> </body> <script type="text/javascript"> var stompClient = null; //建立连接 function connect() { var socket = new SockJS('http://127.0.0.1:9999/endpoint-websocket'); //连接上端点(基站) stompClient = Stomp.over(socket); //用stom进行包装,规范协议 stompClient.connect({}, function (frame) { console.log('Connected: ' + frame); stompClient.subscribe('/topic/game_chat', function (result) { console.info("result="+result) showContent(JSON.parse(result.body)); }); }); alert("连接成功"); } //显示内容 function showContent(body) { console.log(body); document.getElementById('message').innerHTML += body.content+","+body.time + '<br/>'; } //发送消息 function send() { var message = document.getElementById('text').value; stompClient.send("/app/v1/chat", {}, JSON.stringify({'content':message })); } //释放连接 function disconnect(){ if (stompClient !== null) { stompClient.disconnect(); alert("释放连接成功"); }else{ alert("释放连接失败"); } } </script> </html>
WebSocket推送方式的区别
- sendTo,不常用,固定发送给指定的订阅者
- SimpMessagingTemplate,灵活,支持多种发送方式
项目结构

Controller
package com.ybchen.controller.v2; import com.ybchen.model.InMessage; import com.ybchen.service.WebSocketService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.stereotype.Controller; /** * 游戏公告-控制层 * * @author: chenyanbin 2022-07-02 21:17 */ @Controller @Slf4j public class GameInfoV2Controller { @Autowired WebSocketService webSocketService; //消息路由,别人发送的消息会到这里来 @MessageMapping("/v2/chat") public void gameInfo(InMessage message) { log.info("接收信息:{}", message); webSocketService.sendTopicMessage(message); } }
Service
package com.ybchen.service; import com.ybchen.model.InMessage; import com.ybchen.model.OutMessage; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; import java.util.Date; /** * 简单消息模板,用来推送消息 * * @author: chenyanbin 2022-07-03 17:04 */ @Service public class WebSocketService { @Autowired private SimpMessagingTemplate template; public void sendTopicMessage(InMessage message) { OutMessage outMessage = new OutMessage(); BeanUtils.copyProperties(message, outMessage); outMessage.setTime(new Date()); for (int i = 0; i < 10; i++) { outMessage.setTime(new Date()); try { Thread.sleep(300); } catch (InterruptedException e) { } //发送消息 template.convertAndSend("/topic/game_rank", outMessage); } } }
前端页面
<!DOCTYPE HTML>
<html>
<head>
<title>游戏公告客户端</title>
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.js"></script>
</head>
<body>
内容
<input id="text" type="text" style="width:500px"/>
<button onclick="connect()">建立连接</button>
<button onclick="disconnect()">释放连接</button>
<div id="message"></div>
</body>
<script type="text/javascript">
var stompClient = null;
//建立连接
function connect() {
var socket = new SockJS('http://127.0.0.1:9999/endpoint-websocket'); //连接上端点(基站)
stompClient = Stomp.over(socket); //用stom进行包装,规范协议
stompClient.connect({}, function (frame) {
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/game_rank', function (result) {
console.info("result="+result)
showContent(JSON.parse(result.body));
});
});
alert("连接成功");
}
//显示内容
function showContent(body) {
console.log(body);
document.getElementById('message').innerHTML += body.content+","+body.time + '<br/>';
}
//释放连接
function disconnect(){
if (stompClient !== null) {
stompClient.disconnect();
alert("释放连接成功");
}else{
alert("释放连接失败");
}
}
</script>
</html>
<!DOCTYPE HTML>
<html>
<head>
<title>游戏公告服务端</title>
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.js"></script>
</head>
<body>
内容
<input id="text" type="text" style="width:500px"/>
<button onclick="send()">发送消息</button>
<button onclick="connect()">建立连接</button>
<button onclick="disconnect()">释放连接</button>
<div id="message"></div>
</body>
<script type="text/javascript">
var stompClient = null;
//建立连接
function connect() {
var socket = new SockJS('http://127.0.0.1:9999/endpoint-websocket'); //连接上端点(基站)
stompClient = Stomp.over(socket); //用stom进行包装,规范协议
stompClient.connect({}, function (frame) {
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/game_rank', function (result) {
console.info("result="+result)
showContent(JSON.parse(result.body));
});
});
alert("连接成功");
}
//显示内容
function showContent(body) {
console.log(body);
document.getElementById('message').innerHTML += body.content+","+body.time + '<br/>';
}
//发送消息
function send() {
var message = document.getElementById('text').value;
stompClient.send("/app/v2/chat", {}, JSON.stringify({'content':message }));
}
//释放连接
function disconnect(){
if (stompClient !== null) {
stompClient.disconnect();
alert("释放连接成功");
}else{
alert("释放连接失败");
}
}
</script>
</html>
演示

WebSocket 4类监听器
- SessionSubscribeEvent 订阅事件
- SessionUnsubscribeEvent取消订阅事件
- SessionDisconnectEvent 断开连接事件
- SessionDisconnectEvent 建立连接事件
package com.ybchen.listener; import org.springframework.context.ApplicationListener; import org.springframework.messaging.Message; import org.springframework.messaging.simp.SimpMessageType; import org.springframework.messaging.simp.stomp.StompHeaderAccessor; import org.springframework.stereotype.Component; import org.springframework.web.socket.messaging.SessionConnectEvent; /** * 连接事件监听器 * @author: chenyanbin 2022-07-03 17:35 */ @Component public class ConnectEventListener implements ApplicationListener<SessionConnectEvent> { @Override public void onApplicationEvent(SessionConnectEvent event) { Message<byte[]> message = event.getMessage(); //消息头响应器 StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(message); //消息类型 SimpMessageType messageType = headerAccessor.getCommand().getMessageType(); System.err.println("【ConnectEventListener监听器事件】消息类型:"+messageType); } }
package com.ybchen.listener; import org.springframework.context.ApplicationListener; import org.springframework.messaging.Message; import org.springframework.messaging.simp.SimpMessageType; import org.springframework.messaging.simp.stomp.StompHeaderAccessor; import org.springframework.stereotype.Component; import org.springframework.web.socket.messaging.SessionConnectEvent; /** * 释放连接事件监听器 * @author: chenyanbin 2022-07-03 17:35 */ @Component public class DisconnectEventListener implements ApplicationListener<SessionConnectEvent> { @Override public void onApplicationEvent(SessionConnectEvent event) { Message<byte[]> message = event.getMessage(); //消息头响应器 StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(message); //消息类型 SimpMessageType messageType = headerAccessor.getCommand().getMessageType(); System.err.println("【DisconnectEventListener监听器事件】消息类型:"+messageType); } }
package com.ybchen.listener; import org.springframework.context.ApplicationListener; import org.springframework.messaging.Message; import org.springframework.messaging.simp.SimpMessageType; import org.springframework.messaging.simp.stomp.StompHeaderAccessor; import org.springframework.stereotype.Component; import org.springframework.web.socket.messaging.SessionSubscribeEvent; /** * 订阅事件监听器 * @author: chenyanbin 2022-07-03 17:35 */ @Component public class SubscribeEventListener implements ApplicationListener<SessionSubscribeEvent> { @Override public void onApplicationEvent(SessionSubscribeEvent event) { Message<byte[]> message = event.getMessage(); //消息头响应器 StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(message); //消息类型 SimpMessageType messageType = headerAccessor.getCommand().getMessageType(); System.err.println("【SubscribeEventListener监听器事件】消息类型:"+messageType); } }

演示

点对点简单版单人聊天
controller
package com.ybchen.controller.v3; import com.ybchen.model.InMessage; import com.ybchen.service.WebSocketService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.stereotype.Controller; /** * 简单版单人聊天 * * @author: chenyanbin 2022-07-03 17:59 */ @Controller public class V3ChatRoomController { @Autowired WebSocketService webSocketService; @MessageMapping("/v3/single/chat") public void singleChat(InMessage message) { webSocketService.sendChatMessage(message); } }
service
package com.ybchen.service; import com.ybchen.model.InMessage; import com.ybchen.model.OutMessage; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; import java.util.Date; /** * 简单消息模板,用来推送消息 * * @author: chenyanbin 2022-07-03 17:04 */ @Service public class WebSocketService { @Autowired private SimpMessagingTemplate template; /** * 发送消息 * * @param destination 目的地 * @param message 消息内容 */ public void sendTopicMessage(String destination, InMessage message) { OutMessage outMessage = new OutMessage(); BeanUtils.copyProperties(message, outMessage); outMessage.setTime(new Date()); for (int i = 0; i < 10; i++) { outMessage.setTime(new Date()); try { Thread.sleep(300); } catch (InterruptedException e) { } //发送消息 template.convertAndSend(destination, outMessage); } } /** * 发送聊天消息 * * @param message */ public void sendChatMessage(InMessage message) { OutMessage outMessage = new OutMessage(); BeanUtils.copyProperties(message, outMessage); outMessage.setTime(new Date()); outMessage.setContent(message.getFrom()+" 发送:"+message.getContent()); //发送消息 template.convertAndSend("/chat/single/" + message.getTo(), outMessage); } }
前端页面
<!DOCTYPE HTML> <html> <head> <title>简单版单人聊天</title> <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.js"></script> </head> <body> <input type="text" id="from" class="form-control" placeholder="我是"> <input type="text" id="to" class="form-control" placeholder="发送给谁"> 内容 <input id="text" type="text" style="width:500px"/> <button onclick="send()">发送消息</button> <button onclick="connect()">建立连接</button> <button onclick="disconnect()">释放连接</button> <div id="message"></div> </body> <script type="text/javascript"> var stompClient = null; //建立连接 function connect() { var from = document.getElementById('from').value; console.log("from="+from) var socket = new SockJS('http://127.0.0.1:9999/endpoint-websocket'); //连接上端点(基站) stompClient = Stomp.over(socket); //用stom进行包装,规范协议 stompClient.connect({}, function (frame) { console.log('Connected: ' + frame); stompClient.subscribe('/chat/single/'+from, function (result) { console.info("result="+result) showContent(JSON.parse(result.body)); }); }); alert("连接成功"); } //显示内容 function showContent(body) { console.log(body); document.getElementById('message').innerHTML += body.content+","+body.time + '<br/>'; } //发送消息 function send() { var message = document.getElementById('text').value; var from = document.getElementById('from').value; var to = document.getElementById('to').value; stompClient.send("/app/v3/single/chat", {}, JSON.stringify({'content':message,'to':to,'from':from })); } //释放连接 function disconnect(){ if (stompClient !== null) { stompClient.disconnect(); alert("释放连接成功"); }else{ alert("释放连接失败"); } } </script> </html>
演示

实时监控服务器JVM负载
controller
package com.ybchen.controller.v4; import com.ybchen.service.WebSocketService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Controller; /** * 实时推送服务器的JVM负载 * @author: chenyanbin 2022-07-03 18:26 */ @Controller public class V4ServiceInfoController { @Autowired private WebSocketService webSocketService; @MessageMapping("/v4/schedule/push") //3秒一次 @Scheduled(fixedDelay = 3000) public void sendServiceInfo(){ webSocketService.sendServiceInfo(); } }
service
package com.ybchen.service; import com.ybchen.model.InMessage; import com.ybchen.model.OutMessage; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; import java.util.Date; /** * 简单消息模板,用来推送消息 * * @author: chenyanbin 2022-07-03 17:04 */ @Service public class WebSocketService { @Autowired private SimpMessagingTemplate template; /** * 发送消息 * * @param destination 目的地 * @param message 消息内容 */ public void sendTopicMessage(String destination, InMessage message) { OutMessage outMessage = new OutMessage(); BeanUtils.copyProperties(message, outMessage); outMessage.setTime(new Date()); for (int i = 0; i < 10; i++) { outMessage.setTime(new Date()); try { Thread.sleep(300); } catch (InterruptedException e) { } //发送消息 template.convertAndSend(destination, outMessage); } } /** * 发送聊天消息 * * @param message */ public void sendChatMessage(InMessage message) { OutMessage outMessage = new OutMessage(); BeanUtils.copyProperties(message, outMessage); outMessage.setTime(new Date()); outMessage.setContent(message.getFrom() + " 发送:" + message.getContent()); //发送消息 template.convertAndSend("/chat/single/" + message.getTo(), outMessage); } /** * 推送服务器JVM负载信息 */ public void sendServiceInfo() { //系统核数 int processors = Runtime.getRuntime().availableProcessors(); //空闲内存 long freeMemory = Runtime.getRuntime().freeMemory(); //最大内存 long maxMemory = Runtime.getRuntime().maxMemory(); String message = "服务器可用处理器:" + processors+",虚拟机空闲内存大小:"+freeMemory+",最大内存:"+maxMemory; OutMessage outMessage = new OutMessage(); outMessage.setTime(new Date()); outMessage.setContent(message); //发送消息 template.convertAndSend("/topic/service_info", outMessage); } }
前端页面
<!DOCTYPE HTML> <html> <head> <title>服务器JVM负载</title> <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.js"></script> </head> <body> 内容 <input id="text" type="text" style="width:500px"/> <button onclick="connect()">建立连接</button> <button onclick="disconnect()">释放连接</button> <div id="message"></div> </body> <script type="text/javascript"> var stompClient = null; //建立连接 function connect() { var socket = new SockJS('http://127.0.0.1:9999/endpoint-websocket'); //连接上端点(基站) stompClient = Stomp.over(socket); //用stom进行包装,规范协议 stompClient.connect({}, function (frame) { console.log('Connected: ' + frame); stompClient.subscribe('/topic/service_info', function (result) { console.info("result="+result) showContent(JSON.parse(result.body)); }); }); alert("连接成功"); } //显示内容 function showContent(body) { console.log(body); document.getElementById('message').innerHTML += body.content+","+body.time + '<br/>'; } //释放连接 function disconnect(){ if (stompClient !== null) { stompClient.disconnect(); alert("释放连接成功"); }else{ alert("释放连接失败"); } } </script> </html>
演示

案例源码
链接: https://pan.baidu.com/s/18QplqZL7fMyVA7w7PjRKrA?pwd=ecj5 提取码: ecj5
nginx代理websocket
http {
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream websocket {
ip_hash; #使用ip固定转发到后端服务器
server localhost:3100;
server localhost:3101;
server localhost:3102;
}
server {
listen 8020;
location / {
proxy_pass http://websocket;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade; # 声明支持websocket
}
}
}

浙公网安备 33010602011771号