spring boot + vue 整合 websocket

之前工作中用的服务端主动请求一直用的sse,感兴趣的兄弟可以了解一下,但由于sse兼容性问题最近决定替换为websocket,这里分享一下个人的整合思路,有不对的欢迎兄弟们可以私信或留言。
1.导入依赖

 <!--websocket-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

2.开启websocket配置

package com.yl.framework.websocket;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * websocket 配置
 * 开启websocket
 * @author shg
 */
@Configuration
public class WebSocketConfig
{
    @Bean
    public ServerEndpointExporter serverEndpointExporter()
    {
        return new ServerEndpointExporter();
    }
}

3.websocketServer,用于和客户端交互

package com.yl.framework.websocket;

import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import com.alibaba.fastjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

/**
 * websocket 消息处理
 *
 * @author shg
 */
@Component
@ServerEndpoint("/websocket/message/{webSocketKey}")
public class WebSocketServer {
    /**
     * WebSocketServer 日志控制器
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketServer.class);

    /**
     * 默认最多允许同时在线人数100
     */
    public static int socketMaxOnlineCount = 100;

    /**
     * 用户集
     */
    private static Map<String, Session> USERS = new ConcurrentHashMap<String, Session>();

    private static Semaphore socketSemaphore = new Semaphore(socketMaxOnlineCount);

    /**
     * 存储用户
     *
     * @param key 唯一键
     * @param session 用户信息
     */
    public static void putUser(String key, Session session)
    {
        USERS.put(key, session);
    }

    /**
     * 移除用户
     *
     * @param session 用户信息
     *
     * @return 移除结果
     */
    public static boolean removeBySession(Session session)
    {
        String key = null;
        boolean flag = USERS.containsValue(session);
        if (flag)
        {
            Set<Map.Entry<String, Session>> entries = USERS.entrySet();
            for (Map.Entry<String, Session> entry : entries)
            {
                Session value = entry.getValue();
                if (value.equals(session))
                {
                    key = entry.getKey();
                    break;
                }
            }
        }
        else
        {
            return true;
        }
        return removeUserByKey(key);
    }

    /**
     * 移出用户
     *
     * @param key 键
     */
    public static boolean removeUserByKey(String key)
    {
        LOGGER.info("\n 正在移出用户 - {}", key);
        Session remove = USERS.remove(key);
        if (remove != null)
        {
            boolean containsValue = USERS.containsValue(remove);
            LOGGER.info("\n 移出结果 - {}", containsValue ? "失败" : "成功");
            return containsValue;
        }
        else
        {
            return true;
        }
    }

    /**
     * 获取在线用户列表
     *
     * @return 返回用户集合
     */
    public static Map<String, Session> getUsers()
    {
        return USERS;
    }

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("webSocketKey") String webSocketKey) throws Exception {
        //判断是否建立过连接
        if (Objects.nonNull(USERS.get(webSocketKey))) {
            return;
        }

        // 尝试获取信号量
        boolean semaphoreFlag = SemaphoreUtils.tryAcquire(socketSemaphore);
        if (!semaphoreFlag) {
            // 未获取到信号量
            LOGGER.error("\n 当前在线人数超过限制数- {}", socketMaxOnlineCount);
            WebSocketUtil.sendTextMessageToUserBySession(session, new WebSocketMessage("error", "当前在线人数超过限制数:" + socketMaxOnlineCount));
            session.close();
        } else {
            // 添加用户
            //WebSocketUsers.put(session.getId(), session);
            //以webSocketKey作为唯一标识,登录时颁发并存到客户端的localStorage
            putUser(webSocketKey, session);
            LOGGER.info("\n 建立连接 - {}", session);
            LOGGER.info("\n 当前人数 - {}", USERS.size());
            WebSocketUtil.sendTextMessageToUserBySession(session, new WebSocketMessage("heartbeat", "pong"));
        }
    }

    /**
     * 连接关闭时处理
     */
    @OnClose
    public void onClose(Session session, @PathParam("webSocketKey") String webSocketKey) {
        LOGGER.info("\n 关闭连接 - {}", session);
        // 移除用户
        removeUserByKey(webSocketKey);
        // 获取到信号量则需释放
        SemaphoreUtils.release(socketSemaphore);
    }

    /**
     * 抛出异常时处理
     */
    @OnError
    public void onError(@PathParam("webSocketKey") String webSocketKey, Session session, Throwable exception) throws Exception {
        if (session.isOpen()) {
            // 关闭连接
            session.close();
        }
        LOGGER.info("\n 连接异常 - {}", webSocketKey);
        LOGGER.info("\n 异常信息 - {}", webSocketKey);
        // 移出用户
        removeUserByKey(webSocketKey);
        // 获取到信号量则需释放
        SemaphoreUtils.release(socketSemaphore);
    }

    /**
     * 服务器接收到客户端消息时调用的方法
     */
    @OnMessage
    public void onMessage(@PathParam("webSocketKey") String webSocketKey, String message, Session session) {
        WebSocketMessage socketMessage = JSONObject.parseObject(message, WebSocketMessage.class);

        //判断该次请求的消息类型是心跳检测还是获取信息
        if (socketMessage.getType().equals("heartbeat")) {
            //立刻向前台发送消息,代表后台正常运行
            WebSocketUtil.sendTextMessageToUserByWebSocketKey(webSocketKey, new WebSocketMessage("heartbeat", "ok"));
        } else if (socketMessage.getType().equals("message")) {
            //收到消息,执行业务逻辑......
        }
    }


}

