【ASP.NET Core】使用SignalR推送服务器日志
一个多月前接手了一个产线机器人项目,上位机以读写寄存器的方式控制机器人,服务器就是用 ASP.NET Core 写的 Web API。由于前一位开发者写的代码质量问题,导致上位机需要16秒才能启动。经过我近一个月的改造,除了保留业务逻辑代码,其他的基本重写。如今上位机的启动时间在网络状态良好的条件下可以秒启动。原上位机启动慢的原因:
1、启动时使用同步方式访问 Web API,在网络较弱时需要等待很长时间。我改为导步请求,并且不等待请求结果,直接显示窗口;如果前面的请求失败,在窗口显示后再次发出异步请求,并且不等待。如果再失败才提示用户。
2、原项目在 Main 方式处就连接PLC,而产线的PLC压根就没插电源。我改为在连接机器人之后才连接,同样是异步不等待。如果连不上直接忽略。
3、原项目是一个窗口一个项目,然后把这些窗口生成 .dll,放到一个目录下,主程序启动时从目录下扫描 .dll,通过反射动态实例化窗口。这根本不需要的,一个上位机不可能有几百个窗口吧,何必呢。我改为使用服务容器的方式管理窗口,主界面通过依赖注入自动获取子窗口列表,再添加到主界面上。每个子窗口实现 IPage 接口用于识别,接口里面定义标题和页面索引即可。
4、干掉 Log4Net,使用官方的 Logging 库。
5、通信用的 JSON 数据全改用 System.Text.Json,而不是某 Newton,修改后速度快了一个次元。
由于 Web API 程序是运行在服务器的 IIS 中的,上一位开发者没有实现日志功能(仅仅用 ASP.NET Core 应用程序默认开启的控制台等日志功能),问题是日志没有保存。
我原来的计划是把日志写到系统中,这样就能保存下来,用“事件查看器”就能欣赏。后来想想这方案不行,工厂那伙人肯定找不到日志在哪。写数据库里面?想想似乎没这个必要。简单粗暴,直接自定义一个 ILogger,把日志输出到文件中,然后加一个 Web API 读取文件,上位机那里就可以调用,返回日志内容。
后经过现场调试发现,其实也不需要这样。时间长了,会存下很多日志文件,就算用日期标识文件名也是很乱。实际上他们并不要求保存日志,只是在运作过程中实时监控机器人(应该叫机械臂)的工作状态而已。如果不出问题,他们甚至连日志都不看。上面用文件实现的日志方式,主要缺点是不能实时推到上位机。就算他们不看,那我现场调试也方便我自己。
于是,我又想到了另一方案:用 SignalR 实时向上位机推送日志。
----------------------------------------------------------------------------------------------------------------------------------------
上面都是大话,现在开始主题。
原理是这样的:上位机作为 SignalR 客户端,发起连接后,不用主动调用服务器上的方法,而是等服务器调用回调方法。
第一步,咱们要自定义一个 ILogger。
public class KingkingLogger : ILogger
{
private readonly string cateName; 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 的,什么内涵你懂的,反正现在这项目只有我一个人在写,取这个名字也无所谓。这个类不复杂,我解释一下你就明白了。
1、字符串 cateName 是类别名称。就是记录日志时它属于哪个名录下的,比如我们常见的 Microsoft.Hosting.Lifetime、Microsoft.Hosting.Lifetime 等这些就是。在 Logging 库中有两种方式指定:一是用字符串,二是用 ILogger<T> ,这个类型T将作为日志类别的名称。这里我采用的是字符串方式,所以不使用 ILogger<T>。
2、BeginScope 方法的用处是当你要把 logger 用在 using 语句块时才会实现。正因为用在 using 块中,所以它要求是实现 IDisposable 接口。这个实现 IDisposable 的类一般不用公开。这方法会接收一个泛型参数 TState state。这个看你的需要了,运行库内部调用经常会用字典类型传递一些额外数据。这个 TState 你可以自定义。此处我不需要把 logger 用在 using 语句块中,所以直接返回 default(或null)。
3、IsEnabled 方法的功能是分析一下 logLevel 参数指定的日志级别当前是否要输出日志。如果需要输出日志,返回 true;不想输出日志返回 false。后面实现的 Log 方法中也会用到它,如果返回 false,那就不必去处理怎么输出日志了。
4、Log 方法是核心。在此方法中你尽情发挥吧,你想怎样输出日志就在这里完成。比如你要用 Debug 类输出,那就调用 Debug 类的成员输出;你用控制台输出就调用 Console 类的成员。我这里是要把日志传给 SignalR Hub 对象,让其传回给客户端,故要调用静态的 TransferLog 属性。此属性是委托类型,可以与方法绑定,因为咱们不能在这里调用 Hub,Hub 是由 SignalR 组件自动激活的。所以要用委托来间接实现传递。这个和事件的作用一样,只是我不用事件成员罢了。
顺便说一下,我项目中的类是同时把日志写入数据库的(不写文件了,写数据库里好清理),这里老周为了让示例简单,没有加上写入数据的代码。其实也没啥难度的,就是在数据库中加个表,用 EF Core 往表里 INSERT 一条记录。
第二步,实现 Provider。ILogger 咱们定义好了,但这个 Kingking 日志记录器可不是直接扔进服务容器,而是通过叫 ILoggerProvider 的对象来创建实例。就相当于一个工厂类。
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 是把消息发给所有连接的客户端。
这里顺便提一下:Hub 是支持依赖注入的,即你可以在 MyHub 的构造函数里注入你要用的组件,如 DBContext 等。这里我用不到其他组件,所以没有注入。
在Web应用程序初始化时要启用 SignalR 相关服务。
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。
不要以为这样就完事了,当你运行后用客户端一测试,你会发现连毛都接收不到。这是因为 Hub 对象的默认生命周期太短了,仅在用的时候实例化,然后马上 Dispose 了。然后你会想,那我重写 OnConnectedAsync 方法,关联 TransferLog 委托;再重写 OnDisConnectedAsync 方法,把 TransferLog 委托设置为 null。这个也是不行的,原因还是那个—— Hub 对象生命周期太短。
有什么办法让 Hub 长寿一点呢?还真有,直接把 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。
依次点击三个窗口上的“连接”按钮,确认全部都连上。
然后依次调用那几个 mini API 试试。
可以看到,三个客户端都收到日志推送了。
为了演示,没有数据存储,所以如果客户端没有及时连接,会丢失前面的日志。老周的实际项目中是用数据库存起来,用的时候再取出来发给客户端。默认是发最近的 100 条。如果上位机要看全部,就调用一下 Hub 的方法,Hub 的代码会 select 整个日志表再发回。
【ASP.NET Core】使用SignalR推送服务器日志的更多相关文章
- asp.net core 五 SignalR 负载均衡
SignalR : Web中的实时功能实现,所谓实时功能,就是所连接的客户端变的可用时,服务端能实时的推送内容到客户端,而不是被动的等待客户端的请求.Asp.net SignalR 源码 ...
- asp.net core 使用 signalR(一)
asp.net core 使用 signalR(一) Intro SignalR 是什么? ASP.NET Core SignalR 是一个开源代码库,它简化了向应用添加实时 Web 功能的过程. 实 ...
- SingalR 构建 推送服务器初探
项目需要用到推送,于是重新研究了下推送框架,最好能够独立成一个服务,与业务无关的服务,可以给所有的项目通用.找了好久最终决定用SinglR 框架. Signal 是微软支持的一个运行在 Dot NET ...
- 使用SignalR从服务端主动推送警报日志到各种终端(桌面、移动、网页)
微信公众号:Dotnet9,网站:Dotnet9,问题或建议:请网站留言, 如果对您有所帮助:欢迎赞赏. 使用SignalR从服务端主动推送警报日志到各种终端(桌面.移动.网页) 阅读导航 本文背景 ...
- Asp.Net Core使用SignalR进行服务间调用
网上查询过很多关于ASP.NET core使用SignalR的简单例子,但是大部分都是简易聊天功能,今天心血来潮就搞了个使用SignalR进行服务间调用的简单DEMO. 至于SignalR是什么我就不 ...
- .NET 云原生架构师训练营(ASP .NET Core 整体概念推演)--学习笔记
演化与完善整体概念 ASP .NET Core 整体概念推演 整体概念推演到具体的形式 ASP .NET Core 整体概念推演 ASP .NET Core 其实就是通过 web framework ...
- ASP.NET Core 网站发布到Linux服务器(转)
出处;ASP.NET Core 网站发布到Linux服务器 长期以来,使用.NET开发的应用只能运行在Windows平台上面,而目前国内蓬勃发展的互联网公司由于成本的考虑,大量使用免费的Linux平台 ...
- 用 centrifugo 搭建 消息推送服务器 docker + rancher 搭建
关于消息推送服务器 目前有很多第三方的开放成熟的推送服务.鉴于项目需要 我们项目需要自己搭建 自己的推送服务. 我们的推送应用场景 聊天消息 项目内部消息提醒 移动设备接受消息 应用到的相关软件工具知 ...
- asp.net core 使用 signalR(二)
asp.net core 使用 signalR(二) Intro 上次介绍了 asp.net core 中使用 signalR 服务端的开发,这次总结一下web前端如何接入和使用 signalR,本文 ...
- [转]ASP.NET Core 开发-Logging 使用NLog 写日志文件
本文转自:http://www.cnblogs.com/Leo_wl/p/5561812.html ASP.NET Core 开发-Logging 使用NLog 写日志文件. NLog 可以适用于 . ...
随机推荐
- 在线问诊 Python、FastAPI、Neo4j — 创建症状节点
目录 症状数据 创建节点 附学习 电子病历中,患者主诉对应的相关检查,得出的诊断以及最后的用药情况.症状一般可以从主诉中提取. 症状数据 symptom_data.csv CSV 中,没有直接一行一个 ...
- WEB组态编辑器插件(BY组态)介绍
BY组态是一款非常优秀的纯前端的[web组态插件工具],采用标准HTML5技术,基于B/S架构进行开发,支持WEB端呈现,支持在浏览器端完成便捷的人机交互,简单的拖拽即可完成可视化页面的设计.可无缝嵌 ...
- ionic app调试问题
以下是一些ionic app在模拟器中的调试问题: 1. CORS问题 官方原文以及解释:Handling CORS issues in Ionic 国内翻译:彻底解决Ionic项目中的跨域问题 2. ...
- 「codeforces - 1208F」Bits and Pieces
link. 考虑把原问题写成一个在 \(\left(\log_2 \max v \right) \times n\) 的矩阵里选出三列,我们首先预处理出 \(j \cap q\).具体,我们需要对于每 ...
- 「codeforces - 868F」Yet Another Minimization Problem
link. 值域分治优化决策单调性 DP 的 trick.朴素做法 trivial,不赘述. 考虑求取一个区间 \([l,r]\) 的 DP 值.先搞定在 \(m=\lfloor\frac{l+r}{ ...
- P6066 [USACO05JAN] Watchcow S
prologue 这个题这么水的一个板子题. analysis 这个题目我们正反建两条边,在跑欧拉回路的时候,看这个边是不是被走过,走过就不走,跳过这个边.如果没走,就走这条边并且标记这个边走过了. ...
- .NET静态代码织入——肉夹馍(Rougamo)发布2.0
肉夹馍(https://github.com/inversionhourglass/Rougamo)通过静态代码织入方式实现AOP的组件,其主要特点是在编译时完成AOP代码织入,相比动态代理可以减少应 ...
- day1 C语言:对于P1055 ISBN号码的代码优化及多解
day1 C语言:对于P1055 ISBN号码的代码优化及多解 先看题目 直接说最优解,其他方法后置 第一部分 1.第一个点是数据的输入,本人第一的想法是直接用int类型去接受数据,但因为" ...
- [ABC310D] Peaceful Teams 题解
Peaceful Teams 题目大意 将 \(n\) 个人分成 \(T\) 组,要求每组不能包含敌对的人,问有多少种分法. 思路分析 注意到 \(n,T\) 均很小,考虑爆搜. 注意到直接枚举会枚举 ...
- XX-net安装
1.下载https://github.com/XX-net/XX-Net 2. 3. 4.运行google浏览器 5.找到安装XX-net的位置,点击即可访问google ps:校园网用户可以直接使用 ...