若依框架添加websocket服务监听日志文件变化并发送给前端
1 后端
1.1 添加websocket依赖
在ruoyi-framework模块pom.xml添加websocket依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
1.2 添加websocket配置
在ruoyi-framework模块的config下添加WebSocketConfig配置类
package com.ruoyi.framework.config;
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();
}
}
1.3 实现websocket服务
在ruoyi-admin添加一个包名handler,然后往里添加类WebSocketServer
package com.ruoyi.web.handler;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
@ServerEndpoint("/websocket/{userId}")
@Component
@Slf4j
public class WebSocketServer {
/**静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。*/
private static int onlineCount = 0;
/**concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。*/
private static ConcurrentHashMap<String,WebSocketServer> webSocketMap = new ConcurrentHashMap<>();
/**与某个客户端的连接会话,需要通过它来给客户端发送数据*/
private Session session;
/**接收userId*/
private String userId="";
/**
* 连接建立成功调用的方法*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
this.session = session;
this.userId=userId;
if(webSocketMap.containsKey(userId)){
webSocketMap.remove(userId);
webSocketMap.put(userId,this);
//加入set中
}else{
webSocketMap.put(userId,this);
//加入set中
addOnlineCount();
//在线数加1
}
log.info("用户连接:"+userId+",当前在线人数为:" + getOnlineCount());
try {
sendMessage("连接成功");
} catch (IOException e) {
log.error("用户:"+userId+",网络异常!!!!!!");
}
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
if(webSocketMap.containsKey(userId)){
webSocketMap.remove(userId);
//从set中删除
subOnlineCount();
}
log.info("用户退出:"+userId+",当前在线人数为:" + getOnlineCount());
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("用户消息:"+userId+",报文:"+message);
//可以群发消息
//消息保存到数据库、redis
}
/**
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("用户错误:"+this.userId+",原因:"+error.getMessage());
}
/**
* 实现服务器主动推送
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
/**
* 实现服务器主动推送
*/
public void sendAllMessage(String message) throws IOException {
ConcurrentHashMap.KeySetView<String, WebSocketServer> userIds = webSocketMap.keySet();
for (String userId : userIds) {
WebSocketServer webSocketServer = webSocketMap.get(userId);
webSocketServer.session.getBasicRemote().sendText(message);
}
}
/**
* 发送自定义消息
* */
public static void sendInfo(String message,@PathParam("userId") String userId) throws IOException {
log.info("发送消息到:"+userId+",报文:"+message);
if(StringUtils.isNotBlank(userId)&&webSocketMap.containsKey(userId)){
webSocketMap.get(userId).sendMessage(message);
}else{
log.error("用户"+userId+",不在线!");
}
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}
}
1.4 修改SecurityConfig
这是关键,不然客户端连接不上。找到ruoyi-framework下的SecurityConfig,放行websocket请求
.antMatchers("/websocket/**").permitAll()
修改后大概是这样子:
1.5 添加文件变化监听类
在ruoyi-admin与WebSocketServer 所在同意包名下添加文件变化监听类FileChangeWebSocketHandler
package com.ruoyi.web.handler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketSession;
import javax.annotation.Resource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Component
public class FileChangeWebSocketHandler {
private final Path logFilePath = Paths.get("/var/log/syslog");
private long lastSentPosition = 0; // 记录上次发送的位置
private final WatchService watchService;
@Resource
private WebSocketServer webSocketServer;
@Autowired
public FileChangeWebSocketHandler() throws IOException {
this.watchService = FileSystems.getDefault().newWatchService();
logFilePath.getParent().register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
watchFileChanges();
}
private void watchFileChanges() {
new Thread(() -> {
try {
while (true) {
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
sendLogFileContent();
}
}
key.reset();
}
} catch (InterruptedException | IOException e) {
e.printStackTrace();
}
}).start();
}
private void sendLogFileContent() throws IOException {
long fileSize = Files.size(logFilePath);
if (fileSize > lastSentPosition) {
String incrementContent = readIncrementalContent();
System.out.println("log file changed, context="+incrementContent);
sendMessageToAllSessions(incrementContent);
lastSentPosition = fileSize;
}
}
private String readIncrementalContent() throws IOException {
long fileSize = Files.size(logFilePath);
long startPosition = lastSentPosition;
long endPosition = fileSize;
// 读取新增部分内容
byte[] bytes = Files.readAllBytes(logFilePath);
byte[] incrementBytes = Arrays.copyOfRange(bytes, (int) startPosition, (int) endPosition);
return new String(incrementBytes, StandardCharsets.UTF_8);
}
public void sendMessageToAllSessions(String message) throws IOException {
webSocketServer.sendAllMessage(message);
}
private List<WebSocketSession> getSessions() {
// 获取当前所有的WebSocketSession
// 这里可能需要维护一个会话列表
// 可以根据具体情况自行实现
return new ArrayList<>();
}
}
说明,这里只是简单的指定了待监听文件的路径,实际应用中应该是通过外部传入的。
2 前端
2.1 代理配置
由于我前后端分离,服务端运行在另一台linux服务器上,而前端运行在本地,这就跨域了哈,这是需要配置开发环境下的跨域行为,在前端代码,打开.env.development文件,添加一个常量VUE_APP_WEBSOCKET_API = '/ws-api'
, 然后打开vue.config.js,添加websocket代理配置:
devServer: {
host: '0.0.0.0',
port: port,
open: false,
proxy: {
// detail: https://cli.vuejs.org/config/#devserver-proxy
[process.env.VUE_APP_BASE_API]: {
target: `http://172.16.1.137:8080`,
changeOrigin: true,
pathRewrite: {
['^' + process.env.VUE_APP_BASE_API]: ''
}
},
[process.env.VUE_APP_WEBSOCKET_API]: {
target: `ws://172.16.1.137:8080`,
changeOrigin: true,
ws: true,
pathRewrite: {
['^' + process.env.VUE_APP_WEBSOCKET_API]: ''
}
}
},
disableHostCheck: true
}
这里也把http接口代理也配置了。
2.2 连接代码
在某个页面的created() 方法,连接websocket服务器,注意这里的url的请求路径“websocket”,名称要与服务器配置的名称保持一致。
created() {
this.getList();
const url = process.env.VUE_APP_WEBSOCKET_API + "/websocket/1500";
let socket = new WebSocket(url);
socket.onmessage = function(event) {
console.log("Received file increment: " + event.data);
// 处理接收到的文件增量内容,例如更新页面显示等
};
},
2.3 效果
启动服务端和前端,我们可以看到实时打印的每一行变化的文本