1. 程式人生 > >Log2Net元件程式碼詳解(附開原始碼)

Log2Net元件程式碼詳解(附開原始碼)

上一篇,我們介紹了Log2Net的需求和整體框架,我們接下來介紹我們是如何用程式碼實現Log2Net元件的功能的。

一、整體介紹

  Log2Net元件本身是一個Dll,供其他系統呼叫。

  本部分由以下幾部分組成:

  1. 日誌平臺實體定義;
  2. 工具方法定義,包括ComUtil(例如快取幫助類、序列化幫助類、訊息佇列幫助類等)和DBUtil(例如Sql server幫助類、Oracle幫助類、MySql幫助類、EF幫助類等);
  3. 日誌資訊獲取類(例如如獲取客戶端、伺服器端資訊,寫日誌資料到訊息佇列等);
  4. .NetCore中介軟體定義(例如HttpContext中介軟體、錯誤訊息處理中介軟體等);
  5. Config配置類(包括Log2NetConfigurationSectionHandler類、訊息佇列管理類等);
  6. 日誌追加器類(FileAppender、DirectDBAppender、MQ2DBAppender等);
  7. 外部介面LogApi類(例如元件註冊類、寫日誌類等);

  使用的第三方類庫有RabbitMQ訪問類庫RabbitMQ.Client、InfluxDB訪問類庫InfluxData.Net、快取元件CacheManager、物件對映元件AutoMapper、快取工具Microsoft.Extensions.Caching、Microsoft.AspNetCore.Session等。使用NuGet工具下載安裝這些類庫,會自動檢測和匹配當前.NET版本並安裝其他依賴。請儘量不要手動下載類庫安裝,可能會出現各種各樣的不相容、缺少依賴庫的情況。

  本元件使用VS2017開發,為類庫專案,支援.net4.5~netCore2.1。若您把原始碼下載下來,而您的電腦上某個某個.Net版本,請在csproj檔案中的TargetFrameworks中移除該net版本。

  為了測試該元件,分別添加了一個.NET4.5的MVC專案和.netCore2.0的MVC專案。專案檔案圖如下圖所示:

  本專案程式碼已開源,地址為 https://github.com/yuchen1030/Log2Net ,您可以參照程式碼理解下述的設計。

二、模型實體Models類庫

  模型實體包括定義在ModelsInDB.cs中資料庫中使用的模型、定義在ModelsUI.cs中外部介面使用的模型、定義在ModelsInCode.cs中本類庫程式碼中使用的模型。

  系統中的操作軌跡資料的資料庫實體為Log_OperateTrace,監控資料資料庫實體為Log_SystemMonitor。程式碼中以這兩個實體為核心定義了其他資料實體,具體參見程式碼。 

三、工具方法Util定義

  本部分包括公共工具ComUtil類和DBUtil類。

3.1 ComUtil類

  該類庫(Util類庫)中,封裝了了一些公共的方法和類,如下表所示:

檔名

用途描述

AppConfig

配置檔案讀寫類

AutoMapperHelper

物件對映幫助類

CacheHelper.cs

快取操作類

DtModelConvert.cs

泛型Model和DataTable互轉操作類

LambdaToSqlHelper

Lambda表示式轉Sql幫助類

RabbitMQHelper.cs

RabbitMQ訊息佇列幫助類

SerializerHelper.cs

序列化反序列化幫助類

StringEnum.cs

字串列舉類

XmlSerializeHelper.cs

Xml和實體轉換類

   這些類是通用的方法封裝,與具體業務邏輯無關,其他系統可以借鑑使用。

3.2 DBUtil類

  這些類是用來訪問各種資料庫的方法的封裝。包括對Sql Server、Oracle、MySql、InfluxDB等4種資料庫的訪問。若您需要新增對其他資料(如Access、SQLite、PostgreSQL等)的支援,請在此部分下新增。

  對常用的資料庫,本程式碼中使用了兩種方式進行訪問:ADO.net方式和EF方式,如果您需要使用NHibernate/SqlSugar/Dapper等其他方式,也請在該部分下新增。

3.2.1 AdoNet方式訪問資料庫

  該部分是使用ADO.Net方法直接訪問資料庫,因為要支援SqlServer,Oracle,MySql等多種資料庫,支援多個數據庫實體,它們需要遵循相同的介面契約,有一些共同的實現方法,因此定義了泛型介面類和泛型基礎類。類圖如下所示:

 

   在泛型介面IAdoNetBase中,定義了新增和獲取資料的方法,如下所示:

