一、先了解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("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll("\"", """);
//处理等级颜色
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日志分别能输出不同级别的日志,具体可以看我之前写的博客有介绍过