日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 综合教程 >内容正文

综合教程

.NET Core Session源码探究

發(fā)布時間:2023/11/22 综合教程 33 生活家
生活随笔 收集整理的這篇文章主要介紹了 .NET Core Session源码探究 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

  前言

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

  Session 如何使用

  .NET Core 的 Session 使用方式和傳統(tǒng)的使用方式有很大的差別,首先它依賴存儲系統(tǒng) IDistributedCache 來存儲數(shù)據(jù),其次它依賴 SessionMiddleware 為每一次請求提供具體的實例。所以使用 Session 之前需要配置一些操作,詳細(xì)介紹可參閱微軟官方文檔會話狀態(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 注入代碼分析

  注冊的地方設(shè)計到了兩個擴(kuò)展方法 AddDistributedMemoryCache 和 AddSession. 其中 AddDistributedMemoryCache 這是借助 IDistributedCache 為 Session 數(shù)據(jù)提供存儲,AddSession 是 Session 實現(xiàn)的核心的注冊操作。

  IDistributedCache 提供存儲

  上面的示例中示例中使用的是基于本地內(nèi)存存儲的方式,也可以使用 IDistributedCache 針對 Redis 和數(shù)據(jù)庫存儲的擴(kuò)展方法。實現(xiàn)也非常簡單就是給 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;
}

  關(guān)于 IDistributedCache 的其他使用方式請參閱官方文檔的分布式緩存篇,關(guān)于分布式緩存源碼實現(xiàn)可以通過 Cache 的 Github 地址自行查閱。

  AddSession 核心操作

  AddSession 是 Session 實現(xiàn)的核心的注冊操作,具體實現(xiàn)代碼來自擴(kuò)展類 SessionServiceCollectionExtensions,AddSession 擴(kuò)展方法大致實現(xiàn)如下:

public static IServiceCollection AddSession (this IServiceCollection services)
{
    if (services == null)
    {
        throw new ArgumentNullException (nameof (services));
    }
    services.TryAddTransient<ISessionStore, DistributedSessionStore>();
    services.AddDataProtection ();
    return services;
}

  這個方法就做了兩件事,一個是注冊了 Session 的具體操作,另一個是添加了數(shù)據(jù)保護(hù)保護(hù)條例支持。和 Session 真正相關(guān)的其實只有 ISessionStore,話不多說,繼續(xù)向下看 DistributedSessionStore 實現(xiàn)

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);
    }
}

  這里的實現(xiàn)也非常簡單就是創(chuàng)建 Session 實例 DistributedSession,在這里我們就可以看出創(chuàng)建 Session 是依賴 IDistributedCache 的,這里的 sessionKey 其實是 SessionID,當(dāng)前會話唯一標(biāo)識。繼續(xù)向下找到 DistributedSession 實現(xiàn),這里的代碼比較多,因為這是封裝 Session 操作的實現(xiàn)類。老規(guī)矩先找到我們最容易下手的 Get 方法:

public bool TryGetValue (string key, out byte[] value)
{
    Load ();
    return _store.TryGetValue (new EncodedKey (key), out value);
}

  我們看到調(diào)用 TryGetValue 之前先調(diào)用了 Load 方法,這是內(nèi)部的私有方法