1     internal interface IAdoNetBase<T> where T : class
2     {
3         ExeResEdm Add(string tableName, T model, params string[] skipCols);
4         ExeResEdm GetListByPage(string tableName, PageSerach<T> para);
5     }
View Code

  資料庫訪問基礎類AdoNetBase為抽象類,定義了各種資料庫共用的一些基礎方法,如下圖所示:

  在實現這些公共方法的時候,各種資料庫的實現方法不同,因此需要定義抽象方法,子類需要實現它。

  例如介面的public ExeResEdm Add(string tableName, T model, params string[] skipCols)方法需要呼叫私有方法ExecuteNonQuery,而該私有方法的定義如下:

1         ExeResEdm ExecuteNonQuery(string cmdText, params DbParameter[] parameters)
2         {
3             ExeResEdm dBResEdm = SqlCMD(cmdText, cmd => cmd.ExecuteNonQuery(), parameters);
4             if (dBResEdm.ErrCode == 0)
5             {
6                 dBResEdm.ExeNum = Convert.ToInt32(dBResEdm.ExeModel);
7             }
8             return dBResEdm;
9         }
View Code

  該ExecuteNonQuery方法中要呼叫SqlCMD方法,而各種資料庫中SqlCMD方法方法實現不同,因此需要SqlCMD方法為抽象方法,各子類需要各自實現之。以下分別列出SqlServer和MySql中SqlCMD方法的實現:

 1         protected override ExeResEdm SqlCMD(string sql, Func<DbCommand, object> fun, params DbParameter[] pms)
 2         {
 3             ExeResEdm dBResEdm = new ExeResEdm();
 4             try
 5             {
 6                 pms = ParameterPrepare(pms);
 7                 using (SqlConnection con = new SqlConnection(connstr))
 8                 {
 9                     using (SqlCommand cmd = new SqlCommand(sql, con))
10                     {
11                         con.Open();
12                         if (pms != null && pms.Length > 0)
13                         {
14                             cmd.Parameters.AddRange((pms));
15                         }
16                         var res = fun(cmd);
17                         dBResEdm.ExeModel = res;
18                         return dBResEdm;
19                     }
20                 }
21             }
22             catch (Exception ex)
23             {
24                 dBResEdm.Module = "SqlCMD方法";
25                 dBResEdm.ExBody = ex;
26                 dBResEdm.ErrCode = 1;
27                 return dBResEdm;
28             }
29         }
View Code
 1         protected override ExeResEdm SqlCMD(string sql, Func<DbCommand, object> fun, params DbParameter[] pms)
 2         {
 3             ExeResEdm dBResEdm = new ExeResEdm();
 4             try
 5             {
 6                 pms = ParameterPrepare(pms);
 7                 using (MySqlConnection con = new MySqlConnection(connstr))
 8                 {
 9                     using (MySqlCommand cmd = new MySqlCommand(sql, con))
10                     {
11                         con.Open();
12                         if (pms != null && pms.Length > 0)
13                         {
14                             cmd.Parameters.AddRange((pms));
15                         }
16                         var res = fun(cmd);
17                         dBResEdm.ExeModel = res;
18                         return dBResEdm;
19                     }
20                 }
21             }
22             catch (Exception ex)
23             {
24                 dBResEdm.Module = "SqlCMD方法";
25                 dBResEdm.ExBody = ex;
26                 dBResEdm.ErrCode = 1;
27                 return dBResEdm;
28             }
29         }
View Code

  基礎類中的其他方法也是類似的套路,在此不再贅述,具體請參見原始碼。

  在定義了介面和基礎方法之後,各個子類就可以在此基礎上繼承和實現它們了,本程式碼中的子類是SqlServerHelper,OracleHelperBase,MySqlHelper三個,分別實現對SqlServer,oracle,MySql資料庫的訪問。這些子類中的方法就是對基類方法的重寫,例如SqlServerHelper定義如下:

  

  對oracle資料庫,建議使用Oracle.ManagedDataAccess.Client實現的oracle 資料庫訪問類OracleHelper(無需安裝客戶端),無32位/64位之分,使用方便,效能好。但該類庫僅支援Oracle10g及以上,因此又使用System.Data.OracleClient實現的oracle 資料庫訪問類OracleHelperMS。這兩個類的程式碼可以是一模一樣的,只是引用的類庫不同(Oracle.ManagedDataAccess.Client和System.Data.OracleClient)。這兩個類可以合併為一個,只需要新增如下程式碼:

