xxl-job + webSocket实现数据大屏动态展示
1.weSocket引入
传统的javaweb项目通常使用的是HTTP/HTTPS协议,这是非连接协议,只能由客户端主动向服务端发送请求才能获得服务端的响应并取得相关的数据。当客户端需要实时的数据时可以通过
定时轮询
服务端获取数据,这种方式最显著的缺点是如果客户端数量庞大并且定时轮询间隔较短,那么服务端将承受响应这些客户端海量请求的巨大的压力,而相对更优异的
WebSocket
方案也应运而生。
2.weSocket介绍
WebSocket协议是基于TCP的一种新的网络协议,它实现了浏览器与服务器全双工(full-duplex)通信,其本质是先通过HTTP协议进行一次握手后创建一个用于交换数据的TCP连接,此后服务端与客户端通过此TCP连接进行实时通信——允许服务器主动发送信息给客户端。这里有更直白的解释,可转至知乎:
WebSocket 是什么原理?为什么可以实现持久连接?
3.项目实战
3.1配置xxl-job定时任务
上一篇有对xxl-job的一个详细地介绍和使用教程,这里不再做细讲,可转至:
xxl-job(分布式任务调度平台)的介绍和使用
我们先启动调度中心管理后台,然后配置一个执行器,这里我们定义的appName是:xxl-job-executor-data
然后在任务管理里面新增对应的定时任务,由于要更直观地看到效果,所以将定时规则设置为一秒一次。此时该任务的状态为停止状态
3.2创建webSocket项目
新建SpringBoot项目引入websocket和xxl-job的核心包
<!-- websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- xxl-job-core -->
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.1.2</version>
</dependency>
创建定时任务配置类
@Configuration
public class XxlJobConfig {
@Value("http://127.0.0.1:8080/xxl-job-admin")
private String adminAddresses;
@Value("xxl-job-executor-data")
private String appName;
@Value("")
private String ip;
@Value("9998")
private int port;
@Value("")
private String accessToken;
@Value("D:/data/applogs/xxl-job/jobhandler")
private String logPath;
@Value("30")
private int logRetentionDays;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppName(appName);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
}
XxlJobSpringExecutor
:执行器信息的配置类,实例化并将对应属性赋值后再注入IOC容器,由Spring来管理。
- adminAddresses:调度中心地址
- appName:执行器名称
- port:RPC监听端口
- logPath:本地日志路径
- logRetentionDays:日志保留天数
创建webSocket配置类
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
实例化
ServerEndpointExporter
,表示开启webSocket支持
创建webSocket服务
@ServerEndpoint("/ws/{userId}")
@Component
public class WebSocketServer {
static Log log=LogFactory.get(WebSocketServer.class);
private static int onlineCount = 0;
private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>();
private Session session;
private String userId="";
@OnOpen
public void onOpen(Session session,@PathParam("userId") String userId) {
this.session = session;
this.userId=userId;
webSocketSet.add(this);
addOnlineCount();
log.info("用户连接:"+userId+",当前在线人数为:" + getOnlineCount());
try {
sendMessage("连接成功");
} catch (IOException e) {
log.error("用户:"+userId+",网络异常!!!!!!");
}
}
@OnClose
public void onClose() {
webSocketSet.remove(userId);
subOnlineCount();
log.info("用户退出:"+userId+",当前在线人数为:" + getOnlineCount());
}
@OnMessage
public void onMessage(String message, Session session) {
log.info("用户消息:"+userId+",报文:"+message);
}
@OnError
public void onError(Session session, Throwable error) {
log.error("用户错误:"+this.userId+",原因:"+error.getMessage());
error.printStackTrace();
}
/**
* 实现服务器主动推送
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}
public Session getSession() {
return session;
}
public void setSession(Session session) {
this.session = session;
}
public static CopyOnWriteArraySet<WebSocketServer> getWebSocketSet() {
return webSocketSet;
}
public static void setWebSocketSet(CopyOnWriteArraySet<WebSocketServer> webSocketSet) {
WebSocketServer.webSocketSet = webSocketSet;
}
}
@ServerEndpoint
:用于配置webSocket地址,里面提供了encoders (编码器)和 decoders(解码器)。编码器用于发送请求的时候可以发送Object对象,实则是json数据,解码器用于读入Websocket消息,然后输出java对象
CopyOnWriteArraySet
:concurrent包的线程安全Set,用来存放每个客户端对应的WebSocket对象
Session
:与某个客户端的连接会话,需要通过它来给客户端发送数据
@OnMessage
:收到客户端消息后调用的方法
@OnOpent
:连接建立成功调用的方法
@OnClose
:连接关闭调用的方法
@OnError
:连接发生错误的回调方法
编写定时任务处理逻辑
@Component
public class DataXxlJob {
private static Logger logger = LoggerFactory.getLogger(DataXxlJob.class);
@Autowired
private UserDao userDao;
/**
* dataJobHandler任务
*/
@XxlJob("dataJobHandler")
public ReturnT<String> demoJobHandler(String param) throws Exception {
logger.info("定时任务开始");
List<String> list = userDao.getMessage();
int number = new Random().nextInt(10);
CopyOnWriteArraySet<WebSocketServer> webSocketset = WebSocketServer.getWebSocketSet();
webSocketset.forEach(data->{
try {
//这里使用的10以内的随机数,前提是保证数据库查到的数据不少于10条
data.sendMessage(list.get(number));
} catch (IOException e) {
e.printStackTrace();
}
});
logger.info("定时任务结束");
return new ReturnT<String>(200, "demoJobHandler任务成功");
}
}
@XxlJob
:表示以bean的方式定义执行器
通过遍历set集合调用每一个WebSocket对象的sendMessage方法,实现服务器主动数据推送到各个客户端进行展示。
注:这里的dao数据层代码就不做展示了,因业务而异。
编写webSocket页面
<!DOCTYPE HTML>
<html>
<head>
<title>My WebSocket</title>
</head>
<body>
Welcome<br/>
UserId:<input id="text" type="text" value="${uid!!}" /> <button onclick="closeWebSocket()">Close</button>
<div id="message">
</div>
</body>
<script type="text/javascript">
var websocket = null;
//判断当前浏览器是否支持WebSocket
if('WebSocket' in window){
websocket = new WebSocket("ws://127.0.0.1:9999/demo/ws/"+"${uid!!}");
}
else{
alert('Not support websocket')
}
//连接发生错误的回调方法
websocket.onerror = function(){
setMessageInnerHTML("error");
};
//连接成功建立的回调方法
websocket.onopen = function(event){
setMessageInnerHTML("open");
}
//接收到消息的回调方法
websocket.onmessage = function(event){
setMessageInnerHTML(event.data);
}
//连接关闭的回调方法
websocket.onclose = function(){
setMessageInnerHTML("close");
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function(){
websocket.close();
}
//将消息显示在网页上
function setMessageInnerHTML(innerHTML){
document.getElementById('message').innerHTML = innerHTML;
}
//关闭连接
function closeWebSocket(){
websocket.close();
}
//发送消息
function send(){
var message = document.getElementById('text').value;
websocket.send(message);
}
</script>
</html>
前端会先去判断浏览器是否支持,支持的话再去实例化WebSocket对象,并重写相关事件!
编写Controller层
@RestController
public class DemoController {
@GetMapping("index")
public ModelAndView page(){
ModelAndView mav = new ModelAndView("websocket");
mav.addObject("uid", RandomUtil.randomNumbers(3));
return mav;
}
}
这里是通过ModelAndView实现页面的跳转,并且使用3位的随机数作为每个客户端的userId
3.3测试效果
启动webSocket项目,在调度中心查看注册节点可以看出已监听到执行器地址
那我们就可以启动这个定时任务了
分别在不同的窗口请求:
http://localhost:9999/demo/index
这里测试了两个客户端,分别是721和626,可以看出来数据每隔一秒就会发生变化,并且因为是遍历set集合发送的数据,所以每个客户端同一时刻接收到的数据是一致的,由于录屏时间先后的问题在这里看不到效果,有兴趣的码友可以写个demo感受一下哈!!!
感谢您的阅读,希望对您有所帮助,不足之处也希望多探讨!