.NET Core Session源碼探究

前言

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

Session如何使用

    .NET Core的Session使用方式和傳統的使用方式有很大的差異,首先它依賴存儲系統IDistributedCache來存儲數據,其次它依賴SessionMiddleware爲每一次請求提供具體的實例。因此使用Session以前須要配置一些操做,相信介紹情參閱微軟官方文檔會話狀態。簡單來講大體配置以下github

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方法,這是內部的私有方法cookie

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直接獲取的實際上是本地字典裏的數據。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裏的數據,並無真正的存儲下來,會出現丟失的狀況。
對於以上疑問,不知道是我的理解不足,仍是在設計的時候出於別的考慮。歡迎在評論區多多溝通交流,但願能從你們那裏獲得更好的解釋和答案。
相關文章
相關標籤/搜索