SpringBoot集成websocket实现消息推送
websocket介绍
为什么需要websocket
初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?
答案很简单,因为 HTTP 协议有一个缺陷:通信只能由客户端发起。
举例来说,我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。
这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用"轮询":每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。
轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。
WebSocket 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯,它建立在 TCP 之上,同 HTTP 一样通过 TCP 来传输数据。
websocket的特点
WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。
它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
其他特点包括:
(1)建立在 TCP 协议之上,服务器端的实现比较容易。
(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据。
(5)没有同源限制,客户端可以与任意服务器通信。
(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
websocket的适用场景
- 弹幕
- 媒体聊天
- 协同编辑
- 基于位置的应用
- 体育实况更新
- 股票基金报价实时更新
SpringBoot集成websocket
基础用法(原生API)
引入相关的依赖
<!--websocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
开启websocket
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
创建WebSocketSever
@ServerEndpoint("/shell/motorWebsocket")
@Component
public class MotorAlarmWebSocketServer {
// concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象
private static final CopyOnWriteArraySet<MotorAlarmWebSocketServer> webSocketSet = new CopyOnWriteArraySet<>();
public static Set<MotorAlarmWebSocketServer> getWebSocketSet() {
return webSocketSet;
}
// 与某个客户端的连接会话,需要通过它来与客户端进行数据收发
private Session session;
@OnOpen
public void onOpen(Session session) throws IOException {
this.session = session;
webSocketSet.add(this);
System.out.println("系统连接成功");
}
@OnClose
public void onClose() {
webSocketSet.remove(this);
}
/**
* 收到客户端消息后调用的方法
*/
@OnMessage
public void onMessage(String message) {
//群发消息
for (MotorAlarmWebSocketServer item : webSocketSet) {
try {
item.sendMessage(message);
} catch (IOException e) {
System.out.println("消息发送失败");
}
}
}
@OnError
public void onError(Session session, Throwable error) {
System.out.println("断开连接");
}
/**
* 实现服务器主动推送
*/
public void sendMessage(String message) throws IOException {
for (MotorAlarmWebSocketServer item : webSocketSet) {
if (item.session.isOpen()) {
item.session.getBasicRemote().sendText(message);
}
}
}
}
在前端界面使用websocket
websocket.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>websocket演示</title>
</head>
<body>
<h4>websocket演示</h4>
<p id="content"></p>
</body>
<script type="application/javascript">
var websocket = null;
if ('WebSocket' in window) {
websocket = new WebSocket('ws://127.0.0.1:9999/shell/motorWebsocket');
} else {
alert('该浏览器不支持websocket!');
}
websocket.onopen = function (event) {
console.log('建立连接');
document.getElementById("content").innerText = '建立连接';
}
websocket.onclose = function (event) {
console.log('连接关闭');
document.getElementById("content").innerText = '连接关闭';
}
websocket.onmessage = function (event) {
console.log('收到消息:' + event.data)
//所要执行的操作
document.getElementById("content").innerText = "收到消息:" + event.data;
}
websocket.onerror = function () {
alert('websocket通信发生错误!');
}
window.onbeforeunload = function () {
websocket.close();
}
</script>
</html>
提供controller触发服务端向前端推送消息
@RestController
public class WebSocketController {
@Autowired
private MotorAlarmWebSocketServer webSocketServer;
@GetMapping("sendMessage")
public String sendMessage(String message) throws Exception {
webSocketServer.sendMessage(message);
return "SEND OK";
}
}
测试验证
启动项目后,浏览器访问http://localhost:9999/sendMessage?message=hello ,就可以在前端html页面看到收到的消息。
websocket的STOMP支持
STOMP是一个简单的可互操作的协议,通常用于中间服务器与客户端之间进行异步消息传递。
引入相关的依赖
<!--websocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
WebSocketConfig.java
/**
* @Description EnableWebSocketMessageBroker-注解开启STOMP协议来传输基于代理的消息,此时控制器支持使用@MessageMapping
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
//topic用来广播,user用来实现点对点
config.enableSimpleBroker("/topic", "/user");
}
/**
* 开放节点
*
* @param registry
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//注册两个STOMP的endpoint,分别用于广播和点对点
//广播
registry.addEndpoint("/publicServer").setAllowedOrigins("*").withSockJS();
//点对点
registry.addEndpoint("/privateServer").setAllowedOrigins("*").withSockJS();
}
}
推送消息的实体类 Message.java
public class Message {
/**
* 消息编码
*/
private String code;
/**
* 来自(保证唯一)
*/
private String form;
/**
* 去自(保证唯一)
*/
private String to;
/**
* 内容
*/
private String content;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getForm() {
return form;
}
public void setForm(String form) {
this.form = form;
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
前端广播消息调试
1)publicExample.html 监听广播消息的测试页面
<html>
<head>
<meta charset="UTF-8">
<title>等系统推消息</title>
<script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://code.jquery.com/jquery-3.2.0.min.js"
integrity="sha256-JAW99MJVpJBGcbzEuXk4Az05s/XyDdBomFqNlM3ic+I=" crossorigin="anonymous"></script>
<script type="text/javascript">
var stompClient = null;
function setConnected(connected) {
document.getElementById("connect").disabled = connected;
document.getElementById("disconnect").disabled = !connected;
$("#response").html();
}
function connect() {
var socket = new SockJS("http://localhost:9999/publicServer"); //publicServer连接广播节点
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
setConnected(true);
console.log('Connected: ' + frame);
///topic/all 订阅/topic为前缀的主题, /topic/all
stompClient.subscribe('/topic/all', function (response) {
var responseData = document.getElementById('responseData');
var p = document.createElement('p');
p.style.wordWrap = 'break-word';
p.appendChild(document.createTextNode(response.body));
responseData.appendChild(p);
});
}, {});
}
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
function sendMsg() {
var content = document.getElementById('content').value;
stompClient.send("/all", {}, JSON.stringify({'content': content}));
}
</script>
</head>
<body onload="disconnect()">
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being
enabled. Please enable
Javascript and reload this page!</h2></noscript>
<div>
<div>
<labal>连接广播频道</labal>
<button id="connect" onclick="connect();">Connect</button>
<labal>取消连接</labal>
<button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>
</div>
<div id="conversationDiv">
<labal>广播消息</labal>
<input type="text" id="content"/>
<button id="sendMsg" onclick="sendMsg();">Send</button>
</div>
<div>
<labal>接收到的消息:</labal>
<p id="responseData"></p>
</div>
</div>
</body>
</html>
2)TestController.java
@RestController
public class TestSocketController {
@Autowired
public SimpMessagingTemplate template;
/**
* 广播
*
* @param msg
*/
@PostMapping("pushToAll")
public String sendMessage(@RequestBody Message msg) throws Exception {
template.convertAndSend("/topic/all", msg.getContent());
return "SEND OK";
}
}
说明:我们推送消息,直接用 SimpMessagingTemplate ,用的是convertAndSend 广播方式推送到对于的主题目的地 destination 。(其实还有convertAndSendToUser)
3)直接把项目跑起来,打开页面开始测试
我们调用测试接口,推送广播消息(前端页面在浏览器打开多个tab,并点击Connect进行连接):
点对点消息测试
1)前端页面:privateExample.html
<html>
<head>
<meta charset="UTF-8">
<title>聊起来</title>
<script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://code.jquery.com/jquery-3.2.0.min.js"
integrity="sha256-JAW99MJVpJBGcbzEuXk4Az05s/XyDdBomFqNlM3ic+I=" crossorigin="anonymous"></script>
<script type="text/javascript">
var stompClient = null;
function setConnected(connected) {
document.getElementById("connect").disabled = connected;
document.getElementById("disconnect").disabled = !connected;
$("#response").html();
}
function connect() {
var socket = new SockJS("http://localhost:9999/privateServer"); //连接点对点节点
stompClient = Stomp.over(socket);
stompClient.heartbeat.outgoing = 20000;
// client will send heartbeats every 20000ms
stompClient.heartbeat.incoming = 0;
stompClient.connect({}, function (frame) {
setConnected(true);
console.log('Connected: ' + frame);
//订阅/user开头,格式:/user + 连接的用户唯一标识 + /message
stompClient.subscribe('/user/'+document.getElementById('user').value+'/message', function (response) {
var responseData = document.getElementById('responseData');
var p = document.createElement('p');
p.style.wordWrap = 'break-word';
p.appendChild(document.createTextNode(response.body));
responseData.appendChild(p);
});
});
}
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
function sendMsg() {
var headers = {
login: 'mylogin',
passcode: 'mypasscode',
// additional header
'accessToken': 'HWPO325J9814GBHJF933'
};
var content = document.getElementById('content').value;
var to = document.getElementById('to').value;
stompClient.send("/alone", {'accessToken': 'HWPO325J9814GBHJF933'}, JSON.stringify({
'content': content,
'to': to
}));
}
</script>
</head>
<body onload="disconnect()">
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being
enabled. Please enable
Javascript and reload this page!</h2></noscript>
<div>
<div>
<labal>连接用户</labal>
<input type="text" id="user"/>
<button id="connect" onclick="connect();">Connect</button>
</div>
<div>
<labal>取消连接</labal>
<button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>
</div>
<div id="conversationDiv">
<labal>发送消息</labal>
<div>
<labal>内容</labal>
<input type="text" id="content"/>
</div>
<div>
<labal>发给谁</labal>
<input type="text" id="to"/>
</div>
<button id="sendMsg" onclick="sendMsg();">Send</button>
</div>
<div>
<labal>接收到的消息:</labal>
<p id="responseData"></p>
</div>
</div>
</body>
</html>
2)TestController.java
@PostMapping("/pushToOne")
public void queue(@RequestBody Message msg) {
System.out.println("进入方法");
/*使用convertAndSendToUser方法,第一个参数为用户id,此时js中的订阅地址为
"/user/" + 用户Id + "/message",其中"/user"是固定的*/
template.convertAndSendToUser(msg.getTo(), "/message", msg.getContent());
}
用的是convertAndSendToUser 推送到指定的用户 ,对于的主题目的地 destination(/message)。
也许看到这里,你会觉得很奇怪,为什么我们推的主题是 /message,但是前端订阅的却是:
"/user/" + 用户Id + "/message"
我们直接看源码:
应该不用多说,代码帮我们自己拼接起来了,跟前端订阅规则保持一致。
启动项目,模拟下。
① 模拟我们连接的用户标识 19901 ,连接成功
② 使用postman调用我们的测试接口,模拟系统指定推送消息到 19901 这个人 :
③ 我们重新打开一个html页面,然后也给20011发送一下消息:
对点推送,广播推送,也已经完毕了 。
这种情况就是相当于使用http接口方式,去撮合后端服务做消息推送。
其实spring还提供了一些好玩的注解:@MessageMapping 这个注解是对称于 @EnableWebSocketMessageBroker
也就是说,如果我们使用@EnableWebSocketMessageBroker ,那么我们在接口上面其实就能直接使用 @MessageMapping。
然后前端代码里面的使用:
个人认为其实没有必要使用这个注解,直接通过前端调用后端服务代码,我们服务端来根据Message里面 的 发送方、接收方、消息类型(点对点、广播)就可以直接完成相关也业务场景了。
参考: |