springboot集成websocket 清空日志后消息广播通知前端重新连接(二)

前言

这是在页面实时查看日志基础上追加了清空日志文件内容后,广播通知前端重新连接。

什么要清空?

由于java -jar 输出日志文件随着时间越来越大

为什么要通知重连?

清空日志文件之后,socket会读取不到日志内容需要断开重新连接。

一、springboot集成websocket

maven配置:

参考上一章springboot集成websocket 实时输出日志到浏览器(一)_sakyoka的博客-CSDN博客

websocket配置:

package com.sakyoka.test.webscoketlog;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * 
 * 描述:开启WebSocket、注册ServerEndpointExporter实例、开启STOMP协议来传输基于代理
 * @author sakyoka
 * @date 2022年8月14日 2022
 */
@Configuration
@EnableWebSocket
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
    
	@Override
	public void registerStompEndpoints(StompEndpointRegistry registry) {
		//添加一个stomp协议的endpoint
		registry.addEndpoint("/server").withSockJS();
	}
	
	@Override
	public void configureMessageBroker(MessageBrokerRegistry registry) {
		//添加一个topic代理节点
		registry.enableSimpleBroker("/topic");
	}
}

这次在原来基础上开启了stomp socket代理,注册了一个server的endpoint和一个/topic代理节点

二、业务代码

控制层代码:

package com.sakyoka.test.webscoketlog;

import java.io.File;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;

import com.sakyoka.test.utils.FileUtils;

import lombok.extern.log4j.Log4j;

/**
 * 
 * 描述:log控制层
 * @author sakyoka
 * @date 2022年8月14日 上午11:28:25
 */
@RequestMapping("/log")
@Controller
@Log4j
public class LogController {
	
	@Autowired
	SimpMessagingTemplate simpMessagingTemplate;

	private int count = 0;
	
	@GetMapping("/logconsole")
	public ModelAndView logPage() {
		return new ModelAndView("/logconsole/logconsole");
	}
	
	@GetMapping("/testlog")
	@ResponseBody
	public String testlog() {
		String string = "测试数据:" + count;
		log.info(string);
		count++;
		return string;
	}
	
	@GetMapping("/emptylog")
	@ResponseBody
	public String emptylog(@RequestParam(value =  "jarId", required = false)String jarId) {
		if (WebSocketLog.SYSTEM_FLAG.equals(jarId)) {
			FileUtils.writeFile(new File("D:\\system.log"), "", false);
			log.info("清空日志内容 jarId:" + jarId);
			simpMessagingTemplate.convertAndSend("/topic/emptylog", jarId);
			return "OK";
		}else {
			//暂时没有其它jar 测试
			return "not found jarId:" + jarId;
		}
	}
}

这次添加了一个emptylog的主动清除日志内容的接口测试。

页面代码:

<%@ page contentType="text/html; charset=UTF-8" %>
<!DOCTYPE html>
<html>
<head> 
    <title>jarLog</title>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
	<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
	<meta name="renderer" content="webkit">
    <jsp:include page="/WEB-INF/views/common/commonstatic.jsp" flush="true" />
    <!-- socketjs插件 -->
    <script type="text/javascript" src="${root}/components/socketjs/sockjs.js"></script>
    <script type="text/javascript" src="${root}/components/socketjs/stomp.js"></script>
    <!-- jquery菜单 -->
    <link rel="stylesheet" type="text/css" href="${root}/components/jquery/jquery.contextMenu.css"/>
    <script type="text/javascript" src="${root}/components/jquery/jquery.contextMenu.js"></script>
</head>
<body>
<div style="background-color:black;width:99%; height:500px; padding: 10px" id="console-parent">
    <div id="console" style="width:99%; height:95%; color:white;overflow-y: auto; overflow-x:hidden;"></div>
</div>
</body>
<script type="text/javascript" src="${root}/js/console.js"></script>
<script type="text/javascript" src="${root}/js/console-websocket.js"></script>
<script type="text/javascript" src="${root}/js/console-websocket-stomp.js"></script>
<script type="text/javascript">
//var port = window.location.port;//如果经过代理?这个经过网关是网关的端口
//var port = "${pageContext.request.serverPort}";//这个才是后端端口
var jarWebSocket;
var jarConsole;
//jarId根据实际定义传过来,现在只有本系统的日志打印可以定义为system,其它jar的可以定义一个uuid关联标识
var jarId = "system";
//@ServerEndpoint("/log")
var wsurl = 'ws://'+ ip +':'+ port + root +'/log?jarId=' + jarId;
//registry.addEndpoint("/server").withSockJS();
var serverurl = 'http://'+ ip +':'+ port + root + '/server';
$(function(){
	//添加console-parent内容变化,调整滚动条位置,自动滚动最下面
	$("#console").bind("DOMNodeInserted",function(e){
	 	var height = $(this).prop("scrollHeight");
	 	$(this).animate({scrollTop: height}, 10);		
	});
	
	registerConsoleLogSocket();
	
	registerEmptyLogSocket();
	
	addRightClickListener();
});

/**
 * 注册控制台打印的socket事件
 */
function registerConsoleLogSocket(){
	jarConsole = new JarConsole();
	jarConsole.load('console');
    jarWebSocket= new JarWebSocket({
		url: wsurl,
		//获取后台返回信息
		onmessage: function(event){
			jarConsole.fill(event.data);
		}
	
	}).addEventListener();
}

/**
 * 注册清空日志的socket事件,采用消息订阅形式
 */