这里核心参数是websocketKey,因为业务中允许同一账号多设备登录的情况我这里的websocketKey采用:用户名 + ‘_’ +uuid的形式生成。
4. 信号量工具类和消息推送工具类

package com.yl.framework.websocket;

import java.util.concurrent.Semaphore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 信号量相关处理
 *
 * @author shg
 */
public class SemaphoreUtils
{
    /**
     * SemaphoreUtils 日志控制器
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(SemaphoreUtils.class);

    /**
     * 获取信号量
     *
     * @param semaphore
     * @return
     */
    public static boolean tryAcquire(Semaphore semaphore)
    {
        boolean flag = false;

        try
        {
            flag = semaphore.tryAcquire();
        }
        catch (Exception e)
        {
            LOGGER.error("获取信号量异常", e);
        }

        return flag;
    }

    /**
     * 释放信号量
     *
     * @param semaphore
     */
    public static void release(Semaphore semaphore)
    {

        try
        {
            semaphore.release();
        }
        catch (Exception e)
        {
            LOGGER.error("释放信号量异常", e);
        }
    }
}

package com.yl.framework.websocket;

import com.alibaba.fastjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.websocket.Session;
import java.io.IOException;
import java.util.Collection;

/**
 * websocket 工具类
 *
 * @author shg
 */
public class WebSocketUtil {
    /**
     * WebSocketUsers 日志控制器
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketUtil.class);

    /**
     * 根据session发送文本消息到客户端
     *  @param session
     * @param message 消息内容
     */
    public static void sendTextMessageToUserBySession(Session session, WebSocketMessage message)
    {
        if (session != null)
        {
            try
            {
                session.getBasicRemote().sendText(JSONObject.toJSONString(message));
            }
            catch (IOException e)
            {
                LOGGER.error("\n[发送消息异常]", e);
            }
        }
        else
        {
            LOGGER.info("\n[你已离线]");
        }
    }

    /**
     * 根据webSocketKey发送文本消息到客户端
     *  @param webSocketKey 用户名 + uuid
     * @param message 消息内容
     */
    public static void sendTextMessageToUserByWebSocketKey(String webSocketKey, WebSocketMessage message)
    {
        Session session = WebSocketServer.getUsers().get(webSocketKey);
        if (session != null)
        {
            try
            {
                session.getBasicRemote().sendText(JSONObject.toJSONString(message));
            }
            catch (IOException e)
            {
                LOGGER.error("\n[发送消息异常]", e);
            }
        }
        else
        {
            LOGGER.info("\n[你已离线]");
        }
    }

