Log2Net組件代碼詳解(附開源代碼)

上一篇,咱們介紹了Log2Net的需求和總體框架,咱們接下來介紹咱們是如何用代碼實現Log2Net組件的功能的。git

1、總體介紹

  Log2Net組件自己是一個Dll,供其餘系統調用。github

  本部分由如下幾部分組成:redis

  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版本並安裝其餘依賴。請儘可能不要手動下載類庫安裝,可能會出現各類各樣的不兼容、缺乏依賴庫的狀況。sql

  本組件使用VS2017開發,爲類庫項目,支持.net4.5~netCore2.1。若您把源碼下載下來,而您的電腦上某個某個.Net版本,請在csproj文件中的TargetFrameworks中移除該net版本。數據庫

  爲了測試該組件,分別添加了一個.NET4.5的MVC項目和.netCore2.0的MVC項目。項目文件圖以下圖所示:json

  本項目代碼已開源,地址爲 https://github.com/yuchen1030/Log2Net ,您能夠參照代碼理解下述的設計。瀏覽器

2、模型實體Models類庫

  模型實體包括定義在ModelsInDB.cs中數據庫中使用的模型、定義在ModelsUI.cs中外部接口使用的模型、定義在ModelsInCode.cs中本類庫代碼中使用的模型。緩存

  系統中的操做軌跡數據的數據庫實體爲Log_OperateTrace,監控數據數據庫實體爲Log_SystemMonitor。代碼中以這兩個實體爲核心定義了其餘數據實體,具體參見代碼。 服務器

3、工具方法Util定義

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

3.1 ComUtil類

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

文件

用途描述

AppConfig

配置文件讀寫類

AutoMapperHelper

對象映射幫助類

CacheHelper.cs

緩存操做類

DtModelConvert.cs

泛型ModelDataTable互轉操做類

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部分代碼。 

4、日誌信息獲取類

  本部分定義了日誌組件使用的基礎方法,如客戶端服務器信息ClientServerInfo類、在線人數訪客人數統計VisitOnlineCount 類、日誌組件公共類LogCom.cs

4.1 ClientServerInfo類

  該類庫用於收集客戶端和服務器端的信息,包括客戶端信息子類ClientInfo和服務器端信息子類ServerInfo。

  ClientInfo類用來獲取客戶端的ip地址、主機名、Mac地址、瀏覽器信息

  ServerInfo類用來獲取服務器端的ip地址、主機名、操做系統、CLR版本服務器運行時間、可用硬盤空間、CPU使用率、內存使用率等信息。

4.2 訪客人數統計VisitOnlineCount類

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

  對.net平臺,存在Session_StartSession_End事件訪客統計的實現思路較爲清晰,本組件提供了兩種方案:使用Application對象實現、使用緩存實現。具體採用哪一種方案由簡單工廠決定,默認採用緩存方案。

  對.NetCore平臺,不存在Session_StartSession_End事件事件,須要藉助於HttpContext中間件來實現。在HttpContext中,保存了全部的SessionID,Session過時,則視爲該SessionID離線。據此能夠統計出在線人數和歷史訪客。

4.3 公共類LogCom類 

  本類中定義一些本組件內部使用的公共類,主要是寫文件的類、日誌實體封裝類,實現很是簡單,類圖以下:

5、日誌追加器Appender類庫  

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

  日誌追加方式有寫到文件、ADO方式寫到數據庫、經過消息隊列寫到數據庫三種。相應的有FileAppender、DirectDBAppender、MQ2DBAppender三種追加器。三種追加器都實現了公共的追加器BaseAppender類。類圖以下:

                     

  公共追加器BaseAppender爲抽象類中,定義了兩個抽象的WriteLog方法,分別用來寫用戶操做日誌和系統運行日誌。

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

  FileAppender、DirectDBAppender、MQ2DBAppender這三種追加器實現本身的WriteLog方法。MQ2DBAppender是經過消息隊列寫到數據庫,除繼承自DirectDBAppender的方法外,還須要開啓消息隊列消費線程,將消息隊列中的日誌寫到數據庫中:開啓StartWriteTraceDataService線程啓動寫trace日誌數據服務,開啓StartWriteMonitorDataService線程啓動寫monitor日誌數據服務。兩個服務中,都是啓動隊列服務,檢查隊列,如有消息則寫數據到數據庫。只不過一個隊列用於寫軌跡日誌,一個隊列用於寫監控日誌。 

  在寫數據庫中時,一方面寫到SQL數據庫中,便於讀寫分離的實現,另外一方面寫到時序數據庫InfluxDB中,便於之後使用GrafanaELK工具進行更加靈活優雅的監控。

  用戶能夠經過配置來決定使用哪種追加器,代碼中經過追加器工廠類AppenderFac,獲得相應的追加器工廠實例,調用該追加器的方法進行日誌的記錄。

6、.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

 7、外部接口類LogApi類

  在本類中,調用其餘類的方法,造成供其餘業務系統調用的方法。包含如下內容:日誌組件註冊、寫日誌等。業務系統不須要關心這些方法的具體實現,只須要封裝業務實體,調用寫日誌的方法便可。LogApi類會調用其餘的類,如等實現日誌記錄的功能。類圖以下:

  • RegisterLogInitMsg:註冊日誌組件到本系統,爲日誌組件準備基礎信息:服務器IP、服務器主機名,系統名稱等;使用EF自動建立數據;並調用WriteServerStartupLog方法寫啓動日誌,調用WriteMonitorLogThread方法寫定時監控日誌。
  • GetLogWebApplicationsName:從配置文件中獲取用戶自定義的系統名稱。
  • 這裏還包含網站生命週期事件中的日誌記錄,以下:
  • WriteServerStartupLog:服務器啓動時,獲取操做系統,.NET CLR版本;
  • WriteFirstVisitLog:網站被初次訪問,記錄記錄IIS版本;
  • WriteServerStopLog:服務器中止時,獲取已運行時間;
  • WriteServerStartupLog:系統異常時,記錄異常日誌;
  • IncreaseOnlineVisitNumSession Start時,在線人數和訪客人數加1;
  • ReduceOnlineNumSession end時在線人數減1

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

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

  • WriteLog方法重載(2個):封裝日誌實體,調用日誌追加器的方法將日誌寫到媒介中,分別對業務操做和監控數據進行寫。
  • WriteMsgToDebugFile:寫調試日誌寫到文件中,可經過bWriteInfoToDebugFile配置是否開啓。
  • WriteInfoToFile:將將日記寫到本地文件中,記錄一些重要但又沒必要寫入log日誌媒介的信息。

  Log_OperateTraceBllEdm爲操做軌跡業務實體,LogMonitorEdm爲監控信息實體,各業務系統將信息封裝進這兩個實體,而後調用WriteLog方法,就能日誌數據寫到相應媒介中日誌出現異常,將則消息以Json格式備份到本地.log文件並在之後自動將備份寫到相應媒介中。

 8、多平臺的設計和實現

  日誌組件做爲基礎的組件,供不特定的系統使用,因此須要支持.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來標記某人的離線。

相關文章
相關標籤/搜索