這個(gè)是我在 CSDN 的第一百篇原則博文,留念??
#1 需求說(shuō)明
先說(shuō)下項(xiàng)目結(jié)構(gòu),后端基于 Spring Boot 3,前端為 node.js 開(kāi)發(fā)的控制臺(tái)程序?,F(xiàn)在希望能夠在前端模擬 tail 命令,持續(xù)輸出后端的日志文件。
#2 技術(shù)方案
#2.1 基于輪詢(xún)(PASS)
這個(gè)方案實(shí)施較為簡(jiǎn)單,通過(guò)前端不斷(定時(shí))發(fā)起請(qǐng)求,并攜帶已讀的內(nèi)容坐標(biāo)(position),詢(xún)問(wèn)后端日志文件是否有更新,判斷依據(jù)為當(dāng)前文件大小大于 position。若有變動(dòng),則讀取更新的內(nèi)容,回顯在前端控制臺(tái)。
此方案會(huì)產(chǎn)生非常多的請(qǐng)求,如果定時(shí)間隔設(shè)置不好,會(huì)有明顯的延遲,故不采用。
#2.2 WebSocket 長(zhǎng)連接
- 前端開(kāi)啟一個(gè)
WebSocket
- 后端監(jiān)聽(tīng)到長(zhǎng)連接后,啟動(dòng)文件變動(dòng)檢測(cè)線(xiàn)程
- 若文件發(fā)生變動(dòng),則讀取更新內(nèi)容,發(fā)送到前端
#3 實(shí)施
#3.1 后端改造
關(guān)于 Spring Boot 與 WebSocket 的集成,請(qǐng)轉(zhuǎn)到:springboot集成websocket持久連接(權(quán)限過(guò)濾+攔截)
首先,我們定義一個(gè)監(jiān)聽(tīng)文件變動(dòng)并讀取最新內(nèi)容的工具類(lèi)(借助于 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 ,發(fā)現(xiàn)不能正常讀取文件追加的內(nèi)容
private var monitor: FileAlterationMonitor = FileAlterationMonitor(delay)
init {
// 初始化監(jiān)視器,只檢測(cè)同名的文件
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()
}
}
再定義長(zhǎng)連接的通信處理類(lèi):
@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")
// 加入隊(duì)列
monitors[session.id] = FileTail(
textFile,
{ text -> session.sendMessage(TextMessage(text)) }
)
}catch (e:Exception){
logger.error("處理客戶(hù)端消息失敗", e)
session.sendMessage(TextMessage("服務(wù)器出錯(cuò):${ExceptionUtils.getMessage(e)}"))
session.close(CloseStatus.SERVER_ERROR)
}
}
override fun afterConnectionClosed(session: WebSocketSession, status: CloseStatus) {
logger.info("客戶(hù)端(${session.id})${session.remoteAddress} 斷開(kāi)連接...")
monitors.remove(session.id)?.stop()
}
}
編寫(xiě)配置類(lèi),啟用上述的組件:
@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 握手開(kāi)始:${request.uri} 客戶(hù)端=${request.remoteAddress}")
request.headers.forEach { name, v -> logger.debug("[HEADER] $name = $v") }
}
//此處可以進(jìn)行鑒權(quán)
//寫(xiě)入屬性值,方便在 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)
請(qǐng)先安裝依賴(lài):
npm i -D ws
文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-841116.html
/**
* 跟蹤遠(yuǎn)程日志文件
* @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(`與服務(wù)器連接成功 ??`)))
// client.on('close',()=> console.debug(chalk.magenta(`\n與服務(wù)器連接關(guā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 看看效果
文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-841116.html
到了這里,關(guān)于Spring Boot 集成 WebSocket 實(shí)例 | 前端持續(xù)打印遠(yuǎn)程日志文件更新內(nèi)容(模擬 tail 命令)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!