    /**
     * 群发消息文本消息
     *
     * @param message 消息内容
     */
    public static void sendAllUserMessage(WebSocketMessage message)
    {
        Collection<Session> values = WebSocketServer.getUsers().values();
        for (Session value : values)
        {
            sendTextMessageToUserBySession(value, message);
        }
    }

}

5.测试推送消息和文件,这里我的文件是放本地的,可以按需更改

package com.yl.framework.websocket;

import cn.hutool.core.io.IoUtil;
import com.yl.common.core.domain.AjaxResult;
import com.yl.common.utils.Base64Util;
import com.yl.framework.util.Byte2InputStream;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Date;
/**
 * websocket模拟服务端向客户端发送文本和文件
 *
 * @author shg
 */

@RestController
public class WebSocketTestController {

    /**
     * webSocket后端主动发送文本
     *
     * @param webSocketKey 从前端的localStorage获取
     */
    @GetMapping("/test/sendTextMessage/{webSocketKey}")
    public AjaxResult sendTextMessage(@PathVariable("webSocketKey") String webSocketKey) {
        //正常消息类型(message)
        WebSocketUtil.sendTextMessageToUserByWebSocketKey(webSocketKey, new WebSocketMessage("message", "测试消息" + new Date().getTime()));

        return AjaxResult.success();
    }

    /**
     * webSocket后端主动发送文件
     *
     * @param webSocketKey 从前端的localStorage获取
     */
    @GetMapping("/test/sendFileMessage/{webSocketKey}/{fileName}")
    public AjaxResult sendFileMessage(@PathVariable("webSocketKey") String webSocketKey, @PathVariable("fileName") String fileName) throws Exception{
        //正常消息类型(message)
        InputStream inputStream = new FileInputStream("F:\\file\\" + fileName);
        byte[] bytes = Byte2InputStream.inputStream2byte(inputStream);

        String str = Base64Util.encodeBase64(bytes);
        WebSocketUtil.sendTextMessageToUserByWebSocketKey(webSocketKey, new WebSocketMessage("message", str));

        return AjaxResult.success();
    }


}

到这里后端就算整合完毕了,前端整合如下:
1、登录时将websocket放到localsotrge,退出时清除:

// 登录
   window.localStorage.setItem("webSocketKey",res.webSocketKey);
        

// 退出系统
  window.localStorage.removeItem("webSocketKey")
         

2、这里是将核心代码封装了一个工具类,将消息分为message(业务消息)和heartbeat(心跳消息两种),js代码如下:

//暴露自定义websocket对象
export const socket = {
  //后台请求路径
  url: "",
  //websocket对象
  websocket: null,
  //websocket状态
  websocketState: false,
  //重新连接次数
  reconnectNum: 0,
  //重连锁状态,保证重连按顺序执行
  lockReconnect: false,
  //定时器信息
  timeout: null,
  clientTimeout: null,
  serverTimeout: null,
  //初始化方法,根据url创建websocket对象封装基本连接方法,并重置心跳检测
  initWebSocket(newUrl) {
    socket.url = newUrl;
    socket.websocket = new WebSocket(socket.url);
    socket.websocket.onopen = socket.websocketOnOpen;
    socket.websocket.onerror = socket.websocketOnError;
    socket.websocket.onclose = socket.websocketOnClose;
    this.resetHeartbeat()
  },
  reconnect() {
    //判断连接状态
    if (socket.lockReconnect) return;
    socket.reconnectNum += 1;
    //重新连接三次还未成功调用连接关闭方法
    if (socket.reconnectNum === 3) {
      socket.reconnectNum = 0;
      socket.websocket.onclose()
      return;
    }
    //等待本次重连完成后再进行下一次
    socket.lockReconnect = true;
    //5s后进行重新连接
    socket.timeout = setTimeout(() => {
      socket.initWebSocket(socket.url);
      socket.lockReconnect = false;
    }, 5000);
  },
  //重置心跳检测
  resetHeartbeat() {
    socket.heartbeat();
  },
  //心跳检测
  heartbeat() {
    socket.clientTimeout = setTimeout(() => {
      if (socket.websocket) {
        //向后台发送消息进行心跳检测
        socket.websocket.send(JSON.stringify({ type: "heartbeat" }));
        socket.websocketState = false;
        //一分钟内服务器不响应则关闭连接
        socket.serverTimeout = setTimeout(() => {
          if (!socket.websocketState) {
            socket.websocket.onclose()
          } else {
            this.resetHeartbeat()
          }
        }, 60 * 1000);
      }
    }, 3 * 1000);
  },
  //发送消息
  sendMsg(message) {
    socket.websocket.send(JSON.stringify({ type: "message", message: message}));
  },
  websocketOnOpen(event) {
    //连接开启后向后台发送消息进行一次心跳检测
    socket.websocket.send(JSON.stringify({ type: "heartbeat" }));
  },
  websocketOnError(error) {
    console.log(error);
    socket.reconnect();
  },
  websocketOnClose() {
    socket.websocket.close();
  },
};

