Spring Boot 集成 WebSocket 实例 | 前端持续打印远程日志文件更新内容(模拟 tail 命令)

news2024/9/27 17:34:19

这个是我在 CSDN 的第一百篇原则博文,留念😎

#1 需求说明

先说下项目结构,后端基于 Spring Boot 3,前端为 node.js 开发的控制台程序。现在希望能够在前端模拟 tail 命令,持续输出后端的日志文件。

#2 技术方案

#2.1 基于轮询(PASS)

这个方案实施较为简单,通过前端不断(定时)发起请求,并携带已读的内容坐标(position),询问后端日志文件是否有更新,判断依据为当前文件大小大于 position。若有变动,则读取更新的内容,回显在前端控制台。

此方案会产生非常多的请求,如果定时间隔设置不好,会有明显的延迟,故不采用。

#2.2 WebSocket 长连接

  1. 前端开启一个 WebSocket
  2. 后端监听到长连接后,启动文件变动检测线程
  3. 若文件发生变动,则读取更新内容,发送到前端

#3 实施

#3.1 后端改造

关于 Spring Boot 与 WebSocket 的集成,请转到:springboot集成websocket持久连接(权限过滤+拦截)

首先,我们定义一个监听文件变动并读取最新内容的工具类(借助于 common-io 包):

class FileTail(val path:Path, val handler: Consumer<String>, delay:Long=1000): FileAlterationListenerAdaptor() {
    private val watcher = FileSystems.getDefault().newWatchService()

    private val MODE = "r"
    private var reader = RandomAccessFile(path.toFile(), MODE)
    private var position= reader.length()

    // 使用 JDK 自带的 WatchService ,发现不能正常读取文件追加的内容
    private var monitor: FileAlterationMonitor = FileAlterationMonitor(delay)

    init {
        // 初始化监视器,只检测同名的文件
        FileAlterationObserver(path.parent.toFile()) { f: File -> f.name == path.name }.also { observer->
            observer.addListener(this)
            monitor.addObserver(observer)

            monitor.start()
        }
    }

    override fun onFileChange(file: File) {
        reader.seek(position)

        val bytes = mutableListOf<Byte>()
        val tmp = ByteArray(1024)
        var readSize: Int

        while ((reader.read(tmp).also { readSize = it }) != -1) {
            for (i in 0..< readSize){
                bytes.add(tmp[i])
            }
        }

        position += bytes.size
        handler.accept(String(bytes.toByteArray()))
    }
    
    fun stop() {
        reader.close()
        monitor.stop()
    }
}

再定义长连接的通信处理类:

@Component
class FileTailWsHandler : TextWebSocketHandler() {
    private val logger = LoggerFactory.getLogger(javaClass)

    companion object {
        val monitors = mutableMapOf<String, FileTail>()
    }

    override fun afterConnectionEstablished(session: WebSocketSession) {
        try{
             val textFile = Paths.get("logs/spring.log")

            // 加入队列
            monitors[session.id] = FileTail(
                textFile,
                { text -> session.sendMessage(TextMessage(text)) }
            )
        }catch (e:Exception){
            logger.error("处理客户端消息失败", e)
            session.sendMessage(TextMessage("服务器出错:${ExceptionUtils.getMessage(e)}"))
            session.close(CloseStatus.SERVER_ERROR)
        }
    }

    override fun afterConnectionClosed(session: WebSocketSession, status: CloseStatus) {
        logger.info("客户端(${session.id}${session.remoteAddress} 断开连接...")

        monitors.remove(session.id)?.stop()
    }
}

编写配置类,启用上述的组件:

@Component
class WsInterceptor : HandshakeInterceptor {
    private val logger = LoggerFactory.getLogger(javaClass)

    override fun beforeHandshake(
        request: ServerHttpRequest,
        response: ServerHttpResponse,
        wsHandler: WebSocketHandler,
        attributes: MutableMap<String, Any>
    ): Boolean {
        if(logger.isDebugEnabled){
            logger.debug("WS 握手开始:${request.uri} 客户端=${request.remoteAddress}")
            request.headers.forEach { name, v -> logger.debug("[HEADER] $name = $v") }
        }
		//此处可以进行鉴权
	
		//写入属性值,方便在 handler 中获取
        attributes[F.PARAMS]    = request.headers.getFirst(F.PARAMS)?: EMPTY
        // 返回 true 才能建立连接
        return true
    }

    override fun afterHandshake(
        request: ServerHttpRequest,
        response: ServerHttpResponse,
        wsHandler: WebSocketHandler,
        exception: Exception?
    ) {
    }
}

@Configuration
@EnableWebSocket
class SocketConfig : WebSocketConfigurer {
    private val logger = LoggerFactory.getLogger(javaClass)