1 //#define MS_OracleClient  // 是採用微軟oracle類庫還是oracle自家的類庫
2 
3 #if MS_OracleClient
4 using System.Data.OracleClient;
5 #else
6 using Oracle.ManagedDataAccess.Client;
7 #endif
View Code

  若您還需要支援其他資料庫型別,請繼承和實現 AdoNetBase<T>, IAdoNetBase<T> 即可。

 3.2.2 資料訪問層Dal

  上面我們定義了ADO.Net訪問資料的方法,EF方法只需引用類庫即可。工具已備好,我們接下來就可以使用ADO.Net方法或EF方法訪問具體的資料庫表了。類圖如下圖所示:

  首先,我們定義一個泛型抽象類DBAccessDal(也可以定義為介面),裡面定義了需要實現的獲取資料方法和新增資料的方法:

1     internal abstract class DBAccessDal<T> where T : class
2     {
3         internal abstract ExeResEdm GetAll(PageSerach<T> para);
4 
5         internal abstract ExeResEdm Add(AddDBPara<T> dBPara);
6 
7     }
View Code

  然後,分別定義ADO.Net方法訪問資料的基類AdoNetBaseDal和EF方法訪問資料庫的基類EFBaseDal:

                                 

  最後,根據上一步中的基類,實現Log_OperateTrace和Log_SystemMonitor的資料訪問子類,如下圖:

  ADO.Net方式中,基類中已指明瞭資料庫連線物件,Dal中只需要呼叫相關方法即可。EF方式中,前文寫的程式碼很少,但欠債總是要還的,這裡需要額外定義繼承自DbContext的Log_OperateTraceContext和Log_SystemMonitorContext來指定資料庫上下文。

3.2.3 資料庫訪問方式工廠

  上文中,介紹了資料庫又ADO.Net方式和EF訪問方式,我們可以在配置檔案中配置使用ADO.Net方式或EF方式,這是通過工廠模式實現的,類圖如下:

  例如Log_OperateTraceDBAccessFac定義如下:

 1     internal class Log_OperateTraceDBAccessFac : DBAccessFac<Log_OperateTrace>
 2     {
 3         protected override DBAccessDal<Log_OperateTrace> GetDalByDBAccessType(DBAccessType dbAccessType)
 4         {
 5             if (dbAccessType == DBAccessType.EF)
 6             {
 7                 Log_OperateTraceEFDal log_OperateTraceDal = new Log_OperateTraceEFDal(new Log_OperateTraceContext());
 8                 return log_OperateTraceDal;
 9             }
10             else if (dbAccessType == DBAccessType.NH)
11             {
12                 throw new Exception("Not define dal methods when DBAccessType = NH");
13             }
14             else
15             {
16                 return new Log_OperateTraceAdoDal();
17             }
18 
19         }
20     }
View Code

  另外,還有資料庫功能公共類ComDBFun和InfluxDB訪問類InfluxDBHelper的介紹略。

  至此,資料庫訪問幫助類介紹完畢,詳情請參閱DBUtil部分程式碼。 

四、日誌資訊獲取類

  本部分定義了日誌元件使用的基礎方法,如客戶端伺服器資訊ClientServerInfo類、線上人數訪客人數統計VisitOnlineCount 類、日誌元件公共類LogCom.cs。

4.1 ClientServerInfo類

  該類庫用於收集客戶端和伺服器端的資訊,包括客戶端資訊子類ClientInfo和伺服器端資訊子類ServerInfo。

  ClientInfo類用來獲取客戶端的ip地址、主機名、Mac地址、瀏覽器資訊等。

  ServerInfo類用來獲取伺服器端的ip地址、主機名、作業系統、CLR版本、伺服器執行時間、可用硬碟空間、CPU使用率、記憶體使用率等資訊。

