ASP.NET?Core?使用SignalR推送服務(wù)器日志的過(guò)程記錄
一個(gè)多月前接手了一個(gè)產(chǎn)線機(jī)器人項(xiàng)目,上位機(jī)以讀寫(xiě)寄存器的方式控制機(jī)器人,服務(wù)器就是用 ASP.NET Core 寫(xiě)的 Web API。由于前一位開(kāi)發(fā)者寫(xiě)的代碼質(zhì)量問(wèn)題,導(dǎo)致上位機(jī)需要16秒才能啟動(dòng)。經(jīng)過(guò)我近一個(gè)月的改造,除了保留業(yè)務(wù)邏輯代碼,其他的基本重寫(xiě)。如今上位機(jī)的啟動(dòng)時(shí)間在網(wǎng)絡(luò)狀態(tài)良好的條件下可以秒啟動(dòng)。原上位機(jī)啟動(dòng)慢的原因:
1、啟動(dòng)時(shí)使用同步方式訪問(wèn) Web API,在網(wǎng)絡(luò)較弱時(shí)需要等待很長(zhǎng)時(shí)間。我改為導(dǎo)步請(qǐng)求,并且不等待請(qǐng)求結(jié)果,直接顯示窗口;如果前面的請(qǐng)求失敗,在窗口顯示后再次發(fā)出異步請(qǐng)求,并且不等待。如果再失敗才提示用戶。
2、原項(xiàng)目在 Main 方式處就連接PLC,而產(chǎn)線的PLC壓根就沒(méi)插電源。我改為在連接機(jī)器人之后才連接,同樣是異步不等待。如果連不上直接忽略。
3、原項(xiàng)目是一個(gè)窗口一個(gè)項(xiàng)目,然后把這些窗口生成 .dll,放到一個(gè)目錄下,主程序啟動(dòng)時(shí)從目錄下掃描 .dll,通過(guò)反射動(dòng)態(tài)實(shí)例化窗口。這根本不需要的,一個(gè)上位機(jī)不可能有幾百個(gè)窗口吧,何必呢。我改為使用服務(wù)容器的方式管理窗口,主界面通過(guò)依賴注入自動(dòng)獲取子窗口列表,再添加到主界面上。每個(gè)子窗口實(shí)現(xiàn) IPage 接口用于識(shí)別,接口里面定義標(biāo)題和頁(yè)面索引即可。
4、干掉 Log4Net,使用官方的 Logging 庫(kù)。
5、通信用的 JSON 數(shù)據(jù)全改用 System.Text.Json,而不是某 Newton,修改后速度快了一個(gè)次元。
由于 Web API 程序是運(yùn)行在服務(wù)器的 IIS 中的,上一位開(kāi)發(fā)者沒(méi)有實(shí)現(xiàn)日志功能(僅僅用 ASP.NET Core 應(yīng)用程序默認(rèn)開(kāi)啟的控制臺(tái)等日志功能),問(wèn)題是日志沒(méi)有保存。
我原來(lái)的計(jì)劃是把日志寫(xiě)到系統(tǒng)中,這樣就能保存下來(lái),用“事件查看器”就能欣賞。后來(lái)想想這方案不行,工廠那伙人肯定找不到日志在哪。寫(xiě)數(shù)據(jù)庫(kù)里面?想想似乎沒(méi)這個(gè)必要。簡(jiǎn)單粗暴,直接自定義一個(gè) ILogger,把日志輸出到文件中,然后加一個(gè) Web API 讀取文件,上位機(jī)那里就可以調(diào)用,返回日志內(nèi)容。
后經(jīng)過(guò)現(xiàn)場(chǎng)調(diào)試發(fā)現(xiàn),其實(shí)也不需要這樣。時(shí)間長(zhǎng)了,會(huì)存下很多日志文件,就算用日期標(biāo)識(shí)文件名也是很亂。實(shí)際上他們并不要求保存日志,只是在運(yùn)作過(guò)程中實(shí)時(shí)監(jiān)控機(jī)器人(應(yīng)該叫機(jī)械臂)的工作狀態(tài)而已。如果不出問(wèn)題,他們甚至連日志都不看。上面用文件實(shí)現(xiàn)的日志方式,主要缺點(diǎn)是不能實(shí)時(shí)推到上位機(jī)。就算他們不看,那我現(xiàn)場(chǎng)調(diào)試也方便我自己。
于是,我又想到了另一方案:用 SignalR 實(shí)時(shí)向上位機(jī)推送日志。
----------------------------------------------------------------------------------------------------------------------------------------
上面都是大話,現(xiàn)在開(kāi)始主題。
原理是這樣的:上位機(jī)作為 SignalR 客戶端,發(fā)起連接后,不用主動(dòng)調(diào)用服務(wù)器上的方法,而是等服務(wù)器調(diào)用回調(diào)方法。
第一步,咱們要自定義一個(gè) 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 => "錯(cuò)誤", _ => "未知" }; // 加個(gè)日期 string currdate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); // 連接字符串 fstr = $"[{head}:{cateName}][{currdate}]{fstr}"; // 觸發(fā)事件 TransferLog?.Invoke(fstr); } // 靜態(tài)屬性 public static Action<string>? TransferLog { get; set; } }
我暫時(shí)想不到叫啥名字,就暫且叫它 Kingking 日志記錄器吧,我在項(xiàng)目中的類是叫 WTFLogger 的,什么內(nèi)涵你懂的,反正現(xiàn)在這項(xiàng)目只有我一個(gè)人在寫(xiě),取這個(gè)名字也無(wú)所謂。這個(gè)類不復(fù)雜,我解釋一下你就明白了。
1、字符串 cateName 是類別名稱。就是記錄日志時(shí)它屬于哪個(gè)名錄下的,比如我們常見(jiàn)的 Microsoft.Hosting.Lifetime、Microsoft.Hosting.Lifetime 等這些就是。在 Logging 庫(kù)中有兩種方式指定:一是用字符串,二是用 ILogger<T> ,這個(gè)類型T將作為日志類別的名稱。這里我采用的是字符串方式,所以不使用 ILogger<T>。
2、BeginScope 方法的用處是當(dāng)你要把 logger 用在 using 語(yǔ)句塊時(shí)才會(huì)實(shí)現(xiàn)。正因?yàn)橛迷?using 塊中,所以它要求是實(shí)現(xiàn) IDisposable 接口。這個(gè)實(shí)現(xiàn) IDisposable 的類一般不用公開(kāi)。這方法會(huì)接收一個(gè)泛型參數(shù) TState state。這個(gè)看你的需要了,運(yùn)行庫(kù)內(nèi)部調(diào)用經(jīng)常會(huì)用字典類型傳遞一些額外數(shù)據(jù)。這個(gè) TState 你可以自定義。此處我不需要把 logger 用在 using 語(yǔ)句塊中,所以直接返回 default(或null)。
3、IsEnabled 方法的功能是分析一下 logLevel 參數(shù)指定的日志級(jí)別當(dāng)前是否要輸出日志。如果需要輸出日志,返回 true;不想輸出日志返回 false。后面實(shí)現(xiàn)的 Log 方法中也會(huì)用到它,如果返回 false,那就不必去處理怎么輸出日志了。
4、Log 方法是核心。在此方法中你盡情發(fā)揮吧,你想怎樣輸出日志就在這里完成。比如你要用 Debug 類輸出,那就調(diào)用 Debug 類的成員輸出;你用控制臺(tái)輸出就調(diào)用 Console 類的成員。我這里是要把日志傳給 SignalR Hub 對(duì)象,讓其傳回給客戶端,故要調(diào)用靜態(tài)的 TransferLog 屬性。此屬性是委托類型,可以與方法綁定,因?yàn)樵蹅儾荒茉谶@里調(diào)用 Hub,Hub 是由 SignalR 組件自動(dòng)激活的。所以要用委托來(lái)間接實(shí)現(xiàn)傳遞。這個(gè)和事件的作用一樣,只是我不用事件成員罷了。
順便說(shuō)一下,我項(xiàng)目中的類是同時(shí)把日志寫(xiě)入數(shù)據(jù)庫(kù)的(不寫(xiě)文件了,寫(xiě)數(shù)據(jù)庫(kù)里好清理),這里老周為了讓示例簡(jiǎn)單,沒(méi)有加上寫(xiě)入數(shù)據(jù)的代碼。其實(shí)也沒(méi)啥難度的,就是在數(shù)據(jù)庫(kù)中加個(gè)表,用 EF Core 往表里 INSERT 一條記錄。
第二步,實(shí)現(xiàn) Provider。ILogger 咱們定義好了,但這個(gè) Kingking 日志記錄器可不是直接扔進(jìn)服務(wù)容器,而是通過(guò)叫 ILoggerProvider 的對(duì)象來(lái)創(chuàng)建實(shí)例。就相當(dāng)于一個(gè)工廠類。
public class KingkingLoggerProvider : ILoggerProvider { public ILogger CreateLogger(string categoryName) { return new KingkingLogger(categoryName); } public void Dispose() { return; } }
代碼很簡(jiǎn)單,沒(méi)啥玄機(jī)。不過(guò),為了調(diào)用方便,咱們可以封裝一個(gè)擴(kuò)展方法。
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(); // 添加控制臺(tái)日志輸出 o.AddConsole(); // 添加咱們自己寫(xiě)的日志記錄器 o.AddKingkingLogger(); });
第三步,實(shí)現(xiàn) Hub。Hub 是 SignalR 通信的“中心”類,當(dāng)訪問(wèn)的 URL 匹配時(shí)就會(huì)激活咱們的 Hub。自定義 Hub 只要從 Hub 類派生即可。
public class MyHub : Hub { public MyHub() { // 這里關(guān)聯(lián)的就是日志記錄類中的靜態(tài)委托 KingkingLogger.TransferLog = KingkingLogger_TransferLog; } private void KingkingLogger_TransferLog(string obj) { // 向所有客戶端發(fā)日志 Clients.All.SendAsync("onLogged", obj); } protected override void Dispose(bool disposing) { if(disposing) { // 實(shí)例釋放時(shí)移除關(guān)聯(lián) KingkingLogger.TransferLog = null; } base.Dispose(disposing); } }
邏輯很簡(jiǎn)單,就是有日志了就推送給客戶端。Clients.All 是把消息發(fā)給所有連接的客戶端。
這里順便提一下:Hub 是支持依賴注入的,即你可以在 MyHub 的構(gòu)造函數(shù)里注入你要用的組件,如 DBContext 等。這里我用不到其他組件,所以沒(méi)有注入。
在Web應(yīng)用程序初始化時(shí)要啟用 SignalR 相關(guān)服務(wù)。
var builder = WebApplication.CreateBuilder(args); …… builder.Services.AddSignalR(); var app = builder.Build();
還要 Map 一下終結(jié)點(diǎn),以綁定請(qǐng)求 Hub 的地址。
var builder = WebApplication.CreateBuilder(args); …… var app = builder.Build(); …… // 記得這個(gè) app.MapHub<MyHub>("/hub"); app.Run();
這里我設(shè)定的地址是 http://localhost/hub。
不要以為這樣就完事了,當(dāng)你運(yùn)行后用客戶端一測(cè)試,你會(huì)發(fā)現(xiàn)連毛都接收不到。這是因?yàn)?Hub 對(duì)象的默認(rèn)生命周期太短了,僅在用的時(shí)候?qū)嵗?,然后馬上 Dispose 了。然后你會(huì)想,那我重寫(xiě) OnConnectedAsync 方法,關(guān)聯(lián) TransferLog 委托;再重寫(xiě) OnDisConnectedAsync 方法,把 TransferLog 委托設(shè)置為 null。這個(gè)也是不行的,原因還是那個(gè)—— Hub 對(duì)象生命周期太短。
有什么辦法讓 Hub 長(zhǎng)壽一點(diǎn)呢?還真有,直接把 Hub 類型注冊(cè)進(jìn)服務(wù)器中,并使用單實(shí)例。
var builder = WebApplication.CreateBuilder(args); …… // 把Hub注冊(cè)為單實(shí)例 builder.Services.AddSingleton<MyHub>(); builder.Services.AddSignalR(); var app = builder.Build();
第四步,客戶端程序。客戶端并不是只能用 JS 來(lái)寫(xiě),.NET 團(tuán)隊(duì)也做了相關(guān)的 Nuget 包。在項(xiàng)目中引用一下。
<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>
在主窗口中放一個(gè)文本框,兩個(gè)按鈕。文本框顯示收到的日志,按鈕用來(lái)請(qǐng)求連接和斷開(kāi)連接。
using Microsoft.AspNetCore.SignalR.Client; namespace TestClient; public partial class Form1 : Form { // 連接對(duì)象 HubConnection hubConn; public Form1() { InitializeComponent(); // 初始化連接 var connBuilder = new HubConnectionBuilder() .WithUrl("http://localhost:6225/hub") .WithAutomaticReconnect(); hubConn = connBuilder.Build(); // 關(guān)聯(lián)方法 hubConn.On<string>("onLogged", OnLogRecv); } private void OnLogRecv(string msg) { // 服務(wù)器回調(diào),顯示收到的日志 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 = "已斷開(kāi)連接"; } } }
注意,在調(diào)用 On 方法時(shí),onLogged 要與服務(wù)器上指定的一致,否則服務(wù)器回調(diào)無(wú)效。
/*---------------- 服務(wù)器端 ------------------*/ private void KingkingLogger_TransferLog(string obj) { // 向所有客戶端發(fā)日志 Clients.All.SendAsync("onLogged", obj); } /*--------------------- 客戶端 -------------------*/ hubConn.On<string>("onLogged", OnLogRecv);
為了測(cè)試能否真的傳遞了日志,咱們?cè)诜?wù)端寫(xiě)幾個(gè) Mini-API 來(lái)驗(yàn)證。
app.MapGet("/", (ILoggerFactory logFact) => { ILogger logger = logFact.CreateLogger("MINI Main"); logger.LogInformation("歡迎來(lái)到圓環(huán)世界"); return "Hello Guy"; }); app.MapGet("/start", (ILoggerFactory logFact) => { ILogger logger = logFact.CreateLogger("MINI Go Go Go"); logger.LogWarning("游戲開(kāi)始了,你必須先和QB簽訂契約"); return "圓神啟動(dòng)"; }); app.MapGet("/shot", (ILoggerFactory loggerFact) => { ILogger logger = loggerFact.CreateLogger("MINI Wind"); logger.LogInformation("干得好,三發(fā)入魂"); return "第一局完勝"; });
同時(shí)啟動(dòng)服務(wù)端和客戶端試試吧。為了使測(cè)試更真實(shí),我啟動(dòng)了三個(gè)客戶端。觸發(fā)日志記錄,請(qǐng)調(diào)用任意一個(gè) API。
依次點(diǎn)擊三個(gè)窗口上的“連接”按鈕,確認(rèn)全部都連上。
然后依次調(diào)用那幾個(gè) mini API 試試。
可以看到,三個(gè)客戶端都收到日志推送了。
為了演示,沒(méi)有數(shù)據(jù)存儲(chǔ),所以如果客戶端沒(méi)有及時(shí)連接,會(huì)丟失前面的日志。老周的實(shí)際項(xiàng)目中是用數(shù)據(jù)庫(kù)存起來(lái),用的時(shí)候再取出來(lái)發(fā)給客戶端。默認(rèn)是發(fā)最近的 100 條。如果上位機(jī)要看全部,就調(diào)用一下 Hub 的方法,Hub 的代碼會(huì) select 整個(gè)日志表再發(fā)回。
到此這篇關(guān)于ASP.NET Core 使用SignalR推送服務(wù)器日志的文章就介紹到這了,更多相關(guān)ASP.NET Core推送服務(wù)器日志內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
asp.net實(shí)現(xiàn)刪除DataGrid的記錄時(shí)彈出提示信息
這篇文章主要介紹了asp.net實(shí)現(xiàn)刪除DataGrid的記錄時(shí)彈出提示信息,非常實(shí)用的功能,需要的朋友可以參考下2014-08-08Asp.net在線備份、壓縮和修復(fù)Access數(shù)據(jù)庫(kù)示例代碼
這篇文章主要介紹了Asp.net如何在線備份、壓縮和修復(fù)Access數(shù)據(jù)庫(kù),需要的朋友可以參考下2014-03-03Visual Studio(VS2017)配置C/C++ PostgreSQL9.6.3開(kāi)發(fā)環(huán)境
這篇文章主要為大家詳細(xì)介紹了Visual Studio(VS2017)配置C/C++,PostgreSQL9.6.3開(kāi)發(fā)環(huán)境,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07ASP.NET Core中的對(duì)象池化技術(shù)詳解
這篇文章主要為大家詳細(xì)介紹了ASP.NET Core中的對(duì)象池化技術(shù)的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-01-01服務(wù)器安全狗導(dǎo)致ASP.NET網(wǎng)站運(yùn)行出錯(cuò)的一個(gè)案例
這篇文章主要介紹了服務(wù)器安全狗導(dǎo)致ASP.NET網(wǎng)站運(yùn)行出錯(cuò)的一個(gè)案例,最后一并給出了解決方法,需要的朋友可以參考下2014-08-08國(guó)產(chǎn)化中的?.NET?Core?操作達(dá)夢(mèng)數(shù)據(jù)庫(kù)DM8的兩種方式(操作詳解)
這篇文章主要介紹了國(guó)產(chǎn)化之?.NET?Core?操作達(dá)夢(mèng)數(shù)據(jù)庫(kù)DM8的兩種方式,這里提供兩種方式是傳統(tǒng)的DbHelperSQL方式和Dapper?方式,每種方式給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-04-04asp.net實(shí)現(xiàn)Postgresql快速寫(xiě)入/讀取大量數(shù)據(jù)實(shí)例
本篇文章主要介紹了asp.net實(shí)現(xiàn)Postgresql快速寫(xiě)入/讀取大量數(shù)據(jù)實(shí)例,具有一定的參考價(jià)值,有興趣的可以了解一下2017-07-07.NET發(fā)送郵件的實(shí)現(xiàn)方法示例
這篇文章主要給大家介紹了關(guān)于.NET發(fā)送郵件的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用.net具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-06-06