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;    
}
原文地址:https://www.cnblogs.com/xianz666/p/14419001.html