第一步,咱们要自定义一个 ILogger。
public class KingkingLogger : ILogger { private readonly string cateName; // 堆代码 duidaima.com public KingkingLogger(string cate) { cateName = cate; } public IDisposable? BeginScope<TState>(TState state) where TState : notnull { return default; } public bool IsEnabled(LogLevel logLevel) { return logLevel != LogLevel.None; } public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) { if(IsEnabled(logLevel) == false) { return; } // 获取格式化后的文本 string fstr = formatter(state, exception); // 显示消息类型 string head = logLevel switch { LogLevel.Information => "消息", LogLevel.Warning => "警告", LogLevel.Error => "错误", _ => "未知" }; // 加个日期 string currdate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); // 连接字符串 fstr = $"[{head}:{cateName}][{currdate}]{fstr}"; // 触发事件 TransferLog?.Invoke(fstr); } // 静态属性 public static Action<string>? TransferLog { get; set; } }我暂时想不到叫啥名字,就暂且叫它 Kingking 日志记录器吧,我在项目中的类是叫 WTFLogger 的,什么内涵你懂的,反正现在这项目只有我一个人在写,取这个名字也无所谓。这个类不复杂,我解释一下你就明白了。
public class KingkingLoggerProvider : ILoggerProvider { public ILogger CreateLogger(string categoryName) { return new KingkingLogger(categoryName); } public void Dispose() { return; } }代码很简单,没啥玄机。不过,为了调用方便,咱们可以封装一个扩展方法。
public static class CustLoggerExtensions { public static ILoggingBuilder AddKingkingLogger(this ILoggingBuilder builder) { builder.Services.AddSingleton<ILoggerProvider, KingkingLoggerProvider>(); return builder; } }这样就做到了像官方 API 那样,用 AddXXX 的方法添加日志功能,用法如下:
var builder = WebApplication.CreateBuilder(args); // 配置日志 builder.Services.AddLogging(o => { // 清空所有日志提供者 o.ClearProviders(); // 添加控制台日志输出 o.AddConsole(); // 添加咱们自己写的日志记录器 o.AddKingkingLogger(); });第三步,实现 Hub。Hub 是 SignalR 通信的“中心”类,当访问的 URL 匹配时就会激活咱们的 Hub。自定义 Hub 只要从 Hub 类派生即可。
public class MyHub : Hub { public MyHub() { // 这里关联的就是日志记录类中的静态委托 KingkingLogger.TransferLog = KingkingLogger_TransferLog; } private void KingkingLogger_TransferLog(string obj) { // 向所有客户端发日志 Clients.All.SendAsync("onLogged", obj); } protected override void Dispose(bool disposing) { if(disposing) { // 实例释放时移除关联 KingkingLogger.TransferLog = null; } base.Dispose(disposing); } }逻辑很简单,就是有日志了就推送给客户端。Clients.All 是把消息发给所有连接的客户端。
var builder = WebApplication.CreateBuilder(args); …… builder.Services.AddSignalR(); var app = builder.Build();还要 Map 一下终结点,以绑定请求 Hub 的地址。
var builder = WebApplication.CreateBuilder(args); …… var app = builder.Build(); …… // 记得这个 app.MapHub<MyHub>("/hub"); app.Run();这里我设定的地址是 http://localhost/hub。
var builder = WebApplication.CreateBuilder(args); …… // 把Hub注册为单实例 builder.Services.AddSingleton<MyHub>(); builder.Services.AddSignalR(); var app = builder.Build();第四步,客户端程序。客户端并不是只能用 JS 来写,.NET 团队也做了相关的 Nuget 包。在项目中引用一下。
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>net8.0-windows</TargetFramework> <Nullable>enable</Nullable> <UseWindowsForms>true</UseWindowsForms> <ImplicitUsings>enable</ImplicitUsings> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.0" /> </ItemGroup> </Project>在主窗口中放一个文本框,两个按钮。文本框显示收到的日志,按钮用来请求连接和断开连接。
using Microsoft.AspNetCore.SignalR.Client; namespace TestClient; public partial class Form1 : Form { // 连接对象 HubConnection hubConn; public Form1() { InitializeComponent(); // 初始化连接 var connBuilder = new HubConnectionBuilder() .WithUrl("http://localhost:6225/hub") .WithAutomaticReconnect(); hubConn = connBuilder.Build(); // 关联方法 hubConn.On<string>("onLogged", OnLogRecv); } private void OnLogRecv(string msg) { // 服务器回调,显示收到的日志 textBox1.Invoke(() => { textBox1.AppendText(msg + Environment.NewLine); }); } private async void btnConn_Click(object sender, EventArgs e) { try { await hubConn.StartAsync(); lbMessage.Text = "已建立连接"; } catch(Exception ex) { lbMessage.Text = ex.Message; } } private async void btnDisconn_Click(object sender, EventArgs e) { if(hubConn.State == HubConnectionState.Connected) { await hubConn.StopAsync(); lbMessage.Text = "已断开连接"; } } }注意,在调用 On 方法时,onLogged 要与服务器上指定的一致,否则服务器回调无效。
/*---------------- 服务器端 ------------------*/ private void KingkingLogger_TransferLog(string obj) { // 向所有客户端发日志 Clients.All.SendAsync("onLogged", obj); } /*--------------------- 客户端 -------------------*/ hubConn.On<string>("onLogged", OnLogRecv);为了测试能否真的传递了日志,咱们在服务端写几个 Mini-API 来验证。
app.MapGet("/", (ILoggerFactory logFact) => { ILogger logger = logFact.CreateLogger("MINI Main"); logger.LogInformation("欢迎来到圆环世界"); return "Hello Guy"; }); app.MapGet("/start", (ILoggerFactory logFact) => { ILogger logger = logFact.CreateLogger("MINI Go Go Go"); logger.LogWarning("游戏开始了,你必须先和QB签订契约"); return "圆神启动"; }); app.MapGet("/shot", (ILoggerFactory loggerFact) => { ILogger logger = loggerFact.CreateLogger("MINI Wind"); logger.LogInformation("干得好,三发入魂"); return "第一局完胜"; });同时启动服务端和客户端试试吧。为了使测试更真实,我启动了三个客户端。触发日志记录,请调用任意一个 API。