SpringBoot集成WebSocket实现服务器向前端推送数据(全双工通信)
1.场景:
在一次开发中需要实时的向前端发送一些数据,一开始采用了前端轮询访问的方式,每隔一秒发送一次请求,这样虽然能达到项目的需求但是还是有些小问题,当我们的需求增加实时性方面的需求的时候就需要前端做修改,比如说新的需求是只有当前端进入这个页面的点击实时获取数据这个按钮的时候就开始接收消息,这个时候就需要知道前端第一次点击的时间,因为http请求的无状态的特性,没有办法让后端单独实现这个实时的功能,这个时候就需要前端去监听第一次点击的这个状态并记录时间,还要记录用户是否离开页面。所以为了一个简单的实时性功能前端需要额外做这么多的事情。
2. 解决方案:
使用webSocket实现服务器和前端双向通行,当数据有更新的时候后端主动群发给所有连接的用户
3. 实施
3.1依赖(正常情况下只需要这一个依赖就可以实现)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
3.2. 注入webSocket的支持
package com.doudou.websocket;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
import javax.servlet.ServletContext;
/**
* webSocket的配置类, 开启对webSocket的支持
* @author 豆豆
* @date 2019/3/27 15:25
* @flag 以万物智能,化百千万亿身
*/
@Configuration
public class WebSocketConfig {
/*@Bean
这个是为了解决在有些情况下出现serverEndpointExporter为null的情况(StackOverflow上看到的)
public ServletContextAware endpointExporterInitializer(final ApplicationContext applicationContext) {
return new ServletContextAware() {
@Override
public void setServletContext(ServletContext servletContext) {
ServerEndpointExporter serverEndpointExporter = new ServerEndpointExporter();
serverEndpointExporter.setApplicationContext(applicationContext);
try {
serverEndpointExporter.afterPropertiesSet();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
};
}*/
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
3.3. ws的实现类
这个类主要实现了两件事,一件是接收客户端的连接并且保存这些连接,另外一件是当服务器需要向前端发送数据的时候会遍历保存的这些状态,群发消息给连接着的客户端。实现保存这个功能的借助的是java.util.concurrent.CopyOnWriteArraySet,能够保存底层数据状态,并且是线程安全的。类中一些注释掉的代码是我在实际开发中查询数据库的数据。
package com.doudou.websocket;
import lombok.extern.slf4j.Slf4j;
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.Date;
import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* webSocket实现实时抓拍展示
* @author 豆豆
* @date 2019/3/27 15:29
* @flag 以万物智能,化百千万亿身
*/
@ServerEndpoint("/ws/{userId}")
@Slf4j
@Component
public class WebSocketServer {
//这里需要注意我发现在实际的开发中无法注入,解决办法在问题中说明
/*@Autowired
private SnapRecordRepository snapRecordRepository;
@Autowired
private ResourcesService resourcesService;*/
//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<>();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
private String dateStr;
private String userId;
private List<String> cameraIndexCodes;
/**
* 收到请求后调用的方法
* @param session
*/
@OnMessage
public void handleMessage(Session session, String message){
log.info("收到客户端的ws请求");
}
/**
* 连接建立成功调用的方法
* */
@OnOpen
public void onOpen(Session session, @PathParam("userId")String userId) {
log.info("建立webSocket的连接");
this.session = session;
Date date = new Date(System.currentTimeMillis());
//this.dateStr = DateUtil.formatDate(date, "yyyy-MM-dd HH:mm:ss");
this.userId = userId;
//List<String> cameraIndexCodes = resourcesService.getCameraIndexCodeFromXres(ResourcesTypeConstant.CAMERA, userId);
/*if (cameraIndexCodes.size() < 1){
cameraIndexCodes.add("");
}*/
//this.cameraIndexCodes = cameraIndexCodes;
webSocketSet.add(this);
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
log.info("关闭webSocket的连接");
webSocketSet.remove(this);
}
/**
* 发生错误时调用的方法
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误");
error.printStackTrace();
}
/**
* 实现服务器主动推送
*/
public void sendMessage(String message) throws IOException {
webSocketSet.forEach(x -> {
//List<SnapRecordEntity> snapRecordEntities = snapRecordRepository.findByCameraIdInOrderByEventTimeLimit(x.cameraIndexCodes,x.dateStr);
//String message = JSON.toJSONString(BaseResult.success(snapRecordEntities)) ;
try {
x.session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
4. 碰到的问题
4.1. 项目使用undertow作为启动容器,引用的依赖是tomcat-webSocket是不是不可以
(本来直接引入springboot对websocket的支持就可以了,但是公司的父级依赖中排除了一些jboss的依赖所以只能使用替代的这个问题还会导致一些类和注解找不到,所以具体情况按照自己的实际来)?
答案: tomcat-websocket是websocket的一种实现跟容器没有关系。
4.2. 测试demo中ws可以正常连接但是在项目中就是无法访问?
答案:排查是因为单点登录拦截了ws请求。
4.3 . ws的实现类中无法注入一些service数据库访问的repository。
解决:使用ApplicationContext.getBean(“name”);这种形式来获取容器中的bean。
4.4. 如何实现获取application呢?
答案: 可以自定义一个类,实现ApplicationContextAware接口就可以拿到,然后自己包装一个getBean的泛型方法就可以随意获取容器中的bean。
4.5. 获取bean的泛型方法怎么实现?
public static <T> T getBean(String name) {
if (context == null)
throw new IllegalStateException("applicaitonContext未注入");
try {
return (T) context.getBean(name);
} catch (BeansException e) {
e.printStackTrace();
}
return (T) null;
}