Spring应用-2-SpringBoot+WebSocket实现后端向前端主动发送信息

1. 前言

  1. 需求:
    客户端启动后在执行某些命令式,需要长期、持续、及时的获取服务端产生的日志数据。此时就需要用到WebSocket的后端主动往指定前端发送消息的技术来实现。
  2. 技术原理:
    下面引用文章的大佬已经给出讲解,这里不过多赘述。
  3. 前置
    本文是基于 《Spring应用-1-SpringBoot+Thymeleaf+Vue快速搭建前台项目》 扩展实现的。
  4. 鸣谢

2. 引入和配置

  1. SpringBoot已经对WebSocket技术实现了自动配置和版本管理,只要导入标签即可。
    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    
  2. 启用WebSocket
    光引入了WebSocket是不够的,还需要在项目中开启。创建一个名为WebSocketConfig的配置类。
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.socket.server.standard.ServerEndpointExporter;
    
    @Configuration
    public class WebSocketConfig {
        @Bean
        public ServerEndpointExporter serverEndpointExporter() {
            return new ServerEndpointExporter();
        }
    }
    

3. 后端工具类方法

  1. 开启了WebSeocket功能,也需要一系列的方法来使用。创建名为WebSocketServer的业务类,记录连接的客户端、处理客户端发来的数据和往客户端发送数据。
    import org.springframework.stereotype.Component;
    import java.io.IOException;
    import java.util.concurrent.ConcurrentHashMap;
    import javax.websocket.OnClose;
    import javax.websocket.OnError;
    import javax.websocket.OnMessage;
    import javax.websocket.Session;
    import javax.websocket.server.PathParam;
    import javax.websocket.server.ServerEndpoint;
    
    @Component
    // 类似@RequestMapping注解,只不过这个只支持ws协议,不用http/https
    // 这里的id是发送请求时url最后标记客户端的标识,说明这是一个ws的通用入口,内部不会细分
    @ServerEndpoint("/wsserver/{id}")
    public class WebSocketServer {
        // 日志一定要有,这里用的是log4j2+slf4j
        private static Logger logger = LoggerFactory.getLogger(WebSocketServer.class);
        // 连接数量(感觉可有可无,目前并无用到数量限制之类的)
        private static int onlineCount = 0;
        // 线程安全的方式存放客户端对象
        private static ConcurrentHashMap<String, WebSocketServer> webSocketMap = new ConcurrentHashMap<>();
        // 存储客户端会话
        private Session session;
        // 记录会话ID
        private String id = "";
    
        // 创建连接
        @OnOpen
        public void onOpen(Session session, @PathParam("id") String id) {
            // 记录当前请求连接的客户端
            this.session = session;
            this.id = id;
            // 如果在已登陆列表中找到,需要删除老session信息,重新记录
            if (webSocketMap.containsKey(id)) {
                webSocketMap.remove(id);
                webSocketMap.put(id, this);
            } else {
                // 全新用户直接填充
                webSocketMap.put(id, this);
                this.addOnlineConunt();
            }
            logger.info("WebSocket:用户[" + id + "]已连接到WS服务器!");
            // 此时可以给客户端发送连接成功的信息
            try {
                JSONObject result = new JSONObject();
                result.put("type", "register");
                // 发送消息
                sendMessage(result.toJSONString);
            } catch (IOException e) {
                logger.error("WebSocket:用户网络异常,发送失败!");
            }
        }
    
        // 接收客户端消息
        @OnMessage
        public void onMessage(String message, Session session) {
            logger.info("WebSocket:接收到用户[" + id + "]发送消息,报文:" + message);
            if (StringUtils.isNotBlank(message)) {
                // 解析报文内容,是发给服务端的还是发给其他客户端的
                try {
                    JSONObject msgJo = JSON.parseObject(message);
                    if (StringUtils.isNotBlank(msgJo.getString("toId")) {
                        // 发给其他客户端的
                        msgJo.put("fromId", this.id);
                        String toId = msgJo.getString("toId");
                        if (webSocketMap.containsKey(toId)) {
                            webSocketMap.get(toId).sendMessage(msgJo.toJSONString());
                        } else {
                            logger.error("WebSocket:目标ID[" + toId + "]不存在,发送失败!");
                        }
                    } else {
                        // 发给服务端的
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    
        // 服务端主动推送信息
        public void sendMessage(String message) throws IOException {
            this.session.getBasicRemote().sendText(message);
        }
    
        // 向指定客户端发送消息
        public static void sendInfo(String message, @PathParam("id") String id) throws IOException {
            if (StringUtils.isNotBlank(id) && webSocketMap.containsKey(id)) {
                webSocketMap.get(id).sendMessage(message);
            } else {
                logger.error("WebSocket:目标ID[" + id + "]不存在,发送失败!")
            }
        }
    
        // 异常处理
        @OnError
        public void onError(Session session, Throwable error) {
            logger.error("WebSocket:用户错误,原因:" + error.toString());
            error.printStackTrace();
        }
    
        // 客户端主动断开连接
        @OnClose
        public void onClose() {
            if (webSocketMap.containsKey(id)) {
                webSocketMap.remove(id);
                subOnlineCount();
            }
            logger.info("WebSocket:用户[" + id + "]已断开WS服务器连接!");
        }
    
        // 在线数量
        public static synchronized int getOnLineCount() {
            return onlineCount;
        }
    
        // 增加在线数量
        public static synchronized void addOnlineCount() {
            WebSocketServer.onLineCount++;
        }
    
        // 减少在线数量
        public static synchronized void subOnlineCount() {
            WebSocketServer.onLineCount--;
        }
    }
    
  2. 使用也很简单,因为是静态方法,直接使用即可。
    public void testSendMsg() {
        JSONObject msg = new JSONObject();
        msg.put("type", "test");
        msg.put("msg", "服务端测试发送信息");
        try {
            WebSocketServer.sendInfo(msg.toJSONString(), "1");
        } catch (IOException e) {
            logger.error("WebSocket:发送测试消息失败!原因:" + e.toString());
        }
    }
    

4. 客户端应用

这里的示例是基于Vue的代码示例。

new Vue({
    el: '#app',
    data: {
        // 事先需要向服务端请求一个id,来唯一标识客户端
        id: '',
        // 服务端地址,一般都是网关的地址
        wsUrl: 'ws://127.0.0.1:8081/wsserver/',
        // WebSocket实例
        webSocket: null
    },
    methods: {
        // 初始化WebSocket
        initWebSocket() {
            if (typeof WebSocket === 'undefined') {
                this.$message.error('Error:浏览器不支持WebSocket!');
                return;
            }
            // 创建WebSocket实例,创建以前先检查一下id是否获取到
            this.webSocket = new WebSocket(this.wsUrl + this.id);
            // 给WebSocket实例提供实现方法
            this.webSocket.onmessage = this.webSocketOnMessage;
            this.webSocket.onopen = this.webSocketOnOpen;
            this.webSocket.onerror = this.webSocketOnError;
            this.webSocket.onclose = this.webSocketClose;
        },
        // 连接WebSocket服务器
        webSocketOnOpen() {
            this.$message({
                message: 'WebSocket服务器连接成功!',
                type: 'success'
            });
        },
        // 连接失败时进行重连
        webSocketOnError() {
            this.initWebSocket();
        },
        // 接收到数据
        webSocketOnMessage(res) {
            const resData = JSON.parse(res.data);
            // 根据收到的信息做不同的操作
        },
        // 向WebSocket服务器发送信息
        webSocketSend(data) {
            this.webSocket.send(data);
        },
        // 关闭WebSocket连接
        webSocketClose(e) {
            this.$message.error('WebSocket服务器连接断开!');
        },
        // 模拟发送
        testSendMsg() {
            let msg = {msg: '这是一条测试信息,发给服务器', toId: ''};
            this.webSocketSend(JSON.stringify(msg));
        }
    },
    created: function () {
        // 同步处理先获取id,再创建WebSocket连接
        this.initWebSocket();
    },
    destroyed: function () {
        // 在Vue销毁前断开WebSocket服务器连接
        this.webSocket.close();
    }
})
posted @ 2022-07-22 15:32  苍凉温暖  阅读(2367)  评论(0)    收藏  举报