跳到主要內容

.NET Core Session源碼探究

前言


    隨着互聯網的興起,技術的整體架構設計思路有了質的提升,曾經Web開發必不可少的內置對象Session已經被慢慢的遺棄。主要原因有兩點,一是Session依賴Cookie存放SessionID,即使不通過Cookie傳遞,也要依賴在請求參數或路徑上攜帶Session標識,對於目前前後端分離項目來說操作起來限制很大,比如跨域問題。二是Session數據跨服務器同步問題,現在基本上項目都使用負載均衡技術,Session同步存在一定的弊端,雖然可以藉助Redis或者其他存儲系統實現中心化存儲,但是略顯雞肋。雖然存在一定的弊端,但是在.NET Core也並沒有拋棄它,而且結合了更好的實現方式提升了設計思路。接下來我們通過分析源碼的方式,大致了解下新的工作方式。


Session如何使用


    .NET Core的Session使用方式和傳統的使用方式有很大的差別,首先它依賴存儲系統IDistributedCache來存儲數據,其次它依賴SessionMiddleware為每一次請求提供具體的實例。所以使用Session之前需要配置一些操作,詳細介紹可參閱微軟官方文檔會話狀態。大致配置流程,如下


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,話不多說,繼續向下看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);
}
}

這裏的實現也非常簡單就是創建Session實例DistributedSession,在這裏我們就可以看出創建Session是依賴IDistributedCache的,這裏的sessionKey其實是SessionID,當前會話唯一標識。繼續向下找到DistributedSession實現,這裏的代碼比較多,因為這是封裝Session操作的實現類。老規矩先找到我們最容易下手的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)
{
//由於存儲的是按照特定的規則得到的二進制數據,所以獲取的時候要將數據反序列化
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
{
//將數據標識設置為已加載狀態
_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);
//在存儲的數據中按照規則獲取存儲設置的具體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的次數並不會很多,感覺並不會節約性能。

接下來我們在再來查看另一個我們比較熟悉的方法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方法並沒有將數據放入到存儲系統,只是放入了本地字典里。我們再來看其他方法


public void Remove(string key)
{
Load();
_isModified |= _store.Remove(new EncodedKey(key));
}

public void Clear()
{
Load();
_isModified |= _store.Count > 0;
_store.Clear();
}

這些方法都沒有對存儲系統DistributedCache里的數據進行操作,都只是操作從存儲系統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();
//創建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的創建,以及Session里的數據保存到DistributedCache都是在這裏進行的。不過這裏仍存在一個疑問由於調用CommitAsync是在中間件執行完成后統一進行存儲的,也就是說中途對Session進行的Set Remove Clear的操作都是在Session方法的本地字典里進行的,並沒有同步到DistributedCache里,如果中途出現程序異常結束的情況下,保存到Session里的數據,並沒有真正的存儲下來,會出現丟失的情況,不知道在設計這部分邏輯的時候是出於什麼樣的考慮。


總結


    通過閱讀Session相關的部分源碼大致了解了Session的原理,工作三要素,IDistributedCache存儲Session里的數據,SessionStore是Session的實現類,UseSession是Session被創建到當前請求的地方。同時也留下了幾點疑問



  • 針對每個會話存儲到IDistributedCache的其實都在一個Key里,就是以當前會話唯一標識為key的value里,為什麼沒有採取組合會話key單獨存儲。

  • 每次請求第一次操作Session,都會把IDistributedCache里針對當前會話的數據全部加載到本地字典里,一般來說每次會話操作Session的次數並不會很多,感覺並不會節約性能。

  • 調用CommitAsync是在中間件執行完成后統一進行存儲的,也就是說中途對Session進行的Set Remove Clear的操作都是在Session方法的本地字典里進行的,並沒有同步到DistributedCache里,如果中途出現程序異常結束的情況下,保存到Session里的數據,並沒有真正的存儲下來,會出現丟失的情況。

對於以上疑問,不知道是個人理解不足,還是在設計的時候出於別的考慮。歡迎在評論區多多溝通交流,希望能從大家那裡得到更好的解釋和答案。


歡迎掃碼關注 本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】



網頁設計最專業,超強功能平台可客製化



※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面



※回頭車貨運收費標準



※推薦評價好的iphone維修中心



※教你寫出一流的銷售文案?



台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!



台中搬家公司費用怎麼算?



Orignal From: .NET Core Session源碼探究

留言

這個網誌中的熱門文章

強強聯手!攜手打造雲林縣Web3.0 領地方品牌進軍元宇宙

中華電信攜手國內最大Potato Media Web3.0社群共享平台,及品牌醫生果俐文創三方結合,透過經濟部「CBMP智慧雲遊跨域串連計畫」,協助品牌數位轉型,帶動品牌體驗情境、新商業模組升級與行動支付應用,第一站選在雲林縣當地原生消費品牌,打造社群消費循環,探索近期最夯的元宇宙新玩法。 Potato Media執行長顏宏霖表示,在CBMP計畫協助下,加上站內Web3.0資源,將快速協助雲林縣當地品牌升級轉型,幫助店家創造品牌價值,包含黑矸仔醬油、四代目麥芽酥、玉津烘焙坊、禪屋米胖工坊、YuDS沐耳飲、莫蒂精品巧克力、維野納複合式餐飲、頂雲咖啡、媽祖埔豆腐張、玉山碾米廠等,另外嘉義市麥麵、名香茗茶也等不及跨縣市加入,結合Potato Media站內活動,打造雲林專屬限定NFT道具與頭框,發行雲林扭蛋,體驗全新元宇宙新世界。 左起雲林縣計畫處處長李明岳、果俐文創執行長-陳郁涵、雲林縣議員周秀月、中華電信雲林營運處總經理張肇家、Potato Media執行長顏宏霖、LINE禮物代表睿鼎數位、12CMTaiwan行銷總監楊涵柔。(圖/由Potato Media提供) 此次Potato Media平台合作內容是店家會員註冊活動:首次註冊可得100積分 + 一顆扭蛋,店家推薦文章介紹,可領取店家消費優惠券,站內扭蛋禮物抽獎活動,獎項豐富:雲林限定禮品、雲林意象限定NFT道具與頭框,雲林幣扭一下APP政令大聲公獎勵活動,另外各種雲林美食伴手禮、住宿旅遊的店家,都可以使用行動支付或上LINE禮物平台實際體驗消費。 推薦評價好的 iphone維修 中心 擁有專業的維修技術團隊,同時聘請資深iphone手機維修專家,現場說明手機問題,快速修理,沒修好不收錢 產品缺大量曝光嗎?你需要的是一流 包裝設計 窩窩觸角包含自媒體、自有平台及其他國家營銷業務等,多角化經營並具有國際觀的永續理念。 雲林縣副縣長謝淑亞強調,此次計畫和Potato Media 合作,Potato Media 是一個在 2021 年 4 月正式發布的「區塊鏈 Web3.0 共享社群平台」,創作者和使用者都可以透過互動的機制,例如對文章點讚、留言與轉發分享,來獲取相對應比例的加密貨幣CFO(Potato Media 平台上的原生加密貨幣),跟Facebook、Instagram、YouTu...

要上網站行銷農產品,得經過市政府抽驗農藥殘留,檢驗合格才能上網行銷

台北網站設計      網頁設計公司     網站設計公司 食安五環為「源頭控管」、「重建生產管理履歷」、「提高查驗能力」、「加重生產者、廠商的責任」及「鼓勵、創造監督平臺」五大要環,除推動政府與市民消費者共同監督食品安全,更要從農場到餐桌,鏈結農村產業線,以網路社群為媒介,建立嘉義市農村網頁平台。活動現場介紹「農抵嘉」產銷網頁平台的內容,包含在地人文地產景,將景觀、產業、生態及文化等資訊整合、展現,更推廣在地農村農友優質、安全的農產品。要上「農抵嘉」網站行銷農產品,得經過市政府抽驗農藥殘留,檢驗合格才能上網行銷自己的農產品,嘉義市農友可以多利用,也提供店家採購管道。 「尚安心,農抵嘉」,透過「農抵嘉」產銷網頁平台,鏈結農村產業線並促進地產地消,從源頭管理食安,讓政府管理、民間參與,透過食安小教育,讓小朋友接觸農作物,與生產者互動,產生共鳴,一同攜手維護嘉義市的食安。記者會中也展示紅瓦厝社區所做的紅瓦窯烤,並有其他下埤花生等農產品,透過農場職人使「農特產品不只是農產品」,民眾可登入「農抵嘉」網頁http://chiayirural.com/index.asp瞭解更進一步的訊息。 網動廣告 參考資料:蕃新聞https://n.yam.com/Article/20171226529712 Orignal From: 要上網站行銷農產品,得經過市政府抽驗農藥殘留,檢驗合格才能上網行銷

IEA:疫情衝擊能源需求 但再生能源呈創紀錄成長_台中搬家公司

※ 台中搬家公司 教你幾個打包小技巧,輕鬆整理裝箱! 還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」 摘錄自2020年11月10日中央社報導 國際能源總署(IEA)報告今天(10日)指出,武漢肺炎(COVID-19)疫情可能衝擊能源需求,但電力部門再生能源繼續以創紀錄速度成長。 IEA執行董事比羅爾(Fatih Birol)表示:「在2025年,再生能源將成為全球最大發電來源,預計將提供1/3的全球電力,終結煤炭50年來作為最大電力供應來源的地位。」 ※推薦 台中搬家公司 優質服務,可到府估價 台中搬鋼琴,台中金庫搬運,中部廢棄物處理,南投縣搬家公司,好幫手搬家,西屯區搬家 IEA關於再生能源的年度報告估計,儘管受到疫情干擾,今年的再生能源新發電容量可望創紀錄,達將近200GW(GW為十億瓦)。 能源轉型 國際新聞 再生能源 疫情看氣候與能源 本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理 ※ 台中搬家公司 教你幾個打包小技巧,輕鬆整理裝箱! 還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」 Orignal From: IEA:疫情衝擊能源需求 但再生能源呈創紀錄成長_台中搬家公司