Vue+cli+Springboot2.2 整合websocket

强化学习帖子,参考原帖:SpringBoot2.0集成WebSocket,实现后台向前端推送信息

websocket是什么:
 	WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。
在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,
并进行双向数据传输。
	websocket通信模型(图片和定义均来自百度百科):

为什么需要websocket:
	很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的的时间间隔(如每1秒),
由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。
这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,
其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
	而比较新的技术去做轮询的效果是Comet。这种技术虽然可以双向通信,但依然需要反复发出请求。
而且在Comet中,普遍采用的长链接,也会消耗服务器资源。
	在这种情况下,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
webscoket兼容性

1,websocket各个浏览器的兼容性如下图
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实时推送到前台。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值