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

这个是我在 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 看看效果

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

应用案例 | 基于三维机器视觉的自动化码垛解决方案

Part.1 行业背景 通过显扬科技三维机器视觉设备&#xff0c;搭配三维视觉分析系统&#xff0c;实现码垛的智能化升级&#xff0c;早期使用机器人码垛采用机械臂和简单的控制系统&#xff0c;码垛能力和效率较低。随着现代工业的发展&#xff0c;机器人码垛也开始采用先进的传感…

Flink通讯模型—Akka与Actor模型

Carl Hewitt 在1973年对Actor模型进行了如下定义&#xff1a;"Actor模型是一个把Actor作为并发计算的通用原语". Actor是异步驱动&#xff0c;可以并行和分布式部署及运行的最小颗粒。也就是说&#xff0c;它可以被分配&#xff0c;分布&#xff0c;调度到不同的CPU&…

金蝶BI方案能解决云星空数据分析痛点吗?

金蝶云星空作为一个主攻企业管理流程的软件确实立下了汗马功劳&#xff0c;但一到数据分析方面那就阻碍重重了。直接的感受是分析步骤多且复杂&#xff0c;数据展现不够直观易懂&#xff0c;有些分析指标的计算真的很难实现&#xff0c;跨部门跨组织计算指标、合并账套什么的能…

音频读取之wave和liborsa

wave 常见的语音信号处理python库有librosa, scipy, soundfile等等。wave库是python的标准库&#xff0c;对于python来说相对底层&#xff0c;wave不支持压缩/解压&#xff0c;但支持单声道/立体声语音的读取。 读取音频 import wave #导入库file_path D:/ba.wav #文件路径…

微服务分布式基于Springcloud的拍卖管理系统597wx

越来越多的用户利用互联网获得信息&#xff0c;但各种信息鱼龙混杂&#xff0c;信息真假难以辨别。为了方便用户更好的获得信息&#xff0c;因此&#xff0c;设计一种安全高效的拍卖管理系统极为重要。 为设计一个安全便捷&#xff0c;并且使用户更好获取拍卖管理系统&#xff…

数据结构:链式二叉树

对于二叉树而言,如果不是完全二叉树,就不再适合用数组存储了 二叉树的遍历 顺序 访问顺序(n NULL) 1.前序 根,左子树,右子树 1 2 3 n n n 4 5 n n 6 n n 2.中序 左子树,根,右子树 n 3 n 2 n 1 n 5 n 4 n 6 n 3.后…

【算法与数据结构】深入解析二叉树(一)

文章目录 &#x1f4dd;数概念及结构&#x1f320; 树的概念&#x1f309;树的表示&#x1f320; 树在实际中的运用&#xff08;表示文件系统的目录树结构&#xff09; &#x1f309;二叉树概念及结构&#x1f320;概念&#x1f309;数据结构中的二叉树&#x1f320;特殊的二叉…

深入浅出:Objective-C中使用MWFeedParser下载豆瓣RSS

摘要 本文旨在介绍如何在Objective-C中使用MWFeedParser库下载豆瓣RSS内容&#xff0c;同时展示如何通过爬虫代理IP技术和多线程提高爬虫的效率和安全性。 背景 随着信息量的激增&#xff0c;爬虫技术成为了获取和处理大量网络数据的重要手段。Objective-C作为一种成熟的编程…

软考77-上午题-【面向对象技术3-设计模式】-创建型设计模式02

一、生成器模式 1-1、意图 将一个复杂对象的构建与它的表示分离&#xff0c;使得同样的构建过程可以创建不同的表示。 1-2、结构图 Builder 为创建一个 Product 对象的各个部件指定抽象接口。ConcreteBuilder 实现 Builder 的接口以构造和装配该产品的各个部件&#xff0c;定…

IDEA如何删除git最新一次远程提交

IDEA如何删除git最新一次远程提交 选择应用 -> Git -> Show History 选择最新提交上一次提交 -> Reset Current Branch to Here… Reset 提示框选择 Hard push到远程分支 -> 选择Force Push 结果验证 &#xff08;最新分支已被删除&#xff09;

