vue+springboot+sockJs之原生websocket长连接实现消息推送
最近公司需要新增消息通知的功能,首先想到通过websocket长连接实现,由于主要业务还在于服务端的主动推送所以本文重点也是如此。
相信在选择使用websocket实现类似功能的小伙伴肯定对它有一定的了解了,因此本文不在赘述。
本文介绍的是使用原生websocket实现消息推送,下篇文章会着重介绍sockJs+stomp的具体实现。
先从服务端说起吧,首先肯定少不了依赖引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
下面开始进入正题了,需要在服务端开启对websocket的支持,并对websocket进行配置
@Configuration
@EnableWebMvc
@EnableWebSocket
public class WebSocketconfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(systemWebSocketHandler(), "/websocket")
.setAllowedOrigins("*")
.addInterceptors(new HandshakeInterceptor());
registry.addHandler(systemWebSocketHandler(), "/ws")
.setAllowedOrigins("*")
.withSockJS();
}
@Bean
public WebSocketHandler systemWebSocketHandler() {
return new SystemWebSocketHandler();
}
上面WebSocketconfig
类是对websocket的一些基本配置(其实看类名就知道是配置啦。。。),解释一下:
1、@EnableWebSocket
开启对websocket的支持
2、实现WebSocketConfigurer
接口并实现registerWebSocketHandlers
方法,设置websocket的EndPoint
(端点):前端创建连接时的路由终端
3、设置setAllowedOrigins
为“*”
是为了解决跨域问题
4、使用@Bean
注解注入WebSocketHandler
下文再做解释
到这里websocket的最基本的配置就有了,接下来可以进行具体的接收和推送消息的功能实现了(重点说服务端主动推送)
@Component
public class SystemWebSocketHandler implements WebSocketHandler {
private static ConcurrentHashMap<String, ConcurrentHashMap<String,
WebSocketSession>> users = new ConcurrentHashMap();
private static final String USERIDPARAM = "userId";
private static final String COMPANYPARAM = "companyId";
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String url = session.getUri().toString();
String userKey = RequestUrlQuery.getParam(url, USERIDPARAM);
String companyKey = RequestUrlQuery.getParam(url, COMPANYPARAM);
if (users.get(companyKey) == null) {
ConcurrentHashMap<String, WebSocketSession> userMap = new ConcurrentHashMap<>();
userMap.put(userKey, session);
users.put(companyKey, userMap);
} else {
if (users.get(companyKey).get(userKey) == null) {
users.get(companyKey).put(userKey, session);
}
}
}
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
TextMessage returnMessage = new TextMessage((CharSequence) message.getPayload());
if (StringUtils.equals("dead or alive", ((CharSequence) message.getPayload()).toString())) {
//接收心跳数据并响应
session.sendMessage(new TextMessage("alive"));
}
System.out.println("message: " + returnMessage);
}
public void sendWebsocketMessage(Notification notification) {
WebsocketNotification notice = (WebsocketNotification) notification;
Map<String, List<String>> userCompanyMap = notice.getUserCompanyMap();
Set<String> userCompanykeySet = userCompanyMap.keySet();
Iterator<String> iterator = userCompanykeySet.iterator();
while (iterator.hasNext()) {
String companyId = iterator.next();
List<String> userList = userCompanyMap.get(companyId);
if (users.get(companyId) == null) {
break;
}
if (userList == null || userList.size() == 0) {
continue;
}
userList.forEach(userId -> {
ConcurrentHashMap<String, WebSocketSession> map = users.get(companyId);
if (map.get(userId) != null) {
try {
WebSocketSession socketSession = map.get(userId);
socketSession.sendMessage(new TextMessage(JSONObject.toJSONString(notice.getMessage())));
System.out.println("send success......" + userId);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {}
}
实现WebsocketHandler接口可以根据需要对接收消息的前后做一些处理
1、afterConnectionEstablished
:成功连接后的逻辑,一般时将用户、Session的映射放入生命的ConcurrentHashMap中
2、handleMessage
:服务端接收到前端发送来的消息时或调用该方法,需要对消息进行处理时可以在此方法中进行处理(包括后面提到的心跳包的处理)
3、handleTransportError
、afterConnectionClosed
:连接出现异常或断开连接时会调用的方法
4、sendWebsocketMessage
:向前端推送消息的方法(自己定义的方法、不是WebSocketHandler 接口里的方法),根据ConcurrentHashMap
存储的用户与websocketSession映射找到需要推送的用户,调用WebSocketSession
中的sendMessage方法就可以实现向前端推送消息的功能
5、注意要想调用SystemWebSocketHandler
类自定义的sendWebsocketMessage
方法需要将注入该类的实例(上文在websocketonfig
类里使用@Bean
注解的作用就是在这),使用时用@Autowired注解实例化,像这样
@Autowired
private SystemWebSocketHandler webSocketHandler;
//建议使用消息队列进行推送
webSocketHandler.sendMessage(notification)
到这里服务端就差不多讲完了,下面开始对前端websocket创建连接、心跳机制、断线重连等的配置
推送的消息应该在登录后的所有页面都应该能够被看到,所以应该选择在App.vue
这样的公共页面展示。
//App.vue执行created()方法时应该创建websocket连接(注意过滤登录、注册等页面,因为它们也是在App.vue页面加载、渲染的)
createWebSocket() {
this.$store.dispatch('app/createWebSocket', {
componentRef: this
})
}
对应的js文件
var websocket = null
var lockReconnect = false // 避免ws重复连接
var reconnect_times = 0
// 心跳检测
var heartCheck = {
timeout: 60000, // 60s发一次心跳
timeoutObj: null,
serverTimeoutObj: null,
reset() {
clearTimeout(this.timeoutObj)
clearTimeout(this.serverTimeoutObj)
return this
},
start() {
var self = this
this.timeoutObj = setTimeout(function() {
// 这里发送一个心跳,后端收到后,返回一个心跳消息,
// onmessage拿到返回的心跳就说明连接正常
websocket.send('dead or alive')
self.serverTimeoutObj = setTimeout(function() { // 如果超过一定时间还没重置,说明后端主动断开了
websocket.close() // 如果onclose会执行reconnect,我们执行ws.close()就行了.如果直接执行reconnect 会触发onclose导致重连两次
}, self.timeout)
}, this.timeout)
}
}
createWebSocket({ state, commit }, payload) {
function onOpen(evt) {
heartCheck.reset().start()
console.log('Connected to WebSocket server.')
reconnect_times = 0//重置 重连次数
}
function onClose(evt) {
console.log('Disconnected' + evt)
payload.componentRef.reconnect()
}
function onMessage(evt) {
if (!getToken()) {
closeWebsocket()
return
}
heartCheck.reset().start() // 拿到任何消息都说明当前连接是正常的
console.log('Retrieved data from server: ' + evt.data)
if (evt.data === 'alive') {
return
}
var obj = JSON.parse(evt.data)
//前端收到心跳数据之外的数据,进行的处理——展示消息提示框等
}
function onError(evt) {
//连接错误时重连
console.log('Error occured: ' + evt.data)
closeWebsocket()
payload.componentRef.reconnect()
}
// 监听页面刷新事件,当页面刷新时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常
window.onbeforeunload = function() {
closeWebsocket()
}
function closeWebsocket() {
if (websocket.readyState === WebSocket.OPEN) {
websocket.close()
}
}
try {
//api表时网关前缀
//notification对应websocket服务
var connect_url = `/api/notification/ws?access_token=${getToken()}&userId=${sessionStorage.getItem('userId')}&companyId=${sessionStorage.getItem('companyId')}`
var webSocket_connect_url = `ws://localhost:8000/api/notification/websocket?access_token=${getToken()}&userId=${sessionStorage.getItem('userId')}&companyId=${sessionStorage.getItem('companyId')}`
if ('WebSocket' in window) {
//不适用sockJs的连接方式(浏览器支持websocket时)
websocket = new WebSocket(webSocket_connect_url)
} else {
//使用sockJs连接
websocket = new SockJS(connect_url)
}
websocket.onopen = function(evt) { onOpen(evt) }
websocket.onclose = function(evt) { onClose(evt) }
websocket.onmessage = function(evt) { onMessage(evt) }
websocket.onerror = function(evt) { onError(evt) }
} catch (error) {
payload.componentRef.reconnect()
}
},
reconnect({ state, commit }, payload) {
if (lockReconnect) return
if (reconnect_times <= 5) {
if (!getToken()) { return }
setTimeout(() => {
// 如果断开,每隔一分钟重连一次
console.log('reconnect_times==' + reconnect_times)
lockReconnect = true
payload.componentRef.createStompWebSocket()
reconnect_times++
lockReconnect = false
}, 60000)
}
},
大概就是这样有任何疑问可以评论留言,有任何不足或建议欢迎指教