• SpringBoot -- WebSocket实现前后端实时推送数据


    背景

    1. HTTP 协议有一个缺陷:通信只能由客户端发起,HTTP 协议做不到服务器主动向客户端推送信息
    2. WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端
    3. 举例来说,我们想要查询当前的排队情况,只能是页面轮询向服务器发出请求,服务器返回查询结果。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此WebSocket 就是这样发明的。

    SpringBoot的WebSocket

    引入MAVEN依赖
    	  <dependency>  
               <groupId>org.springframework.boot</groupId>  
               <artifactId>spring-boot-starter-websocket</artifactId>  
           </dependency> 
    
    WebSocketConfig

      启用websocket支持很简单,直接一个配置类搞定。

    @Configuration
    public class WebSocketConfig {
    
        @Bean
        public ServerEndpointExporter serverEndpointExporter() {
            return new ServerEndpointExporter();
        }
    
    }
    
    WebSocketServer

      websocket和socket类似,有客户端和服务端,客户端就是pc、app等,服务端就是我们后端了。因为WebSocket是类似客户端服务端的形式(采用ws协议),那么这里的WebSocketServer其实就相当于一个ws协议的Controller,直接使用注解
    @ServerEndpoint(value = “/websocket/{appNo}”)和@Component启用即可,然后在里面实现@OnOpen,@OnClose, @OnMessage,@OnError等方法即可。

    package com.dongzhengafc.facesign.websocket;
    
    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;
    
    /**
     * @Author: TheBigBlue
     * @Description: 向app端实时推送业务状态信息
     * @Date: 2019/7/16
     **/
    //由于是websocket 所以原本是@RestController的http形式
    //直接替换成@ServerEndpoint即可,作用是一样的 就是指定一个地址
    //表示定义一个websocket的Server端
    @Component
    @ServerEndpoint(value = "/websocket/{appNo}")
    public class WebSocketController {
    
        private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketController.class);
    
        /**
         * @Author: TheBigBlue
         * @Description: 加入连接
         * @Date: 2019/7/16
         * @Param appNo: 申请单号
         * @Param relTyp: 关系人类型
         * @Param session:
         * @Return:
         **/
        @OnOpen
        public void onOpen(@PathParam("appNo") String appNo, Session session) {
            LOGGER.info("[" + appNo + "]加入连接!");
            WebSocketUtil.addSession(appNo, session);
        }
    
        /**
         * @Author: TheBigBlue
         * @Description: 断开连接
         * @Date: 2019/7/16
         * @Param appNo:
         * @Param relTyp:
         * @Param session:
         * @Return:
         **/
        @OnClose
        public void onClose(@PathParam("appNo") String appNo, Session session) {
            LOGGER.info("[" + appNo + "]断开连接!");
            WebSocketUtil.remoteSession(appNo);
        }
    
        /**
         * @Author: TheBigBlue
         * @Description: 发送消息
         * @Date: 2019/7/16
         * @Param appNo: 申请单号
         * @Param relTyp: 关系人类型
         * @Param message: 消息
         * @Return:
         **/
        @OnMessage
        public void OnMessage(@PathParam("appNo") String appNo, String message) {
            String messageInfo = "服务器对[" + appNo + "]发送消息:" + message;
            LOGGER.info(messageInfo);
            Session session = WebSocketUtil.ONLINE_SESSION.get(appNo);
            if("heart".equalsIgnoreCase(message)){
                LOGGER.info("客户端向服务端发送心跳");
                //向客户端发送心跳连接成功
                message = "success";
            }
            //发送普通信息
            WebSocketUtil.sendMessage(session, message);
        }
    
        @OnError
        public void onError(Session session, Throwable throwable) {
            LOGGER.error(session.getId() + "异常:", throwable);
            try {
                session.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            throwable.printStackTrace();
        }
    }
    
    
    package com.dongzhengafc.facesign.websocket;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import javax.websocket.RemoteEndpoint.Async;
    import javax.websocket.Session;
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.Future;
    
    /**
     * @Author: TheBigBlue
     * @Description:
     * @Date: 2019/7/16
     **/
    public class WebSocketUtil {
    
        private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketUtil.class);
    
        /**
         * @Author: TheBigBlue
         * @Description: 使用map进行存储在线的session
         * @Date: 2019/7/16
         **/
        public static final Map<String, Session> ONLINE_SESSION = new ConcurrentHashMap<>();
    
        /**
         * @Author: TheBigBlue
         * @Description: 添加Session
         * @Date: 2019/7/16
         * @Param userKey:
         * @Param session:
         * @Return:
         **/
        public static void addSession(String userKey, Session session) {
            ONLINE_SESSION.put(userKey, session);
        }
    
        public static void remoteSession(String userKey) {
            ONLINE_SESSION.remove(userKey);
        }
    
        /**
         * @Author: TheBigBlue
         * @Description: 向某个用户发送消息
         * @Date: 2019/7/16
         * @Param session:
         * @Param message:
         * @Return:
         **/
        public static Boolean sendMessage(Session session, String message) {
            if (session == null) {
                return false;
            }
            // getAsyncRemote()和getBasicRemote()异步与同步
            Async async = session.getAsyncRemote();
            //发送消息
            Future<Void> future = async.sendText(message);
            boolean done = future.isDone();
            LOGGER.info("服务器发送消息给客户端" + session.getId() + "的消息:" + message + ",状态为:" + done);
            return done;
    
        }
    
    }
    
    
    推送消息

      推送消息,可以自己写接口调用,或者前端发起,或者通过第三方工具连接。

    1. 自己写接口调用
    package com.dongzhengafc.facesign.websocket;
    
    import com.dongzhengafc.facesign.base.api.JsonResponse;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * @Author: TheBigBlue
     * @Description: 向客户端推送业务状态信息
     * @Date: 2019/7/16
     **/
    @RestController
    @RequestMapping("/socket")
    public class WebSocketPushController {
    
    	@Autowired
    	private WebSocketController webSocketController;
    
    	/**
    	 * @Author: TheBigBlue
    	 * @Description:
    	 * @Date: 2019/7/16
    	 * @Param appNo: 发送的用户名
    	 * @Param relTyp: 发送的用户名
    	 * @Param message: 发送的信息
    	 * @Return:
    	 **/
    	@RequestMapping("/push")
    	public JsonResponse pushToWeb(String appNo, String message) {
    		webSocketController.OnMessage(appNo, message);
    		return JsonResponse.success();
    	}
    }
    
    
    1. 前端请求连接,发送信息。
      <script> 
        var socket;  
        if(typeof(WebSocket) == "undefined") {  
            console.log("您的浏览器不支持WebSocket");  
        }else{  
            console.log("您的浏览器支持WebSocket");  
            	//实现化WebSocket对象,指定要连接的服务器地址与端口  建立连接  
                //等同于socket = new WebSocket("ws://localhost:8083/checkcentersys/websocket/20");  
                socket = new WebSocket("${basePath}websocket/${cid}".replace("http","ws"));  
                //打开事件  
                socket.onopen = function() {  
                    console.log("Socket 已打开");  
                    //socket.send("这是来自客户端的消息" + location.href + new Date());  
                };  
                //获得消息事件  
                socket.onmessage = function(msg) {  
                    console.log(msg.data);  
                    //发现消息进入    开始处理前端触发逻辑
                };  
                //关闭事件  
                socket.onclose = function() {  
                    console.log("Socket已关闭");  
                };  
                //发生了错误事件  
                socket.onerror = function() {  
                    alert("Socket发生了错误");  
                    //此时可以尝试刷新页面
                }  
                //离开页面时,关闭socket
                //jquery1.8中已经被废弃,3.0中已经移除
                // $(window).unload(function(){  
                //     socket.close();  
                //});  
        }
        </script> 
    
    1. 第三方工具连接:http://www.websocket-test.com/
      在这里插入图片描述
    相关问题
    1. 打war包部署tomcat报错
    Application startup failed
    java.lang.IllegalStateException: Failed to register @ServerEndpoint class: 
    
    • 原因:SpringBoot Run As 可以快速启动项目,且能够即时刷新。其原因是SpringBoot拥有一个内置的Tomcat,此Tomcat的版本可在pom.xml中指定。每次我们使用SpringBoot Run As启动项目时,我们的web容器即就是这个内置的Tomcat。此刻web容器连同项目本身都是由Spring进行代理。而当我们将项目打成war包,部署在服务器上的某个Tomcat下时。此刻我们的项目将会交由这个Tomcat去管理。因为外部Tomcat的优先级高于Spring内置Tomcat。问题就在这里。当我们在IDE内使用 SpringBoot Run As去启动时,Spring会帮我们找到内置Tomcat lib中的javax.websocket包加载使用。所以项目正常运行。而当我们将打好的war包放在外部Tomcat上进行启动时。Tomcat管理器根据之前的Javax.websocket包的路径找不到对应的ServerEndpoint类资源文件,因此自然会注册失败。
    • 解决:pom.xml 引入依赖
      <dependency>
          <groupId>javax.servlet</groupId>
          <artifactId>javax.servlet-api</artifactId>
          <version>3.1.0</version>
      </dependency>
    
    • 部署发现问题仍然存在,这是因为当我们使用外部Tomcat时,项目的管理权将会由Spring交接至Tomcat。 而Tomcat7及后续版本是对websocket直接支持的,且我们所使用的jar包也是tomcat提供的。 但是我们在WebSocketConfig中将ServerEndpointExporter指定给Spring管理。而部署后ServerEndpoint是需要Tomcat直接管理才能生效的。所以此时即就是此包的管理权交接失败,那肯定不能成功了。最后我们需要将WebSocketConfig中的bean配置注释掉。然后再打包上传部署测试。一切正常!
    //@Configuration
    //public class WebSocketConfig {
    //
    //    @Bean
    //    public ServerEndpointExporter serverEndpointExporter() {
    //        return new ServerEndpointExporter();
    //    }
    //
    //}
    
    2. Websocket在1分钟后自动断开连接报错EOFException
    • 这是因为websocket长连接有默认的超时时间(1分钟,由proxy_read_timeout决定),就是超过一定的时间没有发送任何消息,连接会自动断开。解决办法就是让浏览器每隔一定时间(要小于超时时间)发送一个心跳。
    • 或者部署到服务器后,nginx 代理默认配置了访问超时时间为90s,我们可以修改这个值。nginx 通过在客户端和后端服务器之间建立起一条隧道来支持WebSocket。为了使nginx可以将来自客户端的Upgrade请求发送给后端服务器,Upgrade和Connection的头信息必须被显式的设置,一旦我们完成以上设置,nginx就可以处理WebSocket连接了。注意,必须要有proxy_set_header Host h o s t : host:host:server_port; 这个配置,否则会报403错误。
    location /web/count {
            proxy_pass http://tomcat-server;
            proxy_redirect off;
            proxy_http_version 1.1;
    
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
    
            proxy_set_header Host $host:$server_port;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;    
    }
  • 相关阅读:
    前端模板引擎编译
    h5与app混合开发,jsbridge
    vuex
    async await promise
    node端口被占用
    npm工作流 与webpack 分同环境配置
    GraphQL
    mybatis批量删除、插入
    Oracle数据库速查知识文档
    Oracle刷新物化视图
  • 原文地址:https://www.cnblogs.com/xianz666/p/14419001.html
Copyright © 2020-2023  润新知