    @Resource
    lateinit var interceptor: WsInterceptor
    @Resource
    lateinit var fileTailHandler:FileTailWsHandler

    override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) {
        registry.addHandler(fileTailHandler, "/ws/file-tail").addInterceptors(interceptor)
	}
}

#3.2 前端(node.js)

请先安装依赖:npm i -D ws

/**
 * 跟踪远程日志文件
 * @param {*} ps
 */
const _tailRemoteFile = async ps=>{
    let url = remoteUrl("/ws/file-tail")
    let index = url.indexOf("://")

    let headers = {}
    headers.params = JSON.stringify(ps)

    const client = new WebSocket(`ws${url.substring(index)}`, { headers })
    client.on('open', ()=> console.debug(chalk.magenta(`与服务器连接成功 🤝`)))
    // client.on('close',()=> console.debug(chalk.magenta(`\n与服务器连接关闭 👋`)))
    client.on('error', e=> {
        console.debug(chalk.red(e))
    })
    client.on('message', /** @param {Buffer} buf */buf=>{
        let line = buf.toString()
        if(line.endsWith("\n") || line.endsWith("\r\n"))
            line = line.substring(0, line.length-2)
        console.debug(line)
    })
}

#3.3 看看效果

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1517170.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

手动创建线程池各个参数的意义?

今天我们学习线程池各个参数的含义&#xff0c;并重点掌握线程池中线程是在什么时机被创建和销毁的。 线程池的参数 首先&#xff0c;我们来看下线程池中各个参数的含义&#xff0c;如表所示线程池主要有 6 个参数&#xff0c;其中第 3 个参数由 keepAliveTime 时间单位组成。…

人工智能课题、模型源码

人工智能研究生毕业&#xff5e;深度学习、计算机视觉、时间序列预测&#xff08;LSTM、GRU、informer系列&#xff09;、python、人工智能项目代做和指导&#xff0c;各种opencv图像处理、图像分类模型&#xff08;vgg、resnet、mobilenet、efficientnet等&#xff09;、人脸检…

.NET高级面试指南专题十七【 策略模式模式介绍,允许在运行时选择算法的行为】

介绍&#xff1a; 策略模式是一种行为设计模式&#xff0c;它允许在运行时选择算法的行为。它定义了一系列算法&#xff0c;将每个算法封装到一个对象中&#xff0c;并使它们可以互相替换。这使得算法可独立于使用它的客户端变化。 原理&#xff1a; 策略接口&#xff08;Strat…

使用BBDown下载bilibili视频的方法

一款命令行式哔哩哔哩下载器. Bilibili Downloader. 下载地址 https://github.com/nilaoda/BBDown 功能 番剧下载(Web|TV|App) 课程下载(Web) 普通内容下载(Web|TV|App) 合集/列表/收藏夹/个人空间解析 多分P自动下载 选择指定分P进行下载 选择指定清晰度进行下载 下载外挂字幕…

基于springboot+vue实现电子商务平台管理系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现电子商务平台管理系统演示 研究的目的和意义 据我国IT行业发布的报告表明&#xff0c;近年来&#xff0c;我国互联网发展呈快速增长趋势&#xff0c;网民的数量已达8700万&#xff0c;逼近世界第一&#xff0c;并且随着宽带的实施及降价&#xff0c;每天约有…

实现微服务:匹配系统

HTTP与WebSocket协议 1. HTTP协议是无状态的&#xff0c;每次请求都是独立的&#xff0c;服务器不会保存客户端的状态信息。而WebSocket协议是有状态的&#xff0c;一旦建立连接后&#xff0c;服务器和客户端可以进行双向通信&#xff0c;并且可以保持连接状态&#xff0c;服务…

基于Java的海南旅游景点推荐系统(Vue.js+SpringBoot)

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 用户端2.2 管理员端 三、系统展示四、核心代码4.1 随机景点推荐4.2 景点评价4.3 协同推荐算法4.4 网站登录4.5 查询景点美食 五、免责说明 一、摘要 1.1 项目介绍 基于VueSpringBootMySQL的海南旅游推荐系统&#xff…

Let’s Move Sui , 一起来学习吧

Let’s Move Sui是一个全新的交互式学习平台&#xff0c;通过SuiFrens的帮助教您如何在Sui上构建。设计供新手和经验丰富的开发者使用&#xff0c;Let’s Move Sui提供了一次非凡的Sui开发之旅&#xff0c;利用了Move在Sui上的独特之处&#xff0c;从基于对象的数据模型的基础知…

WPF —— Grid网格布局

1 &#xff1a;Grid网格布局简介 Grid为WPF中最常用的布局容器, 作为View中的主要组成部分, 负责框架中整体的页面布局。 2&#xff1a;网格标签Grid.ColumnDef Grid.ColumnDefinitions自定义列 只能设置宽度 不能设置高度ColumnDefinition 每一个列可以设置宽度&#xff0c;…