QT网络编程之实现UDP广播发送和接收

推荐一个不错的人工智能学习网站&#xff0c;通俗易懂&#xff0c;内容全面&#xff0c;作为入门科普和学习提升都不错&#xff0c;分享一下给大家&#xff1a;前言https://www.captainbed.cn/ai 一.UDP通信 1.QT中实现UDP通信主要用到了以下类&#xff1a;QUdpSocket、QHost…

AI 大模型赋能手机影像,小米14 Ultra 让真实有层次

2月22日&#xff0c;小米龙年第一场重磅发布会&#xff0c;正式发布专业影像旗舰小米14 Ultra。 此前小米发布的两代 Ultra&#xff0c;在不同维度&#xff0c;引领了移动影像行业的走向。最新的小米14 Ultra 在定义的时候&#xff0c;我们反复在思考&#xff1a;怎么才能把移动…

解决iview表格固定列横向滚动条无法拖动问题

问题描述&#xff1a; iview的table添加固定列以后&#xff0c;滚动条在固定列下面无法拖动&#xff0c;只能在滚动区域有所反应 解决办法 【写入main.js引入的全局文件时不需要::v-deep; 写入单个文件需要加::v-deep】 方法一&#xff1a;【带合计行也适用】 //解决iview表…

uniapp报错:[获取文件失败] 以下文件已被配置忽略打包上传,模拟器无法获取...

uniapp分包控制台报错&#xff1a; Error: module ‘pagesMember/address/address.js’ is not defined, require args is ‘pagesMember/address/address.js’ 以及 [获取文件失败] 以下文件已被配置忽略打包上传&#xff0c;模拟器无法获取&#xff1a; pagesMember/address/…

【开源-土拨鼠充电系统】鸿蒙 HarmonyOS 4.0+微信小程序+云平台

本人自己开发的开源项目&#xff1a;土拨鼠充电系统 ✍GitHub开源项目地址&#x1f449;&#xff1a;https://github.com/cheinlu/groundhog-charging-system ✍Gitee开源项目地址&#x1f449;&#xff1a;https://gitee.com/cheinlu/groundhog-charging-system ✨踩坑不易&am…

Helm Chart部署最简SpringBoot到K8S(AWS EKS版)

目标 这里假设&#xff0c;我们已经基本会使用k8s的kubectl命令进行部署了&#xff0c;也已经会自己打docker镜像推送到AWS ECR上面去了。而且&#xff0c;已经在云上准备好了AWS ECR镜像库和AWS EKS的k8s集群了。 这个前提上面&#xff0c;我们今天使用Helm Chart项目准备k8s…

java-ssm-jsp基于java的餐厅点餐系统的设计与实现

java-ssm-jsp基于java的餐厅点餐系统的设计与实现 获取源码——》公主号&#xff1a;计算机专业毕设大全

嵌入式方向还有希望吗?

我刚开始学习&#xff0c;也不知道我定位的是单片机工程师&#xff0c;嵌入式工程师职位的。 我只知道电子工程师&#xff0c;这个职位其实偏硬件&#xff0c;很多岗位需求是硬件设计&#xff0c;PCB设计&#xff0c;还要懂焊接、各种仪器仪表使用&#xff0c;还有些需要懂单片…

centos7磁盘管理,lvm挂载、扩容

一、centos7 磁盘挂载 默认盘符格式 centos7 默认文件格式xfscentos6 默认文件格式ext4centos5 默认文件格式ext3 1、/dev/vdb和/dev/mapper/lvm-data对比 1&#xff09;/dev/vdb /dev/vdb通常表示一个裸的块存储设备&#xff0c;比如一个硬盘或者虚拟机中的一个虚拟硬盘。…

vscode-server的搭建方法

一、配置服务器端口支持 1、开放端口&#xff1a; 2、关闭防火墙 systemctl stop firewalld.service systemctl disable firewalld.service二、配置code-server到服务器上** 1、下载code-server-4.22.0-linux-amd64.tar.gz到本地&#xff08;可下载最新的版本&#xff09;&a…