在springboot中用webSocket实时将路径里的日志文件输出到web页面上,并且可以切换多种级别日志

一、先了解webSocket的事件触发机制

websocket是html5规范中的一个部分,它借鉴了socket这种思想,为web应用程序客户端和服务端之间提供了一种全双工通信机制。同时,它又是一种新的应用层协议,websocket协议是为了提供web应用程序和服务端全双工通信而专门制定的一种应用层协议,通常它表示为:ws://echo.websocket.org/?encoding=text HTTP/1.1,可以看到除了前面的协议名和http不同之外,它的表示地址就是传统的url地址。它其实是一个新协议,跟HTTP协议基本没有关系,只是为了兼容现有浏览器的握手规范而已,也就是说它是HTTP协议上的一种补充

WebSocket API是纯事件驱动的。应用程序代码监听WebSocket对象上的事件,以便处理输入数据和连接状态的改变。WebSocket协议也是事件驱动的。客户端应用程序不需要轮询服务器来得到更新的数据。消息和事件将在服务器发送它们的时候异步到达。

WebSocket遵循异步编程模式,只要WebSocket连接打开,应用程序就能简单地监听事件。客户端不需要主动轮询服务器得到更多的信息。要开始监听事件,只要为WebSocket对象添加回调函数。

WebSocket对象有4个不同的事件:open,message,error,close。
有个只读属性 bufferedAmount 已被 send() 放入正在队列中等待传输,但是还没有发出的 UTF-8 文本字节数。
有个只读属性 readyState 表示连接状态,可以是以下值:
0 - 表示连接尚未建立。
1 - 表示连接已建立,可以进行通信。
2 - 表示连接正在进行关闭。
3 - 表示连接已经关闭或者连接不能打开。

1.open

一旦服务器响应了WebSocket连接请求,open会触发并建立一个连接,open事件对应的回调函数称作onopen。
到open事件触发时,协议握手已经完成,WebSocket已经准备好发送和接收数据。如果应用程序接收到一个open事件,那么 可以确定WebSocket服务器成功地处理了连接请求,并且同意与应用程序通信。

		//连接成功建立的回调方法
        websocket.onopen = function () {
    
    
            websocket.binaryType = 'arraybuffer';
			var typedArray = new Uint8Array(4);
			// Send binary data
			websocket.send(typedArray.buffer);
            console.log("WebSocket连接成功")
        };		

2.message

WebSocket消息包含来自服务器的数据。你也可能听说过组成WebSocket消息的WebSocket帧(Frame)。为了理解消息使用API的方式,WebSocket API只输出完整的消息,而不是WebSocket帧。message事件在接收到消息时触发,对应于该事件的回调函数是onmessage。
除了文本,WebSocket消息还可以处理二进制数据,这种数据作为Blob消息或者ArrayBuffer消息处理。因为设置WebSocket消息二进制数据类型的应用程序会影响二进制消息,所以必须在读取数据之前决定用于客户端二进制输入数据的binaryType 类型。

		//接收到消息的回调方法
        websocket.onmessage = function (event) {
    
    
            if (event.data) {
    
    
            //获取data数据进行处理
            }
        }

3.error

error事件在响应意外故障的时候触发。与该事件对应的回调函数为onerror。错误还会导致WebSocket连接关闭。如果你接收一个error事件,可以预期很快就会触发close事件。close事件中的代码和原因有时候能告诉你错误的根源。error事件处理程序是调用服务器重连逻辑以及处理来自WebSocket对象的异常的最佳场所。

 		//连接发生错误的回调方法
        websocket.onerror = function (e) {
    
    
            console.error("WebSocket连接发生错误");
        };

4.close

close事件在WebSocket连接关闭时触发。对应于close事件的回调函数是onclose。一旦连接关闭,客户端和服务器不再能接收或者发送消息。连接关闭可能有多种原因,比如连接失败或者成功的WebSocket关闭握手。

		//连接关闭的回调方法
        websocket.onclose = function () {
    
    
            console.log("WebSocket连接关闭")
        };

5.websocket具有以下几个方面的优势:

(1)建立在 TCP 协议之上,服务器端的实现比较容易。
(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据。
(5)没有同源限制,客户端可以与任意服务器通信。
(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

二、服务端配置

/**
 * @author javafg
 * @date 2020/10/15 16:50
 **/
@Component
@ServerEndpoint(value = "/loggings/{logType}",configurator = MyEndpointConfig.class)
public class LoggingWebConfig{
    
    
    private final Logger log= LoggerFactory.getLogger(LoggingWebConfig.class);

    @Value("${logging.file.path}")
    private String logFilePath;
    @Autowired
    MobileUserService mobileUserService;

    /**
     * 连接集合
     */
    private static Map<String, Session> sessionMap = new ConcurrentHashMap<String, Session>();
    private static Map<String, Integer> lengthMap = new ConcurrentHashMap<String, Integer>();

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("logType") int logType) {
    
    
        //添加到集合中
        sessionMap.put(session.getId(), session);
        lengthMap.put(session.getId(), 1);//默认从第一行开始
        //获取日志信息
        new Thread(() -> {
    
    
            log.info("LoggingWebSocketServer任务开始...");
            boolean first = true;
            while (sessionMap.get(session.getId()) != null) {
    
    
                BufferedReader reader = null;
                try {
    
    
                    //日志文件路径,获取最新的,多种日志切换
                    String filePath ="";
                    if(logType==1){
    
    
                        filePath=logFilePath+"/fg-info.log";//new SimpleDateFormat("yyyy-MM-dd").format(new Date());
                    }else if(logType==2){
    
    
                        filePath=logFilePath+"/fg-error.log";
                    }else if(logType==3){
    
    
                        filePath=logFilePath+"/fg-debug.log";
                    }else{
    
    
                        filePath=logFilePath+"/fg-info.log";
                    }
                    //System.out.println(logType+","+filePath);
                    //字符流
                    reader = new BufferedReader(new FileReader(filePath));
                    Object[] lines = reader.lines().toArray();

                    //只取从上次之后产生的日志
                    Object[] copyOfRange = Arrays.copyOfRange(lines, lengthMap.get(session.getId()), lines.length);

                    //对日志进行着色,更加美观  PS:注意,这里要根据日志生成规则来操作
                    for (int i = 0; i < copyOfRange.length; i++) {
    
    
                        String line = (String) copyOfRange[i];
                        //先转义
                        line = line.replaceAll("&", "&amp;")
                                .replaceAll("<", "&lt;")
                                .replaceAll(">", "&gt;")
                                .replaceAll("\"", "&quot;");

                        //处理等级颜色
                        line = line.replace("DEBUG", "<span style='color: blue;'>DEBUG</span>");
                        line = line.replace("INFO", "<span style='color: green;'>INFO</span>");
                        line = line.replace("ERROR", "<span style='color: red;'>ERROR</span>");

                        //处理类名
                        String[] split = line.split("]");
                        if (split.length >= 2) {
    
    
                            String[] split1 = split[1].split("-");
                            if (split1.length >= 2) {
    
    
                                line = split[0] + "]" + "<span style='color: #298a8a;'>" + split1[0] + "</span>" + "-" 			                          + split1[1];
                            }
                        }

                        // 匹配日期开头加换行,2020-10-14 11:22:19
                        Pattern r = Pattern.compile("[\\d+][\\d+][\\d+][\\d+]-[\\d+][\\d+]-[\\d+][\\d+] [\\d+][\\d+]:[\\d+][\\d+]:[\\d+][\\d+]");
                        Matcher m = r.matcher(line);
                        if (m.find( )) {
    
    
                            //找到下标
                            int start = m.start();
                            //插入
                            StringBuilder  sb = new StringBuilder (line);
                            sb.insert(start,"<br/><br/>");
                            line = sb.toString();
                        }

                        copyOfRange[i] = line;
                    }

                    //存储最新一行开始
                    lengthMap.put(session.getId(), lines.length);

                    //第一次如果太大,截取最新的200行就够了,避免传输的数据太大
                    if(first && copyOfRange.length > 200){
    
    
                        copyOfRange = Arrays.copyOfRange(copyOfRange, copyOfRange.length - 200, copyOfRange.length);
                        first = false;
                    }

                    String result = StringUtils.join(copyOfRange, "<br/>");

                    //发送数据到客户端
                    send(session, result);
                    //休眠一秒
                    Thread.sleep(1000);
                } catch (Exception e) {
    
    
                    //捕获但不处理
                    e.printStackTrace();
                } finally {
    
    
                    try {
    
    
                        reader.close();
                    } catch (IOException ignored) {
    
    
                    }
                }
            }
            log.info("LoggingWebSocketServer任务结束..");
        }).start();
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(Session session) {
    
    
        //从集合中删除
        sessionMap.remove(session.getId());
        lengthMap.remove(session.getId());
    }

    /**
     * 发生错误时调用
     */
    @OnError
    public void onError(Session session, Throwable error) {
    
    
        error.printStackTrace();
    }

    /**
     * 服务器接收到客户端消息时调用的方法
     */
    @OnMessage
    public void onMessage(String message, Session session) {
    
    

    }

    /**
     * 封装一个send方法,发送消息到前端
     */
    private void send(Session session, String message) {
    
    
        try {
    
    
            session.getBasicRemote().sendText(message);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
}

三、客户端配置

<!DOCTYPE>
<!--解决idea thymeleaf 表达式模板报红波浪线-->
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>实时日志</title>
  <!-- jquery -->
  <script type="text/javascript" src="../js/jquery.min.js"></script>
</head>
<body>
<!-- 标题 -->
<h1 style="text-align: center;">实时web日志</h1>

<!-- 显示区 -->
<div id="loggingText" contenteditable="true"
     style="width:100%;height: 600px;background-color: ghostwhite; overflow: auto;background-color: #d1d1d1;"></div>

<!-- 操作栏 -->
<div style="text-align: center;">
  <button onclick="$('#loggingText').text('')" style="color: green; height: 35px;">清屏</button>
  <button onclick="changeType(1)" style="color: green; height: 35px;">info</button>
  <button onclick="changeType(2)" style="color: red; height: 35px;">error</button>
  <button onclick="changeType(3)" style="color: blue; height: 35px;">debug</button>
  <button onclick="$('#loggingText').animate({scrollTop:$('#loggingText')[0].scrollHeight});"
          style="color: green; height: 35px;">滚动至底部
  </button>
  <button onclick="if(window.loggingAutoBottom){$(this).text('开启自动至底部');}else{$(this).text('关闭自动至底部');};window.loggingAutoBottom = !window.loggingAutoBottom"
          style="color: green; height: 35px; ">开启自动至底部
  </button>
</div>
</body>
<script th:inline="javascript">
    //websocket对象

    $(document).ready(function(){
    
    
        changeType(1)
    })
    function changeType(logType) {
    
    
        let websocket = null;
        //判断当前浏览器是否支持WebSocket
        if ('WebSocket' in window) {
    
    
            websocket = new WebSocket("ws://localhost:8080/fg_basic_system/loggings/"+logType+"");
        } else {
    
    
            console.error("不支持WebSocket");
        }

        //连接发生错误的回调方法
        websocket.onerror = function (e) {
    
    
            console.error("WebSocket连接发生错误");
        };

        //连接成功建立的回调方法
        websocket.onopen = function () {
    
    
            console.log("WebSocket连接成功")
        };

        //接收到消息的回调方法
        websocket.onmessage = function (event) {
    
    
            //追加
            if (event.data) {
    
    
                $("#loggingText").text('');
                //日志内容
                let $loggingText = $("#loggingText");
                $loggingText.append(event.data);
                //是否开启自动底部
                if (window.loggingAutoBottom) {
    
    
                    //滚动条自动到最底部
                    $loggingText.scrollTop($loggingText[0].scrollHeight);
                }
            }
        }

        //连接关闭的回调方法
        websocket.onclose = function () {
    
    
            console.log("WebSocket连接关闭")
        };
    }
</script>
</html>

四、效果展示

点击不同级别的,可以切换不同级别的日志,刷新接口后,可以实时在页面上输出出来,就不用打开路径里的日志文件了
在这里插入图片描述

五、总结

上面两个配置只是实时输出到web页面功能的实现,另外还要一些基础的webSocketConfig的配置,引入springboot的webSocket的包,还要使你的logback日志分别能输出不同级别的日志,具体可以看我之前写的博客有介绍过

猜你喜欢

转载自blog.csdn.net/qq_40136782/article/details/109099041