windows 免密码ssh登录linux

参考&#xff1a;https://blog.csdn.net/qq285744011/article/details/118293937 1&#xff09;windows先生成公钥私钥 ssh-keygen -t rsa -C "你的邮箱地址"生成后放在用户命令.ssh文件下 2&#xff09;把公钥复制到linux /root/.ssh/authorized_keys 3)然后就可…

未解决的问题:字符数组中元素的个数

情形1&#xff1a; #include<stdio.h> int main() {int arr_int1[10];int arr_int2[]{1,2,3,4,5};char arr_char1[10];char arr_char2[]"world";char arr_char3[]{h,e,l,l,o};int i;i0;while(arr_char2[i]!\0){i;}printf("%d\n",i);i0;while(arr_ch…

【MMDetection3D实战4】利用mmdet3d进行训练

文章目录 1. 介绍1.1 训练流程1.2 测试及验证2. 训练过程演示2.1 准备数据集并处理2.2 加载并修改配置文件2.3 启动训练2.4 测试1. 介绍 1.1 训练流程 MMDetection3D(mmdet3d)和OpenMMlab其他代码库是一样的,在训练的时候需要准备好一个配置文件,在配置文件中定义好所使用的…

2.MongoDB与关系数据库对比

MongoDB的简单操作与比较 与关系数据库对比 MySQL与MongoDB都是开源的常用数据库&#xff0c;但是MySQL是传统的关系型数据库&#xff0c;MongoDB则是非关系型数据库&#xff0c;也叫文档型数据库&#xff0c;是一种NoSQL的数据库。它们各有各的优点&#xff0c;来看看他们之…

轮趣 IMU N100 九轴 IMU 在 ROS 下安装驱动

本篇介绍如何在ROS环境中使用 WHEELTEC N100 惯导模块。 轮趣 IMU N100 的 ROS 驱动程序下载链接&#xff1a;轮趣 IMU 资料 - 坚果云 - 云盘|网盘|企业网盘|同步|备份|无限空间|免费网络硬盘|企业云盘 1、CP2102 固定串口号 1.1 、修改串口号 在 Windows 中需要把 WHEELTE…

H5 宠物店官网源码

源码名称&#xff1a;H5宠物店官网源码 源码介绍&#xff1a;一款宠物店官网单页源码&#xff0c;可以用于开设宠物店宣传页、宠物店官网。源码H5自适应无后台。 需求环境&#xff1a;H5 下载地址&#xff1a; https://www.changyouzuhao.cn/11098.html

Linux-vim显示乱码

Linux运维工具-ywtool 目录 一.问题二.解决2.1 编辑VIM的配置文件2.2 添加以下内容 一.问题 用vim编辑的时候,中文显示乱码 二.解决 2.1 编辑VIM的配置文件 vim ~/.vimrc #如果这个文件不存在,创建一个即可2.2 添加以下内容 添加完成以后就不会在出现中文乱码了 set fil…

操作系统系列学习——CPU调度策略

文章目录 前言CPU调度策略 前言 一个本硕双非的小菜鸡&#xff0c;备战24年秋招&#xff0c;计划学习操作系统并完成6.0S81&#xff0c;加油&#xff01; 本文总结自B站【哈工大】操作系统 李治军&#xff08;全32讲&#xff09; 老师课程讲的非常好&#xff0c;感谢 【哈工大…

1.7 什么是可变形卷积?可变形卷积旨在解决哪类问题?

1.7 什么是可变形卷积&#xff1f;可变形卷积旨在解决哪类问题&#xff1f; 问题&#xff1a;深度卷积神经网络在许多视觉任务上获得了重大突破&#xff0c;其强大的特征提取能力避免了传统的人工特征工程的弊端。然而&#xff0c;普通的卷积操作是在固定的、规则的网格点上进…

为什么要用scrapy爬虫库?而不是纯python进行爬虫?

为什么要用scrapy爬虫库&#xff1f;而不是纯python进行爬虫&#xff1f; Scrapy的优点Scrapy节省的工作使用纯Python编写爬虫的不足 Scrapy是一个使用Python编写的开源和协作的web爬虫框架&#xff0c;它被设计用于爬取网页数据并从中提取结构化数据。Scrapy的强大之处在于其广…

安装kibaba

官方地址&#xff1a;Past Releases of Elastic Stack Software | Elastic 直接下载就可以 安装好了之后开始配置文件/kibana/config打开kibanba.yml server.port:5601 服务器地址 sercer.name:kibana 服务器名称 kibana.index:.kibana 索引 elasticsearch.hosts:[http://1…