function registerEmptyLogSocket(){
	new StompSocketDefine({
		serverUrl: serverurl,
		subscribes: [{
			subscribeUrl: '/topic/emptylog',
			onmessage: function(res){
				//后端通知需要重新连接
				if (res.body == jarId && jarWebSocket.isConnect()){
					jarWebSocket.close();
					jarWebSocket.reset().reconnect();	
				}
			}
		}]
	}).connect();
}

/**
 * 添加右键菜单
 */
function addRightClickListener(){
	var items = {
	         "clear": {
	        	 name: "清空控制台信息", 
	        	 callback: function(){
	        		 jarConsole.clear();
	          }},
	         'emptyLog': {
	        	 name: "清空日志文件内容",
	        	 callback: function(){
	        		 var url = root + '/log/emptylog';
	        		 $.get(url, {jarId: jarId}, function(res){
	        			 console.log("emptylog result >>> " + res);
	        		 });
	        	 }
	         }
    };
	
	//右键菜单
    $.contextMenu({
        selector: '#console', 
        events:{preShow: function(){}},
        items: items
    });	
}
</script>
</html>	

registerEmptyLogSocket注册/server,订阅/topic/emptylog方法,后端清空的消息发送到这个路由上,接到对应jar的控制台断开重新连接;addRightClickListener 右键菜单,为了方便测试,提供的菜单按钮

封装的根据SockJS和stomp 封装的socket配置对象:

/**
 * add by sakyoka 2022-08-26
 * stomp协议的scoket
 * 
 */
var StompSocketDefine = function(config){
	
	/**配置数据对象 */
	config = config || {};
	/**server 地址*/
	var serverUrl = config.serverUrl;
	/**订阅信息集合*/
	var subscribes = config.subscribes || [];
	/**创建socket*/
	var socket = new SockJS(serverUrl);
	/**使用stomp协议*/
	var stompClient = Stomp.over(socket);
	/**是否自动重连 */
	var autoConnect = config.autoConnect || true;
	/**判断是否主动关闭*/
	var driving = false; 
	/** 尝试重新连接次数*/
	var defaultFailTryConnTimes = config.failTryConnTimes || 5;
	/**尝试累加次数 */
	var tryConnTimes = 0;
	/**是否连接中 */
	var isConnect = false;
	var _this = this;
	
	/**
	 * 恢复初始状态值
	 */
	this.reset = function(){
		autoConnect = config.autoConnect || true;
		driving = false; 
		defaultFailTryConnTimes = config.failTryConnTimes || 5;
		tryConnTimes = 0;
		isConnect = false;
	}
	
	/**
	 * 获取client
	 */
	this.getStompClient = function(){
		return stompClient;	
	}
	
	/**
	 * 断开连接
	 */
	this.disconnect = function(){
		driving = true;
		if (stompClient != undefined){
			stompClient.disconnect();
		}
		return this;
	}
	
	/**
	 * 连接
	 */
	this.connect = function(){
		
		/**
		 * 链接服务
		 */
		stompClient.connect({}, function(frame){
			isConnect = true;
			driving = false;
			tryConnTimes = 0;
			console.log("stomp socket link state: "+ frame);
			/**
			 * 遍历订阅地址
			 */
			for (var i in subscribes){
				var subscribeObject = subscribes[i];
				var subscribeUrl = subscribeObject.subscribeUrl;
				var onmessage = subscribeObject.onmessage;
				stompClient.subscribe(subscribeUrl, function(res){
					if (onmessage){
						onmessage(res);
					}
				});
			}
			
		}, function(){	
			isConnect = false;
			console.log('stomp socket link error');
			if (isConnect === false 
			         && autoConnect === true 
			         && driving === false){
				_this.tryToConnect();
			}
		});	
		
		return this;
	}
	
	/**
	 * 尝试连接
	 */
	this.tryToConnect = function(){
		console.log('stomp socket try to connect...');
		socket = new SockJS(serverUrl);
		stompClient = Stomp.over(socket);
	    var _tryToConnect = function (){
		
			tryConnTimes += 1;
			if (defaultFailTryConnTimes < tryConnTimes){
				console.log('stomp socket try to connect times is more than max times:' + tryConnTimes);
				return ;
			}
			
			if (isConnect === true){
				return ;
			}
			
			_this.connect();
			
			setTimeout(function(){
				_tryToConnect();
		    }, 3000);
		}
		
		_tryToConnect();
	}
	
	/**
	 * 刷新页面前把连接关掉
	 */
	window.onbeforeunload = function(){
		
		driving = true;
		
		if (stompClient != undefined){
			stompClient.disconnect();
		}
	}
}  

三 测试

首先打开两个窗口

 

然后右键点击清空日志内容

 结果:

 

 可以看到两个窗口都有收到消息来自jarId为system,然后业务端口socket重新连接日志输出。

这就是消息广播到两个控制台了。

总结

1、配代理时候一开始配置topic 收不到消息,后面改成/topic就收到了registry.enableSimpleBroker("/topic");,可能是因为我没有配置前缀原因,待测。

2、日志清空在linux不存在文件多进程占用问题,但在window时候会有多进程问题,进程占用导致操作文件失败。

业务拓展

其实这种主动请求要后端广播发消息是其中一个应用例子,另外还可以是后端定时任务,定时清空后发送消息通知前端,就是主动与被动获取消息。

附件

springboot+websocket实例:实时读取日志升级版(追加消息广播,清空日志通知重新连接)-Java文档类资源-CSDN下载


版权声明:本文为qq_16430873原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。