.NET Core Session源码探究
前言
隨著互聯網的興起,技術的整體架構設計思路有了質的提升,曾經 Web 開發(fā)必不可少的內置對象 Session 已經被慢慢的遺棄。主要原因有兩點,一是 Session 依賴 Cookie 存放 SessionID,即使不通過 Cookie 傳遞,也要依賴在請求參數或路徑上攜帶 Session 標識,對于目前前后端分離項目來說操作起來限制很大,比如跨域問題。二是 Session 數據跨服務器同步問題,現在基本上項目都使用負載均衡技術,Session 同步存在一定的弊端,雖然可以借助 Redis 或者其他存儲系統(tǒng)實現中心化存儲,但是略顯雞肋。雖然存在一定的弊端,但是在 .NET Core 也并沒有拋棄它,而且結合了更好的實現方式提升了設計思路。接下來我們通過分析源碼的方式,大致了解下新的工作方式。
Session 如何使用
.NET Core 的 Session 使用方式和傳統(tǒng)的使用方式有很大的差別,首先它依賴存儲系統(tǒng) IDistributedCache 來存儲數據,其次它依賴 SessionMiddleware 為每一次請求提供具體的實例。所以使用 Session 之前需要配置一些操作,詳細介紹可參閱微軟官方文檔會話狀態(tài)。大致配置流程,如下:
public class Startup
{
public Startup (IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices (IServiceCollection services)
{
services.AddDistributedMemoryCache ();
services.AddSession (options =>
{
options.IdleTimeout = TimeSpan.FromSeconds (10);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
}
public void Configure (IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseSession ();
}
}
Session 注入代碼分析
注冊的地方設計到了兩個擴展方法 AddDistributedMemoryCache 和 AddSession. 其中 AddDistributedMemoryCache 這是借助 IDistributedCache 為 Session 數據提供存儲,AddSession 是 Session 實現的核心的注冊操作。
IDistributedCache 提供存儲
上面的示例中示例中使用的是基于本地內存存儲的方式,也可以使用 IDistributedCache 針對 Redis 和數據庫存儲的擴展方法。實現也非常簡單就是給 IDistributedCache 注冊存儲操作實例:
public static IServiceCollection AddDistributedMemoryCache (this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException (nameof (services));
}
services.AddOptions ();
services.TryAdd (ServiceDescriptor.Singleton<IDistributedCache, MemoryDistributedCache>());
return services;
}
關于 IDistributedCache 的其他使用方式請參閱官方文檔的分布式緩存篇,關于分布式緩存源碼實現可以通過 Cache 的 Github 地址自行查閱。
AddSession 核心操作
AddSession 是 Session 實現的核心的注冊操作,具體實現代碼來自擴展類 SessionServiceCollectionExtensions,AddSession 擴展方法大致實現如下:
public static IServiceCollection AddSession (this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException (nameof (services));
}
services.TryAddTransient<ISessionStore, DistributedSessionStore>();
services.AddDataProtection ();
return services;
}
這個方法就做了兩件事,一個是注冊了 Session 的具體操作,另一個是添加了數據保護保護條例支持。和 Session 真正相關的其實只有 ISessionStore,話不多說,繼續(xù)向下看 DistributedSessionStore 實現
public class DistributedSessionStore : ISessionStore
{
private readonly IDistributedCache _cache;
private readonly ILoggerFactory _loggerFactory;
public DistributedSessionStore (IDistributedCache cache, ILoggerFactory loggerFactory)
{
if (cache == null)
{
throw new ArgumentNullException (nameof (cache));
}
if (loggerFactory == null)
{
throw new ArgumentNullException (nameof (loggerFactory));
}
_cache = cache;
_loggerFactory = loggerFactory;
}
public ISession Create (string sessionKey, TimeSpan idleTimeout, TimeSpan ioTimeout, Func<bool> tryEstablishSession, bool isNewSessionKey)
{
if (string.IsNullOrEmpty (sessionKey))
{
throw new ArgumentException (Resources.ArgumentCannotBeNullOrEmpty, nameof (sessionKey));
}
if (tryEstablishSession == null)
{
throw new ArgumentNullException (nameof (tryEstablishSession));
}
return new DistributedSession (_cache, sessionKey, idleTimeout, ioTimeout, tryEstablishSession, _loggerFactory, isNewSessionKey);
}
}
這里的實現也非常簡單就是創(chuàng)建 Session 實例 DistributedSession,在這里我們就可以看出創(chuàng)建 Session 是依賴 IDistributedCache 的,這里的 sessionKey 其實是 SessionID,當前會話唯一標識。繼續(xù)向下找到 DistributedSession 實現,這里的代碼比較多,因為這是封裝 Session 操作的實現類。老規(guī)矩先找到我們最容易下手的 Get 方法:
public bool TryGetValue (string key, out byte[] value)
{
Load ();
return _store.TryGetValue (new EncodedKey (key), out value);
}
我們看到調用 TryGetValue 之前先調用了 Load 方法,這是內部的私有方法
private void Load ()
{
//判斷當前會話中有沒有加載過數據
if (!_loaded)
{
try
{
//根據會話唯一標識在 IDistributedCache 中獲取數據
var data = _cache.Get (_sessionKey);
if (data != null)
{
//由于存儲的是按照特定的規(guī)則得到的二進制數據,所以獲取的時候要將數據反序列化
Deserialize (new MemoryStream (data));
}
else if (!_isNewSessionKey)
{
_logger.AccessingExpiredSession (_sessionKey);
}
//是否可用標識
_isAvailable = true;
}
catch (Exception exception)
{
_logger.SessionCacheReadException (_sessionKey, exception);
_isAvailable = false;
_sessionId = string.Empty;
_sessionIdBytes = null;
_store = new NoOpSessionStore ();
}
finally
{
//將數據標識設置為已加載狀態(tài)
_loaded = true;
}
}
}
private void Deserialize (Stream content)
{
if (content == null || content.ReadByte () != SerializationRevision)
{
// Replace the un-readable format.
_isModified = true;
return;
}
int expectedEntries = DeserializeNumFrom3Bytes (content);
_sessionIdBytes = ReadBytes (content, IdByteCount);
for (int i = 0; i < expectedEntries; i++)
{
int keyLength = DeserializeNumFrom2Bytes (content);
//在存儲的數據中按照規(guī)則獲取存儲設置的具體 key
var key = new EncodedKey (ReadBytes (content, keyLength));
int dataLength = DeserializeNumFrom4Bytes (content);
//將反序列化之后的數據存儲到_store
_store[key] = ReadBytes (content, dataLength);
}
if (_logger.IsEnabled (LogLevel.Debug))
{
_sessionId = new Guid (_sessionIdBytes) .ToString ();
_logger.SessionLoaded (_sessionKey, _sessionId, expectedEntries);
}
}
通過上面的代碼我們可以得知 Get 數據之前之前先 Load 數據,Load 其實就是在 IDistributedCache 中獲取數據然后存儲到了_store 中,通過當前類源碼可知_store 是本地字典,也就是說 Session 直接獲取的其實是本地字典里的數據。
private IDictionary<EncodedKey, byte[]> _store;
這里其實產生兩點疑問:
- 1. 針對每個會話存儲到 IDistributedCache 的其實都在一個 Key 里,就是以當前會話唯一標識為 key 的 value 里,為什么沒有采取組合會話 key 單獨存儲。
- 2. 每次請求第一次操作 Session,都會把 IDistributedCache 里針對當前會話的數據全部加載到本地字典里,一般來說每次會話操作 Session 的次數并不會很多,感覺并不會節(jié)約性能。
接下來我們在再來查看另一個我們比較熟悉的方法 Set 方法
public void Set (string key, byte[] value)
{
if (value == null)
{
throw new ArgumentNullException (nameof (value));
}
if (IsAvailable)
{
//存儲的 key 是被編碼過的
var encodedKey = new EncodedKey (key);
if (encodedKey.KeyBytes.Length > KeyLengthLimit)
{
throw new ArgumentOutOfRangeException (nameof (key),
Resources.FormatException_KeyLengthIsExceeded (KeyLengthLimit));
}
if (!_tryEstablishSession ())
{
throw new InvalidOperationException (Resources.Exception_InvalidSessionEstablishment);
}
//是否修改過標識
_isModified = true;
//將原始內容轉換為 byte 數組
byte[] copy = new byte[value.Length];
Buffer.BlockCopy (src: value, srcOffset: 0, dst: copy, dstOffset: 0, count: value.Length);
//將數據存儲到本地字典_store
_store[encodedKey] = copy;
}
}
這里我們可以看到 Set 方法并沒有將數據放入到存儲系統(tǒng),只是放入了本地字典里。我們再來看其他方法
public void Remove (string key)
{
Load ();
_isModified |= _store.Remove (new EncodedKey (key));
}
public void Clear ()
{
Load ();
_isModified |= _store.Count > 0;
_store.Clear ();
}
這些方法都沒有對存儲系統(tǒng) DistributedCache 里的數據進行操作,都只是操作從存儲系統(tǒng) Load 到本地的字典數據。那什么地方進行的存儲呢,也就是說我們要找到調用_cache.Set 方法的地方,最后在這個地方找到了 Set 方法,而且看這個方法名就知道是提交 Session 數據的地方
public async Task CommitAsync (CancellationToken cancellationToken = default)
{
//超過_ioTimeout CancellationToken 將自動取消
using (var timeout = new CancellationTokenSource (_ioTimeout))
{
var cts = CancellationTokenSource.CreateLinkedTokenSource (timeout.Token, cancellationToken);
//數據被修改過
if (_isModified)
{
if (_logger.IsEnabled (LogLevel.Information))
{
try
{
cts.Token.ThrowIfCancellationRequested ();
var data = await _cache.GetAsync (_sessionKey, cts.Token);
if (data == null)
{
_logger.SessionStarted (_sessionKey, Id);
}
}
catch (OperationCanceledException)
{
}
catch (Exception exception)
{
_logger.SessionCacheReadException (_sessionKey, exception);
}
}
var stream = new MemoryStream ();
//將_store 字典里的數據寫到 stream 里
Serialize (stream);
try
{
cts.Token.ThrowIfCancellationRequested ();
//將讀取_store 的流寫入到 DistributedCache 存儲里
await _cache.SetAsync (
_sessionKey,
stream.ToArray (),
new DistributedCacheEntryOptions () .SetSlidingExpiration (_idleTimeout),
cts.Token);
_isModified = false;
_logger.SessionStored (_sessionKey, Id, _store.Count);
}
catch (OperationCanceledException oex)
{
if (timeout.Token.IsCancellationRequested)
{
_logger.SessionCommitTimeout ();
throw new OperationCanceledException ("Timed out committing the session.", oex, timeout.Token);
}
throw;
}
}
else
{
try
{
await _cache.RefreshAsync (_sessionKey, cts.Token);
}
catch (OperationCanceledException oex)
{
if (timeout.Token.IsCancellationRequested)
{
_logger.SessionRefreshTimeout ();
throw new OperationCanceledException ("Timed out refreshing the session.", oex, timeout.Token);
}
throw;
}
}
}
}
private void Serialize (Stream output)
{
output.WriteByte (SerializationRevision);
SerializeNumAs3Bytes (output, _store.Count);
output.Write (IdBytes, 0, IdByteCount);
//將_store 字典里的數據寫到 Stream 里
foreach (var entry in _store)
{
var keyBytes = entry.Key.KeyBytes;
SerializeNumAs2Bytes (output, keyBytes.Length);
output.Write (keyBytes, 0, keyBytes.Length);
SerializeNumAs4Bytes (output, entry.Value.Length);
output.Write (entry.Value, 0, entry.Value.Length);
}
}
那么問題來了當前類里并沒有地方調用 CommitAsync,那么到底是在什么地方調用的該方法呢?姑且別著急,我們之前說過使用 Session 的三要素,現在才說了兩個,還有一個 UseSession 的中間件沒有提及到呢。
UseSession 中間件
通過上面注冊的相關方法我們大概了解到了 Session 的工作原理。接下來我們查看 UseSession 中間件里的代碼,探究這里究竟做了什么操作。我們找到 UseSession 方法所在的地方 SessionMiddlewareExtensions 找到第一個方法
public static IApplicationBuilder UseSession (this IApplicationBuilder app)
{
if (app == null)
{
throw new ArgumentNullException (nameof (app));
}
return app.UseMiddleware<SessionMiddleware>();
}
SessionMiddleware 的源碼
public class SessionMiddleware
{
private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create ();
private const int SessionKeyLength = 36; // "382c74c3-721d-4f34-80e5-57657b6cbc27"
private static readonly Func<bool> ReturnTrue = () => true;
private readonly RequestDelegate _next;
private readonly SessionOptions _options;
private readonly ILogger _logger;
private readonly ISessionStore _sessionStore;
private readonly IDataProtector _dataProtector;
public SessionMiddleware (
RequestDelegate next,
ILoggerFactory loggerFactory,
IDataProtectionProvider dataProtectionProvider,
ISessionStore sessionStore,
IOptions<SessionOptions> options)
{
if (next == null)
{
throw new ArgumentNullException (nameof (next));
}
if (loggerFactory == null)
{
throw new ArgumentNullException (nameof (loggerFactory));
}
if (dataProtectionProvider == null)
{
throw new ArgumentNullException (nameof (dataProtectionProvider));
}
if (sessionStore == null)
{
throw new ArgumentNullException (nameof (sessionStore));
}
if (options == null)
{
throw new ArgumentNullException (nameof (options));
}
_next = next;
_logger = loggerFactory.CreateLogger<SessionMiddleware>();
_dataProtector = dataProtectionProvider.CreateProtector (nameof (SessionMiddleware));
_options = options.Value;
//Session 操作類在這里被注入的
_sessionStore = sessionStore;
}
public async Task Invoke (HttpContext context)
{
var isNewSessionKey = false;
Func<bool> tryEstablishSession = ReturnTrue;
var cookieValue = context.Request.Cookies[_options.Cookie.Name];
var sessionKey = CookieProtection.Unprotect (_dataProtector, cookieValue, _logger);
//會話首次建立
if (string.IsNullOrWhiteSpace (sessionKey) || sessionKey.Length != SessionKeyLength)
{
//將會話唯一標識通過 Cookie 返回到客戶端
var guidBytes = new byte[16];
CryptoRandom.GetBytes (guidBytes);
sessionKey = new Guid (guidBytes) .ToString ();
cookieValue = CookieProtection.Protect (_dataProtector, sessionKey);
var establisher = new SessionEstablisher (context, cookieValue, _options);
tryEstablishSession = establisher.TryEstablishSession;
isNewSessionKey = true;
}
var feature = new SessionFeature ();
//創(chuàng)建 Session
feature.Session = _sessionStore.Create (sessionKey, _options.IdleTimeout, _options.IOTimeout, tryEstablishSession, isNewSessionKey);
//放入到 ISessionFeature,給 HttpContext 中的 Session 數據提供具體實例
context.Features.Set<ISessionFeature>(feature);
try
{
await _next (context);
}
finally
{
//置空為了在請求結束后可以回收掉 Session
context.Features.Set<ISessionFeature>(null);
if (feature.Session != null)
{
try
{
//請求完成后提交保存 Session 字典里的數據到 DistributedCache 存儲里
await feature.Session.CommitAsync ();
}
catch (OperationCanceledException)
{
_logger.SessionCommitCanceled ();
}
catch (Exception ex)
{
_logger.ErrorClosingTheSession (ex);
}
}
}
}
private class SessionEstablisher
{
private readonly HttpContext _context;
private readonly string _cookieValue;
private readonly SessionOptions _options;
private bool _shouldEstablishSession;
public SessionEstablisher (HttpContext context, string cookieValue, SessionOptions options)
{
_context = context;
_cookieValue = cookieValue;
_options = options;
context.Response.OnStarting (OnStartingCallback, state: this);
}
private static Task OnStartingCallback (object state)
{
var establisher = (SessionEstablisher) state;
if (establisher._shouldEstablishSession)
{
establisher.SetCookie ();
}
return Task.FromResult (0);
}
private void SetCookie ()
{
//會話標識寫入到 Cookie 操作
var cookieOptions = _options.Cookie.Build (_context);
var response = _context.Response;
response.Cookies.Append (_options.Cookie.Name, _cookieValue, cookieOptions);
var responseHeaders = response.Headers;
responseHeaders[HeaderNames.CacheControl] = "no-cache";
responseHeaders[HeaderNames.Pragma] = "no-cache";
responseHeaders[HeaderNames.Expires] = "-1";
}
internal bool TryEstablishSession ()
{
return (_shouldEstablishSession |= !_context.Response.HasStarted);
}
}
}
通過 SessionMiddleware 中間件里的代碼我們了解到了每次請求 Session 的創(chuàng)建,以及 Session 里的數據保存到 DistributedCache 都是在這里進行的。不過這里仍存在一個疑問由于調用 CommitAsync 是在中間件執(zhí)行完成后統(tǒng)一進行存儲的,也就是說中途對 Session 進行的 Set Remove Clear 的操作都是在 Session 方法的本地字典里進行的,并沒有同步到 DistributedCache 里,如果中途出現程序異常結束的情況下,保存到 Session 里的數據,并沒有真正的存儲下來,會出現丟失的情況,不知道在設計這部分邏輯的時候是出于什么樣的考慮。
總結
通過閱讀 Session 相關的部分源碼大致了解了 Session 的原理,工作三要素,IDistributedCache 存儲 Session 里的數據,SessionStore 是 Session 的實現類,UseSession 是 Session 被創(chuàng)建到當前請求的地方。同時也留下了幾點疑問
- 針對每個會話存儲到 IDistributedCache 的其實都在一個 Key 里,就是以當前會話唯一標識為 key 的 value 里,為什么沒有采取組合會話 key 單獨存儲。
- 每次請求第一次操作 Session,都會把 IDistributedCache 里針對當前會話的數據全部加載到本地字典里,一般來說每次會話操作 Session 的次數并不會很多,感覺并不會節(jié)約性能。
- 調用 CommitAsync 是在中間件執(zhí)行完成后統(tǒng)一進行存儲的,也就是說中途對 Session 進行的 Set Remove Clear 的操作都是在 Session 方法的本地字典里進行的,并沒有同步到 DistributedCache 里,如果中途出現程序異常結束的情況下,保存到 Session 里的數據,并沒有真正的存儲下來,會出現丟失的情況。
對于以上疑問,不知道是個人理解不足,還是在設計的時候出于別的考慮。歡迎在評論區(qū)多多溝通交流,希望能從大家那里得到更好的解釋和答案。
總結
以上是生活随笔為你收集整理的.NET Core Session源码探究的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 补充医疗保险一年多少钱??
- 下一篇: 格力没赢,美的没输,空调“一哥”15年拉