4.2 訪客人數統計類VisitOnlineCount類

  本類中定義了線上人數和訪客統計抽象類IVisitCount類,具體的類要實現該類中的抽象方法。

  對.net平臺,存在Session_Start和Session_End事件,訪客統計的實現思路較為清晰,本元件提供了兩種方案:使用Application物件實現、使用快取實現。具體採用哪種方案由簡單工廠決定,預設採用快取方案。

  對.NetCore平臺,不存在Session_Start和Session_End事件事件,需要藉助於HttpContext中介軟體來實現。在HttpContext中,儲存了所有的SessionID,若Session過期,則視為該SessionID離線。據此就可以統計出線上人數和歷史訪客。

4.3 公共類LogCom類 

  本類中定義一些本元件內部使用的公共類,主要是寫檔案的類、日誌實體封裝類,實現非常簡單,類圖如下:

五、日誌追加器Appender類庫  

  日誌追加器用於將封裝後的日誌實體寫到媒介中。根據追加方式的不同,實現方案也不同。

  日誌追加方式有寫到檔案、ADO方式寫到資料庫、通過訊息佇列寫到資料庫三種。相應的有FileAppender、DirectDBAppender、MQ2DBAppender三種追加器。這三種追加器都實現了公共的追加器BaseAppender類。類圖如下:

                     

  公共追加器BaseAppender為抽象類中,定義了兩個抽象的WriteLog方法,分別用來寫使用者操作日誌和系統執行日誌。

  BaseAppender類中還定義了寫日誌的WriteLogAndHandFail方法和WriteLogAgain方法,兩者的區別在於前者在失敗時要寫備份日誌,引數為集合型別,在初次將日誌寫到媒介中使用;後者在失敗時不進行其他處理,引數為單一實體,在讀備份日誌到媒介中使用。 

  FileAppender、DirectDBAppender、MQ2DBAppender這三種追加器實現自己的WriteLog方法。MQ2DBAppender是通過訊息佇列寫到資料庫,除繼承自DirectDBAppender的方法外,還需要開啟訊息佇列消費執行緒,將訊息佇列中的日誌寫到資料庫中:開啟StartWriteTraceDataService執行緒啟動寫trace日誌資料服務,開啟StartWriteMonitorDataService執行緒啟動寫monitor日誌資料服務。在這兩個服務中,都是啟動佇列服務,檢查佇列,若有訊息則寫資料到資料庫。只不過是一個佇列用於寫軌跡日誌,一個佇列用於寫監控日誌。 

  在寫資料庫中時,一方面寫到SQL資料庫中,便於讀寫分離的實現,另一方面寫到時序資料庫InfluxDB中,便於以後使用Grafana、ELK等工具進行更加靈活優雅的監控。

  使用者可以通過配置來決定使用哪一種追加器,程式碼中通過追加器工廠類AppenderFac,得到相應的追加器工廠例項,呼叫該追加器的方法進行日誌的記錄。

六、.NetCore中介軟體DNCMiddleware類庫

  日誌追加器用於將封裝後的日誌實體寫到媒介中。根據追加方式的不同,實現方案也不同。

  .NetCore中沒有Application_Error事件來捕捉全域性異常,沒有HttpContext.Current來儲存當前請求的資訊,需要我們自定義中介軟體來實現。

61 異常處理中介軟體ErrorHandlingMiddleware

  在這裡定義了異常處理中介軟體,在捕捉到異常時,將異常日誌進行記錄。

 

 1     internal class ErrorHandlingMiddleware
 2     {
 3         private readonly RequestDelegate next;
 4 
 5         public ErrorHandlingMiddleware(RequestDelegate next)
 6         {
 7             this.next = next;
 8         }
 9 
10         public async Task Invoke(Microsoft.AspNetCore.Http.HttpContext context)
11         {
12             try
13             {
14                 await next(context);
15             }
16             catch (Exception ex)
17             {
18                 var statusCode = context.Response.StatusCode;
19                 if (ex is ArgumentException)
20                 {
21                     statusCode = 200;
22                 }
23                 await HandleExceptionAsync(context, statusCode, ex.Message);
24             }
25             finally
26             {
27                 var statusCode = context.Response.StatusCode;
28                 var msg = "";
29                 if (statusCode == 401)
30                 {
31                     msg = "未授權";
32                 }
33                 else if (statusCode == 404)
34                 {
35                     msg = "未找到服務";
36                 }
37                 else if (statusCode == 502)
38                 {
39                     msg = "請求錯誤";
40                 }
41                 else if (statusCode != 200 && statusCode != 302)
42                 {
43                     msg = "未知錯誤" + statusCode;
44                 }
45                 if (!string.IsNullOrWhiteSpace(msg))
46                 {
47                     await HandleExceptionAsync(context, statusCode, msg);
48                 }
49             }
50         }
51 
52         private static Task HandleExceptionAsync(Microsoft.AspNetCore.Http.HttpContext context, int statusCode, string msg)
53         {
54             var data = new { code = statusCode.ToString(), is_success = false, msg = msg };
55             var result = JsonConvert.SerializeObject(new { data = data });
56 
57             Log_OperateTraceBllEdm exLog = new Log_OperateTraceBllEdm()
58             {
59                 Detail = result,
60                 LogType = LogType.異常,
61                 Remark = "異常時間" + DateTime.Now,
62                 TabOrModu = "異常模組",
63             };
64             LogApi.WriteLog( LogLevel.Error,exLog);
65 
66             context.Response.ContentType = "application/json;charset=utf-8";
67             return context.Response.WriteAsync(result);
68         }
69     }
70 
71 
72     public static class ErrorHandlingExtensions
73     {
74         public static IApplicationBuilder UseErrorHandling(this IApplicationBuilder builder)
75         {
76             return builder.UseMiddleware<ErrorHandlingMiddleware>();
77         }
78     }
View Code

 