3.引入websocket.js

//引入socket对象
import { socket } from "@/utils/websocket";
//引入base64生成文件工具类
import Base64StrToFileUtil from "@/utils/Base64StrToFileUtil";

4.使用

created() {
    this.getMessageNumber()
    //初始化websocket对象
    socket.initWebSocket(process.env.VUE_APP_BASE_WS + "/" + window.localStorage.getItem("webSocketKey"));
    //绑定接收消息方法
    socket.websocket.onmessage = this.websocketOnMessage;
  },
  beforeDestroy() {
    // 组件销毁时关闭链接释放资源
    socket.websocketOnClose()
  },
  methods: {
    /** 初始化连接测试*/
    init() {
      socket.websocketOnOpen();
    },

    /** 接收websocket消息*/
    websocketOnMessage(event) {
      //初始化界面时,会主动向后台发送一次消息,获取数据
      this.websocketCount += 1;

      //没有发送成功则重新初始化一次
      if (this.websocketCount === 0) {
        this.init();
      }
      let info = JSON.parse(event.data);
      switch (info.type) {
        case "heartbeat":
          socket.websocketState = true;
          break;
        case "message":
          this.loading = true;
          this.$nextTick(() => {
            // 收到文本消息
            this.consumeTextMessage(info)
            
            //收到文件消息
            //this.consumeFileMessage(info);
          })
          break;
        case "error":
          this.loading = false;
          break;
      }
    },

    /** 处理推送的文本数据 */
    consumeTextMessage(info) {
      const h = this.$createElement;
      this.$notify({
        title: '新的通知',
        message: h('i', { style: 'color: teal'}, info.message)
      });
    },

    /** 处理推送的文件数据 这里发送的是xlsx文件,可根据需求自定义,也可以将文件名一起从后端传过来 */
    consumeFileMessage(info) {
      let infoObj = info
      //服务器向客户端推送文件  生成文件
      Base64StrToFileUtil.downloadFileByBase64(infoObj.message, "01.xlsx")
    }
  }

5、我这里发送文件是将文件转base64串的形式发送到前端,前端在生成对应文件,如果需要采用这种方式还需要添加base64转文件工具栏,代码如下:

const util = {
  /** 通过base64字符串生成文件 */
  downloadFileByBase64: function(base64Str, fileName) {
    let myBlob = this.dataURLtoBlob(base64Str)
    let myUrl = URL.createObjectURL(myBlob)
    this.downloadFile(myUrl, fileName)
  },
  /**封装base64Str blob对象*/
  dataURLtoBlob: function (base64Str) {
    let bstr = atob(base64Str), n = bstr.length, u8arr = new Uint8Array(n);
    alert(JSON.stringify(bstr))
    while (n--) {
      u8arr[n] = bstr.charCodeAt(n);
    }
    return new Blob([u8arr]);
  },
  /**创建一个a标签,并做下载点击事件*/
  downloadFile: function (hrefUrl, fileName) {
    let a = document.createElement("a")
    a.setAttribute("href", hrefUrl)
    a.setAttribute("download", fileName)
    a.setAttribute("target", "_blank")
    let clickEvent = document.createEvent("MouseEvents");
    clickEvent.initEvent("click", true, true);
    a.dispatchEvent(clickEvent);
  }
}
export default util

今天的整合就到这了,有觉得还可以的兄弟麻烦给点亮一下,谢谢!

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值