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

24 篇文章 0 订阅
5 篇文章 0 订阅

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

  • 14
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
Spring Boot可以很容易地集成WebSocket来实现后台向前端推送信息。首先,在你的项目中添加WebSocket的依赖。然后,在controller层加上相应的注解,如@SpringBootApplication和@EnableWebSocket。最后,启动项目并访问指定的URL来与WebSocket进行交互。 具体步骤如下: 1. 添加WebSocket的依赖到你的项目中,可以在pom.xml文件中添加以下代码: ``` <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> ``` 2. 在controller层,加上@SpringBootApplication和@EnableWebSocket注解,如下所示: ```java @SpringBootApplication @EnableWebSocket public class MywebsocketApplication { public static void main(String[] args) { SpringApplication.run(MywebsocketApplication.class, args); } } ``` 3. 创建一个WebSocket处理器类,可以通过继承自AbstractWebSocketHandler来实现。你可以在这个类中定义处理WebSocket连接、消息发送和接收的逻辑。 4. 在controller中,创建一个处理WebSocket请求的方法,并使用@MessageMapping注解来指定接收消息的路径。在这个方法中,你可以调用WebSocket处理器类的方法来处理消息,并使用WebSocketSession对象来发送消息给前端。 5. 启动你的项目,并访问指定的URL(例如http://localhost:8081/demo/toWebSocketDemo/{cid}),跳转到页面后,就可以与WebSocket进行交互了。 以上就是Spring Boot集成WebSocket的基本步骤。你可以根据具体的需求来进一步扩展和定制WebSocket的功能。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [SpringBoot 集成WebSocket详解](https://blog.csdn.net/qq_42402854/article/details/130948270)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [springboot 集成webSocket](https://blog.csdn.net/just_learing/article/details/125899260)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

集成显卡

码字不易,需要您的鼓励😄

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值