6.2 請求上下文中介軟體HttpContext

  在這裡,定義了請求上下文的中介軟體,記錄了當前求請求的上下文,模擬了當前上下文的Session資訊,將所有的SessionId儲存起來,將過期的SessionId移除,來實現線上人數統計和歷史訪客統計。 

 1     internal static class HttpContext
 2     {
 3         public class SessionEdm
 4         {
 5             public string Key { get; set; }
 6             public string Val { get; set; }
 7             public DateTime ExpiresAtTime { get; set; }
 8         }
 9 
10 
11         public static Microsoft.AspNetCore.Http.HttpContext Current => _accessor.HttpContext;
12 
13         static ConcurrentDictionary<string, SessionEdm> sessionMaps = new ConcurrentDictionary<string, SessionEdm>();
14 
15         static double dncSessionMins = AppConfig.GetDncSessionTimeoutMins();
16 
17         private static IHttpContextAccessor _accessor;
18         internal static void Configure(IHttpContextAccessor accessor)
19         {
20             _accessor = accessor;
21         }
22 
23         public static VOEdm GetOnlineVisitNum(int preVisitNum)
24         {          
25             if (_accessor.HttpContext != null)
26             {
27                 var curSession = _accessor.HttpContext.Session;
28                 SessionEdm sessionEdm = new SessionEdm() { Key = curSession.Id, Val = "1", ExpiresAtTime = DateTime.Now.AddMinutes(dncSessionMins) };
29                 sessionMaps.TryAdd(curSession.Id, sessionEdm);
30             }
31             int visitorsNum = sessionMaps.Count;
32             VOEdm vOEdm = new VOEdm() { VisitNum = preVisitNum + visitorsNum };
33             //將過期session的值變為0,未過期的session的數量為線上人數
34             var keys = sessionMaps.Keys.ToArray();
35             for (int i = 0; i < sessionMaps.Count; i++)
36             {
37                 var cur = sessionMaps[keys[i]];
38                 if (cur.Val == "1" && cur.ExpiresAtTime <= DateTime.Now) //已過期
39                 {
40                     cur.Val = "0";
41                 }
42             }
43             var onlineNums = sessionMaps.Where(a => a.Value.Val == "1").Count();
44             vOEdm.OnlineNum = onlineNums;
45             return vOEdm;
46         }
47 
48     }
49 
50     public static class StaticHttpContextExtensions
51     {
52         public static void AddHttpContextAccessor(this IServiceCollection services)
53         {
54             services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
55         }
56 
57         public static IApplicationBuilder UseStaticHttpContext(this IApplicationBuilder app)
58         {
59             var httpContextAccessor = app.ApplicationServices.GetRequiredService<IHttpContextAccessor>();
60             HttpContext.Configure(httpContextAccessor);
61             return app;
62         }
63     }
View Code

 七、外部介面類LogApi類

  在本類中,呼叫其他類的方法,形成供其他業務系統呼叫的方法。包含以下內容:日誌元件註冊、寫日誌等。各業務系統不需要關心這些方法的具體實現,只需要封裝業務實體,呼叫寫日誌的方法即可。LogApi類會呼叫其他的類,如等來實現日誌記錄的功能。類圖如下:

  • RegisterLogInitMsg:註冊日誌元件到本系統,為日誌元件準備基礎資訊:伺服器IP、伺服器主機名,系統名稱等;使用EF自動建立資料;並呼叫WriteServerStartupLog方法寫啟動日誌,呼叫WriteMonitorLogThread方法寫定時監控日誌。
  • GetLogWebApplicationsName:從配置檔案中獲取使用者自定義的系統名稱。
  • 這裡還包含網站生命週期事件中的日誌記錄,如下:
  • WriteServerStartupLog:伺服器啟動時,獲取作業系統,.NET CLR版本;
  • WriteFirstVisitLog:網站被初次訪問,記錄記錄IIS版本;
  • WriteServerStopLog:伺服器停止時,獲取已執行時間;
  • WriteServerStartupLog:系統異常時,記錄異常日誌;
  • IncreaseOnlineVisitNum:Session Start時,線上人數和訪客人數加1;
  • ReduceOnlineNum:Session end時,線上人數減1。

  以上的生命週期事件中,有些僅在.net中可以使用,.netCore中不存在,要實現類似的功能,就需要使用netCore中介軟體來實現。AddLog2netService和AddLog2netConfigure分別用來註冊Log2net服務和Log2net配置。

  本類中定義了4個寫日誌的方法:

  • WriteLog方法過載(2個):封裝日誌實體,呼叫日誌追加器的方法將日誌寫到媒介中,分別對業務操作和監控資料進行寫。
  • WriteMsgToDebugFile:寫除錯日誌寫到檔案中,可通過bWriteInfoToDebugFile配置是否開啟。
  • WriteInfoToFile:將將日記寫到本地檔案中,記錄一些重要但又不必寫入log日誌媒介的資訊。

  Log_OperateTraceBllEdm為操作軌跡類業務實體,LogMonitorEdm為監控資訊實體,各業務系統將資訊封裝進這兩個實體,然後呼叫WriteLog方法,就能將日誌資料寫到相應媒介中。若寫日誌出現異常,將則該訊息以Json格式備份到本地.log檔案中,並在以後自動將備份寫到相應媒介中。

 八、多平臺的設計和實現

  日誌元件作為基礎的元件,供不特定的系統使用,所以需要支援.net4.5/.net4.5.1/.net4.5.2/.net4.6/.net4.6.1/.net4.6.2.net4.7/.net4.7.1/.net4.7.2等平臺,支援 .netCore2.0/.netCore2.1平臺,其他的平臺由於版本較舊,功能效能不太完善,使用較少,故不予支援。

  實現多平臺建議使用VS2017,將專案配置 .csporj 中的程式碼<TargetFramework>net45</TargetFramework> 改為 <TargetFrameworks>net45;net451;net452;net46;net461;net462;net47;net471;net472;netcoreapp2.0;netcoreapp2.1</TargetFrameworks> ,即可將單目標框架變為多目標框架。然後在專案配置中的ItemGroup 節點中新增 Condition條件,來指明這些引用所適用的框架平臺,具體情況請參見專案配置.csporj 中檔案。最後在專案程式碼中使用 #if #else #endif條件編譯指明各個平臺下適用的編碼。
  本元件中主要涉及生命週期事件的多平臺實現、快取的多平臺實現、線上人數的多平臺實現等。
  對生命週期事件,.net平臺中有 Application Started、Application Stop、Application Error、Session_Start、Session_End、Application_BeginRequest等事件,而在.netCore平臺中僅有Application Started、Application Stop事件,其他事件需要通過Middleware中介軟體來實現。
  對快取,本系統使用http快取和CacheManager快取。http快取中,分別使用HttpRuntime.Cache快取和Microsoft.Extensions.Caching.Memory快取;對CacheManager快取,.net平臺中支援記憶體快取、Memcached快取、redis快取三種,.netCore平臺中僅支援記憶體快取、redis快取兩種。
  對線上人數,.net平臺中可以通過Application/快取結合Session_Start、Session_End事件來實現,但在.netCore平臺中,該實現較為麻煩,需要開啟Session、自定義HttpContext中介軟體等,利用SessionId列表來標記歷史訪客,利用Session過期時間來移除過期的SessionId來標記某人的離