概要
サーバー側で蓄積するログデータをある程度リアルタイムにブラウザに表示したかったので、WebSocket通信を使って、サーバーから通知した方法です。
環境
ASP.NET Core
React
Typescript
実装方針
サーバー側でlogファイルへの書き込み内容を、クライアント(ブラウザ)へ通知するプログラム。
最終的にはgifのような動きになります。
実装
サーバー実装
まずASP.NET CoreのWebApplicationBuilderにWebSocketを使用するよう行を追加します。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
app.UseHsts();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
// 追加
app.UseWebSockets();
app.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
app.MapFallbackToFile("index.html");;
app.Run();
次にWebSocketの接続要求の入り口になるControllerを追加します。
using Microsoft.AspNetCore.Mvc;
namespace websocket_react_aspdotnetcore.Controllers;
[ApiController]
public class LogController : ControllerBase
{
[Route("/ws/TrailLog")]
public async Task TrailLog(string fileName)
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
return;
using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
}
}
ここで重要なのはTrailLog関数にRoute属性がついていることです。
通常のWebApiのようにPostやGetではないので注意が必要です。
次にログファイルのStreamのReadとWebSocketへの書き込み処理を追加していきますが、その前にWebSocketクラスに拡張メソッドを追加して、処理をシンプルにしていきます。
WebSocketExtensionクラスでは、WebSocket通信をJsonでやり取りするための拡張メソッドを定義しました。
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
namespace websocket_react_aspdotnetcore;
public class WebSocketData<T>
{
public bool IsClosed { get; set; }
public T? Data { get; set; }
}
public static class WebSocketExtension
{
public static async Task SendAsync<T>(this WebSocket self, T obj)
{
await self.SendAsync(
new ArraySegment<byte>(Encoding.ASCII.GetBytes(JsonSerializer.Serialize(obj))),
WebSocketMessageType.Text,
true,
CancellationToken.None);
}
public static async Task<WebSocketData<T>> ReadAsync<T>(this WebSocket self) where T : class
{
var buffer = new byte[1024];
var receivedResult = await self.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
if (receivedResult.Count == 0)
return new WebSocketData<T>
{
// WebSocketではデータのやり取りと同様に、切断の通知がくるため
// receivedResult.CloseStatusで切断が通知されたかチェックする
IsClosed = receivedResult.CloseStatus.HasValue,
Data = null
};
var read = buffer.Take(receivedResult.Count);
var ascii = Encoding.ASCII.GetString(read.ToArray());
var res = JsonSerializer.Deserialize<T>(ascii);
return new WebSocketData<T>
{
IsClosed = receivedResult.CloseStatus.HasValue,
Data = res
};
}
}
上記で用意した拡張メソッドを使って、ログファイルのStreamのReadとWebSocketへの書き込み処理をControllerに追加します。
using Microsoft.AspNetCore.Mvc;
namespace websocket_react_aspdotnetcore.Controllers;
[ApiController]
[Route("[controller]")]
public class LogController : ControllerBase
{
[Route("/ws/TrailLog")]
public async Task TrailLog(string fileName)
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
return;
using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
// ログファイルのStream取得
using var logStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var logStreamReader = new StreamReader(logStream);
bool open = true;
try
{
// 接続後は非同期的にログファイルの内容をWebSocketに書き込み続ける
_ = Task.Run(async () =>
{
while (open)
{
var line = await logStreamReader.ReadLineAsync();
if (string.IsNullOrEmpty(line))
{
// Streamが最終行になったら1秒待機してから読み込みを再開する
Thread.Sleep(1000);
continue;
}
await webSocket.SendAsync(new LogResponse()
{
Log = line + "\n"
});
}
});
// クライアントからの要求を処理する
while (true)
{
var request = await webSocket.ReadAsync<LogRequest>();
if (request.IsClosed || request.Data == null)
break;
if (request.Data.LogCommand == LogCommand.Ping)
continue;
}
}
finally
{
open = true;
}
}
}
public enum LogCommand
{
Ping = 1,
}
public class LogRequest
{
public LogCommand LogCommand { get; set; }
}
public class LogResponse
{
public string Log { get; set; }
}
デフォルトでは2分間の無通信時にタイムアウトするため、クライアントからのPingコマンドを受信できるようにしてあります。
クライアント実装
クライアント側は表示時にWebSocket通信を開始して、受信したメッセージを画面に表示していきます。タイムアウトを防ぐため、30秒に一度コマンドを送信する処理も入れました。
import { useEffect, useRef, useState } from 'react';
import './custom.css';
export default function App() {
const socketRef = useRef<WebSocket | null>();
const [log, setLog] = useState("");
const createWebSocket = (fileName: string) => {
const webSocket = new WebSocket(`ws://localhost:5123/ws/TrailLog?fileName=${fileName}`)
socketRef.current = webSocket
// サーバーからの通知に対するコールバックの設定
webSocket.addEventListener("message", event => {
const response = JSON.parse(event.data);
setLog(log => log + response.Log);
})
}
const closeSocket = () => {
socketRef.current?.close()
socketRef.current = null;
}
const sendLogCommand = ((command: number) => {
socketRef.current?.send(JSON.stringify({
LogCommand: command
}))
})
useEffect(() => {
// 30秒に一度Ping
const intervalId = setInterval(() => {
sendLogCommand(1)
}, 30000)
// ソケット通信の開始
createWebSocket("log.txt")
// 画面遷移時の破棄処理
return () => {
clearInterval(intervalId);
closeSocket();
}
}, [])
return (
<pre>{log}</pre>
);
}
確認
プログラムを起動して、プロジェクトルートにlog.txtファイルを追加します。
log.txtに書き込んだ内容が、ブラウザに表示されれば完了です。
最後に
主題はWebSocketなので、ログファイルを定期チェックさせていますが、FileSystemWatcherクラスなどで監視させれば、もう少しリアルタイム性が上がりますね。
ソースファイル
https://github.com/skspwork/websocket-react-aspdotnetcore
参考サイト
https://learn.microsoft.com/ja-jp/aspnet/core/fundamentals/websockets?view=aspnetcore-8.0
https://qiita.com/_ytori/items/a92d69760e8e8a2047ac