首先,如果我们想在客户端获取服务端的日志,最容易想到的肯定是客户端浏览器发个get请求,后端收到后读取本地日志文件,然后返回给客户端,完成一次请求,但是因为日志是在不断更新写入的,如果用户想实时动态的看到日志的话,就需要浏览器不断的发送这个get请求,这样才能实现动态的刷新日志的效果,显然这么做会对网络造成较大的负担。今天要说一个更好的实现方式:websocket。
websocket其实就是客户端连接服务端后,连接不会中断,这样服务端就可以不断的推送信息给客户端,具体是啥就不细说了,直接上代码了。
首先是配置类:
package com.sunsy.websocket_demo.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
然后我们需要写一个读取日志的工具类,这个类要继承Thread,不然的话我们在WebSocketServer类里使用的时候会因为Process.getInputStream()阻塞导致程序无法运行。下面是工具类TailLogUtil代码:
package com.sunsy.websocket_demo.util;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import javax.websocket.Session;
public class TailLogUtil extends Thread {
private final int READ_SIZE = 128; //日志文件默认读取大小,单位:KB
private final int TIME_MILLS = 100; //日志读取返回时间
private final int READ_LINES = 10; //日志读取每次返回的行数
private BufferedReader reader;
private Session session;
private int lineCount = 0;
public TailLogUtil(InputStream in, Session session) {
this.reader = new BufferedReader(new InputStreamReader(in));
this.session = session;
}
@Override
public void run() {
StringBuilder reportContent = new StringBuilder();
long startTime = getTime();
long tempMill;
String line;
try {
while ((line = reader.readLine()) != null) {
System.out.println(line);
reportContent.append(line).append("\r\n");
tempMill = getTime();
// 将实时日志通过WebSocket发送给客户端
if (lineCount >= READ_LINES || tempMill - startTime >= TIME_MILLS) {
session.getBasicRemote().sendText(reportContent.toString());
startTime = tempMill;
lineCount = 0;
reportContent.setLength(0);
} else {
lineCount++;
}
}
} catch (IOException e) {
System.out.println("thead raed file error:" + e);
}
}
private long getTime() {
return System.currentTimeMillis();
}
}
之后是WebSocketServer.java的代码:
package com.sunsy.websocket_demo.service;
import java.io.IOException;
import java.io.InputStream;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import com.sunsy.websocket_demo.util.TailLogUtil;
@ServerEndpoint("/websocket/{id}")
@Component
public class WebSocketServer {
private Process process;
private InputStream inputStream;
private static ApplicationContext applicationContext;
public static void setApplicationContext(ApplicationContext applicationContext) {
WebSocketServer.applicationContext = applicationContext;
}
/**
* 新的WebSocket请求开启
*/
@OnOpen
public void onOpen(@PathParam("id") String id, Session session) {
String path = "";
try {
path = getPathById(id);
System.out.println(path);
// 执行tail -f命令
process = Runtime.getRuntime().exec("tail -f " + path);
inputStream = process.getInputStream();
// 一定要启动新的线程,防止InputStream阻塞处理WebSocket的线程
TailLogUtil thread = new TailLogUtil(inputStream, session);
thread.start();
} catch (IOException e) {
System.out.println(String.format("read file [%s] error.%s", path, e));
}
}
/**
* WebSocket请求关闭
*/
@OnClose
public void onClose() {
if(inputStream != null){
try {
inputStream.close();
} catch (IOException e) {
System.out.println("close websocket error.:" + e);
}
}
if(process != null){
process.destroy();
}
}
@OnError
public void onError(Throwable thr) {
System.out.println("websocket error." + thr);
}
private String getPathById(String id) {
System.out.println(id);
return "/data/sunsy/centerTestPG/register/logs/register-1.2.0-SNAPSHOT.log";
}
}
最后是主启动类:
package com.sunsy.websocket_demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
@SpringBootApplication
public class WebsocketDemoApplication {
public static void main(String[] args) {
SpringApplication.run(WebsocketDemoApplication.class, args);
}
}
后端代码就完成了。接下来我们写一个简单的html页面来测试一下我们的功能,websocketTest.html文件内容如下:
<!DOCTYPE HTML>
<html>
<head>
<title>My WebSocket</title>
</head>
<body>
Welcome<br/>
<div id="message">
</div>
</body>
<script type="text/javascript">
var websocket = null;
//判断当前浏览器是否支持WebSocket
if('WebSocket' in window){
websocket = new WebSocket("ws://192.168.4.90:8080/websocket/1");
}
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 + '<br/>';
}
//关闭连接
function closeWebSocket(){
websocket.close();
}
</script>
</html>
我在自己的虚拟机192.168.4.90上启动后端java服务后(注意,我为了省事在WebSocketServer类的getPathById方法中把日志路径固定写成了/data/sunsy/centerTestPG/register/logs/register-1.2.0-SNAPSHOT.log,所以你们自己测试的时候要将该路径定为你自己需要的路径),在我本地打开websocketTest.html文件,可以看到如下效果,我们可以看到,日志是在不断更新的(因为我的/data/sunsy/centerTestPG/register/logs/register-1.2.0-SNAPSHOT.log日志文件是在不断更新写入的)。