强化学习帖子,参考原帖:SpringBoot2.0集成WebSocket,实现后台向前端推送信息
websocket是什么:
WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。
在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,
并进行双向数据传输。
websocket通信模型(图片和定义均来自百度百科):
为什么需要websocket:
很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的的时间间隔(如每1秒),
由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。
这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,
其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
而比较新的技术去做轮询的效果是Comet。这种技术虽然可以双向通信,但依然需要反复发出请求。
而且在Comet中,普遍采用的长链接,也会消耗服务器资源。
在这种情况下,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
webscoket兼容性
1,websocket各个浏览器的兼容性如下图
2,解决兼容性问题
参考掘金的这个帖子: 如何解决WebSocket的兼容性
开发环境
vue:2.6.11
vue-cli:3.10.0
springboot:2.3.1
jdk:1.8
springboot内置tomcat:9.0.17
maven依赖
在已经搭建了springboot可以调试通接helloworld的基础上添加下面的依赖
<!-- websocket的starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 中间会用到处理字符串StringUtils工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.10</version>
</dependency>
application.properties配置
避免和本地其他项目冲突,这块对端口进行了配置,vue部分需要对接端口,默认端口是8080
server.port=9999
WebsocketServerConfig
配置启用websocket支持,很简单几行代码就可以实现
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* websocket的配置类
* @author user
* @date 2020年6月10日15:24:36
* @version 1.0
*/
@Configuration
public class WebsocketConfig implements WebSocketMessageBrokerConfigurer {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
WebsocketServer
核心代码都在这里,websocket的服务端部分
1,因为websocket是类似客户服务端的ws协议,因此这里的WebsocketServier就相当于一个ws协议的Controller了
2,直接 @ServerEndpoint("/imserver/{userId}") 、@Component启用即可,然后在里面实现 @OnOpen开启连接,@onClose关闭连接,@onMessage接收消息等方法。
3,新建一个ConcurrentHashMap webSocketMap 用于接收当前userId的WebSocket,方便IM之间对userId进行推送消息。
package com.jitu.vuepersonback.controller;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
/**
* websocket的服务端
* @author user
* @date 2020年6月15日10:49:28
* @version 1.0
*/
@ServerEndpoint("/imserver/{userId}")
@Component
public class WebsocketServer {
/**
* 日志
*/
private static final Logger logger = LoggerFactory.getLogger(WebsocketServer.class);
/**
* 静态变量,用来计算当前在线连接数
*/
private static Integer onlineCount = 0;
/**
* concurrent包的线程安全set,用来存放每个客户端对应的MyWebsocket对象
*/
private static ConcurrentHashMap<String,WebsocketServer> webSocketMap = new ConcurrentHashMap();
/**
* 与某个客户端连接绘画,需要通过它发送数据给客户端
*/
private Session session;
/**
* 接受userId
*/
private String userId = "";
/**
* 链接成功调用的方法
* @param session 会话信息
* @param userId 用户id
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId")String userId ){
this.session = session;
this.userId = userId;
if(webSocketMap.containsKey(userId)){
webSocketMap.remove(userId);
webSocketMap.put(userId,this);
// 加入到set中
} else {
webSocketMap.put(userId,this);
// 加入到set中
addOnlineCount();
}
logger.info("用户:"+userId+"连接,当前在线人数:"+getOnlineCount());
try {
}catch (Exception ex){
logger.info("用户:"+userId+",网络异常!");
}
}
/**
* 关闭时候触发的方法
*/
@OnClose
public void onClose(){
if(webSocketMap.containsKey(userId)){
webSocketMap.remove(userId);
// 从set中移除
subOnlineCount();
}
logger.info("用户:"+userId+",退出,当前在线人数:"+getOnlineCount());
}
/**
* 收到客户端消息调用的对象
* @param message 消息信息 JSON数据
* @param session 对话
*/
@OnMessage
public void onMessage(String message,Session session){
logger.info("用户:"+userId+",消息:"+message);
if(StringUtils.isNotBlank(message)){
// 解析发的报文
System.out.println(message);
}
}
/**
* 发生错误触发的方法
* @param session 对话
* @param throwable 异常对象
*/
@OnError
public void onError(Session session,Throwable throwable){
logger.info("用户:"+userId+",发生错误。错误原因:"+throwable.getMessage());
throwable.printStackTrace();
}
/**
* 发送消息的方法
* @param message 消息
* @throws IOException 异常对象
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
/**
* 发送自定义消息
*/
public static void sendInfo(String message,@PathParam("userId")String userId)throws IOException{
logger.info("发送消息到:"+userId+",消息内容:"+message);
if(StringUtils.isNotBlank(message) && webSocketMap.containsKey(userId)){
webSocketMap.get(userId).sendMessage(message);
}else{
logger.error("用户:"+userId+",不在线!");
}
}
public static synchronized Integer getOnlineCount(){
return WebsocketServer.onlineCount;
}
public static synchronized void addOnlineCount(){
WebsocketServer.onlineCount ++;
}
public static synchronized void subOnlineCount(){
WebsocketServer.onlineCount --;
}
}
后台到前台的消息推送
消息推送,单独写一个controller,通过接口调用的形式实现消息推送
package com.jitu.vuepersonback.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
/**
* websocket测试的demo控制层
* @author user
* @date 2020年6月15日16:47:502
* @version 1.0
*/
@RestController
public class WebsocketDemoController {
/**
*ResponseEntity 是一个springframework封装的返回结果集,可有可无。
*/
@RequestMapping("index")
public ResponseEntity<String> index(){
return ResponseEntity.ok("ok");
}
/**
* @Description 发送消息的接口
* @param message 消息
* @param toUserId 发送给的用户id
* @Return org.springframework.http.ResponseEntity<java.lang.String>
* @Author user
* @Date 2020/7/23 17:33
* @version 1.0
* @since 1.0
*/
@RequestMapping("/push/{toUserId}")
public ResponseEntity<String> pushToWeb(String message, @PathVariable String toUserId) throws IOException {
// message = "this is a websocket server message";
WebsocketServer.sendInfo(message,toUserId);
return ResponseEntity.ok("MSG SEND SUCCESS");
}
}
跨域
因为用vue是前后端分离的架构形式,所以这边要解决下跨域问题,通过java配置过滤器的方式解决,这边为了测试,没有设置放行规则,直接全部放行了。
package com.jitu.vuepersonback.filter;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
/**
* @author user
* @Title: CrosFilter
* @ProjectName
* @Description: 解决跨域,全部放行
* @date 2020年6月4日17:57:43
*/
@Component
public class CorsFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String url = request.getServletPath();
response.setHeader("Access-Control-Allow-Origin", "*");
response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.addHeader("Access-Control-Allow-Headers","Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers,token");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
前端vue实现
后端的代码,到这里就告一段落了,下面是前端的,我这边采用的是vue实现websocket,原贴是原生HTML+jq实现。
脚手架搭建过程、路由配置我这边都不赘述,直接上dmeo部分的代码
<template>
<div class="wrapper">
<h1>websocketDemo 测试页面</h1>
<p>【userId】:<div><input id="userId" type="text" v-model="userId"></div>
<p>【toUserId】:<div><input id="toUserId" type="text" v-model="toUserId"></div>
<p>【toUserId】:<div><input id="contentText" type="text" v-model="contentText"></div>
<p>【receive msg】:<div class="receiveMsgDiv" style="text-align:center;width:100%;height:40px;">{{receiveMsg}}</div>
<p>【操作】:<div><a href="javascript:;" @click="openSocket()">开启socket</a></div>
<p>【操作】:<div><a href="javascript:;" @click="sendMessage()">发送消息</a></div>
</div>
</template>
<script>
export default {
components: {},
props: {},
data () {
return {
socket: undefined,
userId: '10',
toUserId: '20',
contentText: 'hello websocket',
receiveMsg: ''
}
},
watch: {},
computed: {},
created () {},
mounted () {
},
methods: {
openSocket () {
console.log(this.socket)
if (typeof (WebSocket) === 'undefined') {
console.log('your browser not support websocket')
} else {
console.log('your bowser support websocket')
// 创建websocket对象,指定要链接的服务器地址和端口,建立连接
var socketUrl = 'http://192.168.4.24:9999/imserver/' + this.userId
socketUrl = socketUrl.replace('https', 'ws').replace('http', 'ws')
// 如果已经被打开了,先关闭,然后重新创建链接
if (this.socket != null) {
this.socket.close()
this.socket = null
}
this.socket = new WebSocket(socketUrl)
// 打开事件
this.socket.onopen = function () {
console.log('webssocket is open!')
}
// 获得消息事件
var that = this
this.socket.onmessage = function (msg) {
// this为websocket对象
console.log('webscoket message :' + msg.data)
that.receiveMsg = msg.data
}
// 关闭事件
this.socket.onclose = function () {
console.log('websocket is close!')
}
// 发生错误事件
this.socket.onerror = function () {
console.log('webscoket occur an error')
}
console.log(socketUrl)
}
},
sendMessage () {
if (typeof (WebSocket) === 'undefined') {
console.log('your browser not support websocket!')
} else {
console.log('your borwserr support websocket!')
const message = '{"toUserId":"' + this.toUserId + '","contentText":"' + this.contentText + '"}'
console.log(message)
// this.socket.send(message)
}
}
}
}
</script>
<style scoped>
.wrapper{
width: 100%;
height: 100%;
}
</style>
测试
前端页面运行之后结果如下,点击开启websocket按钮之后,控制台会打印当前连接的websocket地址,在WebSocket 在线测试这块填入这个地址,测试后台的websocket服务是否开启成功
如果出现下面这种情况说明你的websocket服务已经正常启动且服务正常。
通过调用后台的 **/push/{toUserId}**这个接口可以推送消息到前台页面,直接通过浏览器地址栏的形式发送消息进行测试
前端接收到消息更新到指定div中如下:
至此,实现后台websocket实时推送到前台。