vue+springboot之原生websocket长连接实现消息推送

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、handleTransportErrorafterConnectionClosed:连接出现异常或断开连接时会调用的方法
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)
    }
  },

大概就是这样有任何疑问可以评论留言,有任何不足或建议欢迎指教

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值