系列
源碼地址:https://github.com/QQ2287991080/SignalRServerAndVueClientDemo
效果
老規矩先看最后效果
步驟
1、配置log4net日志
實現日志推送,首先需要配置log4net日志,然后定義一個全局異常捕獲器,用於捕獲錯誤寫入到日志文件。
先把nuget包安裝一下。
然后需要配置log4net的xml信息,右鍵web項目“添加”->“新建項”
找到Web配置文件->“命名”->"點擊添加"
然后把xml配置放入到config文件中,配置如下:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <log4net> <appender name="DebugAppender" type="log4net.Appender.DebugAppender" > <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%date [%thread] %-5level %logger - %message%newline" /> </layout> </appender> <!--全局異常日志--> <appender name="RollingFile" type="log4net.Appender.RollingFileAppender"> <!--日志文件存放位置--> <file value="../../../logs/system.log" /> <!--是否追加到日志文件中--> <appendToFile value="true" /> <!--基於文件大小滾動設置--> <rollingStyle value="Composite" /> <!--是否指定了日志文件名稱--> <staticLogFileName value="true" /> <!--根據日期生成日志文件--> <!--<datePattern value="yyyyMMdd'.log'" />--> <!--最多保留10個舊文件--> <maxSizeRollBackups value="10" /> <!--日志文件的大小--> <maximumFileSize value="1GB" /> <layout type="log4net.Layout.PatternLayout"> <!--日志模板,這個東西很重要后續讀取日志文件的時候就是依據這個配置--> <conversionPattern value="%n時間:%date{yyyy-MM-dd HH:mm:ss},%n線程Id:%thread,%n日志級別:%-5level,%n描述:%message|%newline"/> </layout> </appender> <root> <level value="All"/> <appender-ref ref="DebugAppender" /> <appender-ref ref="RollingFile" /> </root> </log4net> </configuration>
想要更多配置的可以前往官網:http://logging.apache.org/log4net/release/config-examples.html
如果對生成多個文件夾有興趣的可以看我另外:Asp.Net Core Log4Net 配置分多個文件記錄日志(不同日志級別)
接下來就需要在Startup中配置log4net.
public Startup(IConfiguration configuration) { Configuration = configuration; Logger = LogManager.CreateRepository(Assembly.GetEntryAssembly(), typeof(log4net.Repository.Hierarchy.Hierarchy)); XmlConfigurator.Configure(Logger, new FileInfo("log4net.config")); // _logger = LogManager.GetLogger(Logger.Name, typeof(Startup)); } public static ILoggerRepository Logger { get; set; }
按照我最開始說的,在配置好日志之后需要配置一個全局錯誤捕獲器,直接上代碼。
public class SysExceptionFilter : IAsyncExceptionFilter { readonly IHubContext<ChatHub> _hub; //使用log4 ILog _log = LogManager.GetLogger(Startup.Logger.Name, typeof(SysExceptionFilter)); public SysExceptionFilter(IHubContext<ChatHub> hub) { _hub = hub; } public async Task OnExceptionAsync(ExceptionContext context) { //錯誤 var ex = context.Exception; //錯誤信息 string message = ex.Message; //請求方法的路由 string url = context.HttpContext?.Request.Path; //寫入日志文件描述 注意這個地方盡量不要用中文冒號,否則讀取日志文件的時候會造成信息確實,當然你可以定義自己的規則 string logMessage = $"錯誤信息=>【{message}】,【請求地址=>{url}】"; //寫入日志 _log.Error(logMessage); //讀取日志 var data = ReadHelper.Read(); //發送給客戶端 await _hub.Clients.All.SendAsync("ReceiveLog", data); //返回一個正確的200http碼,避免前端錯誤 context.Result = new JsonResult(new { ErrCode = 0, ErrMsg = message, Data = true }); } }
代碼中的讀取日志會在第二節中講到。
在Startup服務中注冊這個過濾器。
public void ConfigureServices(IServiceCollection services) { ...... services.AddMvc(option => { //添加錯誤捕獲 option.Filters.Add(typeof(SysExceptionFilter)); //option.EnableEndpointRouting = false; }); ...... }
按照我這個配置將會在程序目錄生成一個logs文件夾,以及一個system.log文件。
2、讀取日志文件
在配置日志文件中已經將日志配置了,再看看生成日志文件內容。
跟我在log4net.config中配置的是一樣的。
<layout type="log4net.Layout.PatternLayout"> <!--日志模板,這個東西很重要后續讀取日志文件的時候就是依據這個配置--> <conversionPattern value="%n時間:%date{yyyy-MM-dd HH:mm:ss},%n線程Id:%thread,%n日志級別:%-5level,%n描述:%message|%newline"/> </layout>
然后需要讀取日志文件的,把日志文件的內容轉換成前端能夠識別的數據。
public class ReadHelper { /// <summary> /// https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.readerwriterlockslim?view=netframework-4.8 /// 這里主要控制控制多個線程讀取日志文件 /// </summary> static ReaderWriterLockSlim _slimLock = new ReaderWriterLockSlim(); public static List<SysExceptionData> Read(string filePath="") { //日志對象集合 List<SysExceptionData> datas = new List<SysExceptionData>(); filePath = Directory.GetCurrentDirectory() + "\\logs\\system.log"; //判斷日志文件是否存在 if (!File.Exists(filePath)) { return datas; } _slimLock.EnterReadLock(); try { //獲取日志文件流 var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); //讀取內容 var reader = new StreamReader(fs); var content = reader.ReadToEnd(); reader.Close(); fs.Close(); /* *處理內容,換行符替換掉,然后在log4net配置文件中在每一寫入日志結尾的地方加上 | *這樣做的好處是便於在讀取日志文件的時候處理日志數據返回給客戶端 *由於是在每一行結束的地方加上| 所有根據Split分割之后最后一個數據必然是空的 *所有Where去除一下。 */ var contentList = content.Replace("\r\n", "").Split('|').Where(w => !string.IsNullOrEmpty(w)); foreach (var item in contentList) { //根據逗號分割單個日志數據的內容 var info = item.Split(','); //實例化日志對象 SysExceptionData data = new SysExceptionData(); data.CreateTime = Convert.ToDateTime(info[0].Split(':')[1]); data.Level = info[2].Split(':')[1]; data.Summary = info[3].Split(':')[1]; datas.Add(data); } } finally { //退出 _slimLock.ExitReadLock(); } return datas.OrderByDescending(bo=>bo.CreateTime).ToList(); } } public class SysExceptionData { /// <summary> /// 時間 /// </summary> public DateTime CreateTime { get; set; } /// <summary> /// 日志級別 /// </summary> public string Level { get; set; } /// <summary> /// 日志描述 /// </summary> public string Summary { get; set; } }
這里需要說一下的是為什么要用ReaderWriterLockSlim,其實在寫這篇博客之前我剛好看書學到這個東西。
來一段原文描述:
通常一個類型實例的並發讀操作是線程安全的,而並發更新操作則不是。諸如文件這樣的資源也具有相同的特點。
雖然可以簡單的使用一個排它鎖來保護對實例的任何形式的訪問。
但是如果其讀操作很多但是更新操作很少,則使用單一的鎖限制並發性就不大合理了。
這種情況出現在業務應用服務器上,它會將常用的數據緩存在靜態字段中進行快速檢索。
ReaderWriterLockSlim是專門為這種情形設計的,它可以最大限度的保證鎖的可用性。ReaderWriterLockSlim在.net3.5引入的它替代了笨重的ReaderWriterLock類。雖然兩者功能相識,但是后者的執行速度比前置慢數倍。ReaderWriteLockSlim和ReaderWriterLock都擁有兩種基本鎖,讀和寫。
寫鎖是全局排它鎖
讀鎖可以兼容其他的鎖
因此,一個持有寫鎖的線程將阻塞其他任何試圖獲取讀鎖或寫鎖的京城。但是如果沒有任何線程持有寫鎖的話,那么任意數量的線程都可以獲得讀鎖。
ReaderWriterLockSlim和lock一樣也有類似TryEnter之類的方法,來判斷是否超時,如果超時就拋出錯誤(lock返回false)
這是關於ReaderWriterLockSlim官網最新的描述:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.readerwriterlockslim?view=netframework-4.8
對了,我看的是孔雀鳥--《c# 7.0核心技術指南》c#想進階強烈推薦這本書。
同時這部分代碼也有參考老張的Blog.Core的源碼,感謝!
接下來調試一下看看讀取日志文件處理后的數據,我在TestController加了故意拋出錯誤的接口。
直接在瀏覽器輸入 :http://localhost:13989/api/test/getLog
成功進入斷點
shift+f9監聽data看看數據
拿到這個數據,在客戶端就直接可以用來展示,那么讀取日志文件這部分就說完了,然后再說如何發送日志給客戶端。
3、實時發送日志數據
在日志過濾器中有這樣一段代碼,玩過signalr的人都知道SendAsync的第一個字符串其實是集線器中方法(Hub)的名稱,但是我們也是可以自定義它的名稱的。
//發送給客戶端 await _hub.Clients.All.SendAsync("ReceiveLog", data);
signalr強類型中心:https://docs.microsoft.com/zh-cn/aspnet/core/signalr/hubs?view=aspnetcore-3.1#change-the-name-of-a-hub-method
之前用的Hub不是強類型中心,這次一並給他改造了。
/// <summary> /// https://docs.microsoft.com/zh-cn/aspnet/core/signalr/hubs?view=aspnetcore-3.1 /// 強類型中心 /// </summary> public interface IChatClient { Task ReceiveMessage(string user, string message); Task ReceiveMessage(object message); Task ReceiveCaller(object message); Task ReceiveLog(object data); }
重構源碼之前的方法。
public class ChatHub : Hub<IChatClient> { /// <summary> /// 給所有客戶端發送消息 /// </summary> /// <param name="user">用戶</param> /// <param name="message">消息</param> /// <returns></returns> public async Task SendMessage(string user, string message) { await Clients.All.ReceiveMessage(user, message); } /// <summary> /// 向調用客戶端發送消息 /// </summary> /// <param name="message"></param> /// <returns></returns> public async Task SendMessageCaller(string message) { await Clients.Caller.ReceiveCaller( message); } /// <summary> /// 客戶端連接服務端 /// </summary> /// <returns></returns> public override Task OnConnectedAsync() { var id = Context.ConnectionId; //_logger.Info($"客戶端ConnectionId=>【{id}】已連接服務器!"); return base.OnConnectedAsync(); } /// <summary> /// 客戶端斷開連接 /// </summary> /// <param name="exception"></param> /// <returns></returns> public override Task OnDisconnectedAsync(Exception exception) { var id = Context.ConnectionId; //_logger.Info($"客戶端ConnectionId=>【{id}】已斷開服務器連接!"); return base.OnDisconnectedAsync(exception); } public async Task ReceiveLog(object data) { data = ReadHelper.Read(); await Clients.All.ReceiveLog(data); } }
ps:這個改動不會影響它在控制器注入,或者其它注入地方的使用。
其實服務端的配置差不多好了,現在需要想的是在客戶端,首次進入頁面的時候是應該手動給他調用一次發送日志,否則進入頁面是沒有數據的。
然后我在TestController中加上一個接口手動觸發
[HttpGet] public async Task<JsonResult> GetLogMessage() { var data = ReadHelper.Read(); await _hubContext.Clients.All.SendAsync("ReceiveLog", data); return new JsonResult(0); }
🆗,接下來需要把注意力集中到客戶端上了,
之前的兩篇博客我是沒有安裝element-ui的,這一次我為了展示數據省事,就打算直接用element-table展示數據好了。
element官網:https://element.eleme.cn/#/zh-CN/component/installation
npm i element-ui -S
在mian.js添加配置
//element import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css'
vue 這里我不敢亂講,這個我也不是很會,所以直接放代碼了,我把客戶端直接的代碼進行了一下改造,加了個菜單,然后之前的內容都放在不同的菜單。
<template> <div class="home"> <h1>服務端錯誤日志返回</h1> <button @click="sendErr">執行一個錯誤</button> <div class="table"> <el-table :data="tableData" border style="width: 100%"> <el-table-column type="index" label="序號" width="100"></el-table-column> <el-table-column prop="createTime" label="日期" width="180"></el-table-column> <el-table-column prop="level" label="級別" width="100"></el-table-column> <el-table-column prop="summary" label="描述" width="300"></el-table-column> </el-table> </div> </div> </template> <script> // @ is an alias to /src import HelloWorld from "@/components/HelloWorld.vue"; import * as signalR from "@aspnet/signalr"; export default { name: "Home", components: { HelloWorld, }, data() { return { message: "", //消息 connection: "", //signalr連接 messages: [], //返回消息 tableData: [], }; }, methods: { //發出一個錯誤 sendErr: function () { this.$http.get("http://localhost:13989/api/test/getLog").then((resp) => { //console.log(resp); }); }, //獲取系統日志 getLog: function () { this.$http .get("http://localhost:13989/api/test/GetLogMessage") .then((res) => { console.log(res); }); }, getdatalist: function () { this.$http .get("http://localhost:13989/api/test/GetLogMessage") .then((res) => { // console.log(res); //this.tableData = res.data; }) .catch((err) => { console.log(err); }); }, }, computed: {}, mounted: function () { let thisVue = this; this.connection = new signalR.HubConnectionBuilder() .withUrl("http://localhost:13989/chathub", { skipNegotiation: true, transport: signalR.HttpTransportType.WebSockets, }) .configureLogging(signalR.LogLevel.Information) .build(); this.connection.start(); //連接日志發送事件 this.connection.on("ReceiveLog", function (message) { console.log("listening receivelog"); thisVue.tableData = message; }); //初始化表格數據 thisVue.getdatalist(); }, }; </script> <style scoped> .table { margin: 20px; } </style>
啟動看看效果。
這是日志接口展示的客戶端頁面
之前博客的內容在聊天中。。
來個gif看看效果
結語
今天的分享到這里就結束了,內心覺得寫一篇博客真不容易,從這個想法的萌芽到寫demo去實現大概花了一周,不斷地去看資料,研究源碼。
俗話說,人不逼自己一下,不知道有多少潛力。
最后希望博客能夠幫助到需要的人,后續還想研究下signalr 配置jwt,redis,sqlserver等。
Dome源碼地址:https://github.com/QQ2287991080/SignalRServerAndVueClientDemo
學習使我快樂!!!