private void Load ()
{
    //判斷當(dāng)前會話中有沒有加載過數(shù)據(jù)
    if (!_loaded)
    {
        try
        {
            //根據(jù)會話唯一標(biāo)識在 IDistributedCache 中獲取數(shù)據(jù)
            var data = _cache.Get (_sessionKey);
            if (data != null)
            {
                //由于存儲的是按照特定的規(guī)則得到的二進(jìn)制數(shù)據(jù),所以獲取的時候要將數(shù)據(jù)反序列化
                Deserialize (new MemoryStream (data));
            }
            else if (!_isNewSessionKey)
            {
                _logger.AccessingExpiredSession (_sessionKey);
            }
            //是否可用標(biāo)識
            _isAvailable = true;
        }
        catch (Exception exception)
        {
            _logger.SessionCacheReadException (_sessionKey, exception);
            _isAvailable = false;
            _sessionId = string.Empty;
            _sessionIdBytes = null;
            _store = new NoOpSessionStore ();
        }
        finally
        {
           //將數(shù)據(jù)標(biāo)識設(shè)置為已加載狀態(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);
        //在存儲的數(shù)據(jù)中按照規(guī)則獲取存儲設(shè)置的具體 key
        var key = new EncodedKey (ReadBytes (content, keyLength));
        int dataLength = DeserializeNumFrom4Bytes (content);
        //將反序列化之后的數(shù)據(jù)存儲到_store
        _store[key] = ReadBytes (content, dataLength);
    }

    if (_logger.IsEnabled (LogLevel.Debug))
    {
        _sessionId = new Guid (_sessionIdBytes) .ToString ();
        _logger.SessionLoaded (_sessionKey, _sessionId, expectedEntries);
    }
}

  通過上面的代碼我們可以得知 Get 數(shù)據(jù)之前之前先 Load 數(shù)據(jù),Load 其實就是在 IDistributedCache 中獲取數(shù)據(jù)然后存儲到了_store 中,通過當(dāng)前類源碼可知_store 是本地字典,也就是說 Session 直接獲取的其實是本地字典里的數(shù)據(jù)。

private IDictionary<EncodedKey, byte[]> _store;

  這里其實產(chǎn)生兩點疑問:

  • 1. 針對每個會話存儲到 IDistributedCache 的其實都在一個 Key 里,就是以當(dāng)前會話唯一標(biāo)識為 key 的 value 里,為什么沒有采取組合會話 key 單獨存儲。
  • 2. 每次請求第一次操作 Session,都會把 IDistributedCache 里針對當(dāng)前會話的數(shù)據(jù)全部加載到本地字典里,一般來說每次會話操作 Session 的次數(shù)并不會很多,感覺并不會節(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);
        }
        //是否修改過標(biāo)識
        _isModified = true;
        //將原始內(nèi)容轉(zhuǎn)換為 byte 數(shù)組
        byte[] copy = new byte[value.Length];
        Buffer.BlockCopy (src: value, srcOffset: 0, dst: copy, dstOffset: 0, count: value.Length);
        //將數(shù)據(jù)存儲到本地字典_store
        _store[encodedKey] = copy;
    }
}

  這里我們可以看到 Set 方法并沒有將數(shù)據(jù)放入到存儲系統(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 里的數(shù)據(jù)進(jìn)行操作,都只是操作從存儲系統(tǒng) Load 到本地的字典數(shù)據(jù)。那什么地方進(jìn)行的存儲呢,也就是說我們要找到調(diào)用_cache.Set 方法的地方,最后在這個地方找到了 Set 方法,而且看這個方法名就知道是提交 Session 數(shù)據(jù)的地方

public async Task CommitAsync (CancellationToken cancellationToken = default)
{
    //超過_ioTimeout CancellationToken 將自動取消
    using (var timeout = new CancellationTokenSource (_ioTimeout))
    {
        var cts = CancellationTokenSource.CreateLinkedTokenSource (timeout.Token, cancellationToken);
        //數(shù)據(jù)被修改過
        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 字典里的數(shù)據(jù)寫到 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 字典里的數(shù)據(jù)寫到 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);
    }
}

  那么問題來了當(dāng)前類里并沒有地方調(diào)用 CommitAsync,那么到底是在什么地方調(diào)用的該方法呢?姑且別著急,我們之前說過使用 Session 的三要素,現(xiàn)在才說了兩個,還有一個 UseSession 的中間件沒有提及到呢。

  UseSession 中間件

  通過上面注冊的相關(guān)方法我們大概了解到了 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)
      {
          //將會話唯一標(biāo)識通過 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 數(shù)據(jù)提供具體實例
      context.Features.Set<ISessionFeature>(feature);
      try
      {
          await _next (context);
      }
      finally
      {
          //置空為了在請求結(jié)束后可以回收掉 Session
          context.Features.Set<ISessionFeature>(null);
          if (feature.Session != null)
          {
              try
              {
                  //請求完成后提交保存 Session 字典里的數(shù)據(jù)到 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 ()
      {
          //會話標(biāo)識寫入到 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 里的數(shù)據(jù)保存到 DistributedCache 都是在這里進(jìn)行的。不過這里仍存在一個疑問由于調(diào)用 CommitAsync 是在中間件執(zhí)行完成后統(tǒng)一進(jìn)行存儲的,也就是說中途對 Session 進(jìn)行的 Set Remove Clear 的操作都是在 Session 方法的本地字典里進(jìn)行的,并沒有同步到 DistributedCache 里,如果中途出現(xiàn)程序異常結(jié)束的情況下,保存到 Session 里的數(shù)據(jù),并沒有真正的存儲下來,會出現(xiàn)丟失的情況,不知道在設(shè)計這部分邏輯的時候是出于什么樣的考慮。

  總結(jié)

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

  • 針對每個會話存儲到 IDistributedCache 的其實都在一個 Key 里,就是以當(dāng)前會話唯一標(biāo)識為 key 的 value 里,為什么沒有采取組合會話 key 單獨存儲。
  • 每次請求第一次操作 Session,都會把 IDistributedCache 里針對當(dāng)前會話的數(shù)據(jù)全部加載到本地字典里,一般來說每次會話操作 Session 的次數(shù)并不會很多,感覺并不會節(jié)約性能。
  • 調(diào)用 CommitAsync 是在中間件執(zhí)行完成后統(tǒng)一進(jìn)行存儲的,也就是說中途對 Session 進(jìn)行的 Set Remove Clear 的操作都是在 Session 方法的本地字典里進(jìn)行的,并沒有同步到 DistributedCache 里,如果中途出現(xiàn)程序異常結(jié)束的情況下,保存到 Session 里的數(shù)據(jù),并沒有真正的存儲下來,會出現(xiàn)丟失的情況。

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

總結(jié)

以上是生活随笔為你收集整理的.NET Core Session源码探究的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。