.NET Core Session源码探究-阿里云开发者社区

开发者社区> 优惠券活动> 正文

.NET Core Session源码探究

简介:
+关注继续查看

.NET Core Session源码探究

前言#
随着互联网的兴起,技术的整体架构设计思路有了质的提升,曾经Web开发必不可少的内置对象Session已经被慢慢的遗弃。主要原因有两点,一是Session依赖Cookie存放SessionID,即使不通过Cookie传递,也要依赖在请求参数或路径上携带Session标识,对于目前前后端分离项目来说操作起来限制很大,比如跨域问题。二是Session数据跨服务器同步问题,现在基本上项目都使用负载均衡技术,Session同步存在一定的弊端,虽然可以借助Redis或者其他存储系统实现中心化存储,但是略显鸡肋。虽然存在一定的弊端,但是在.NET Core也并没有抛弃它,而且借助了更好的实现方式提升了它的设计思路。接下来我们通过分析源码的方式,大致了解下新的工作方式。

Session如何使用#
.NET Core的Session使用方式和传统的使用方式有很大的差别,首先它依赖存储系统IDistributedCache来存储数据,其次它依赖SessionMiddleware为每一次请求提供具体的实例。所以使用Session之前需要配置一些操作,相信介绍情参阅微软官方文档会话状态。简单来说大致配置如下

Copy
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注册存储操作实例

Copy
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扩展方法大致实现如下

Copy
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实现

Copy
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方法

Copy
public bool TryGetValue(string key, out byte[] value)
{

Load();
return _store.TryGetValue(new EncodedKey(key), out value);

}
我们看到调用TryGetValue之前先调用了Load方法,这是内部的私有方法

Copy
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直接获取的其实是本地字典里的数据。

Copy
private IDictionary _store;
这里其实产生两点疑问:
1.针对每个会话存储到IDistributedCache的其实都在一个Key里,就是以当前会话唯一标识为key的value里,为什么没有采取组合会话key单独存储。
2.每次请求第一次操作Session,都会把IDistributedCache里针对当前会话的数据全部加载到本地字典里,一般来说每次会话操作Session的次数并不会很多,感觉并不会节约性能。接下来我们在再来查看另一个我们比较熟悉的方法Set方法

Copy
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方法并没有将数据放入到存储系统,只是放入了本地字典里。我们再来看其他方法

Copy
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数据的地方

Copy
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找到第一个方法

Copy
public static IApplicationBuilder UseSession(this IApplicationBuilder app)
{

if (app == null)
{
    throw new ArgumentNullException(nameof(app));
}
return app.UseMiddleware<SessionMiddleware>();

}
SessionMiddleware的源码

Copy
public class SessionMiddleware
{
private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create();
private const int SessionKeyLength = 36; // "382c74c3-721d-4f34-80e5-57657b6cbc27"
private static readonly Func 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里的数据,并没有真正的存储下来,会出现丢失的情况。
对于以上疑问,不知道是个人理解不足,还是在设计的时候出于别的考虑。欢迎在评论区多多沟通交流,希望能从大家那里得到更好的解释和答案。

作者: yi念之间

出处:https://www.cnblogs.com/wucy/p/13044467.html

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
OPC Client 核心源码
好像技术一沾上工业,便有了很高的价值,大家三缄其口,谁都不点破这层窗户纸,好多的思路和源码都要从国外网站获得,国内总是有条件,有限制--就是不告诉你,怕教会徒弟,饿死师父吧。
716 0
Spring IOC 容器源码分析
原文出处:https://javadoop.com/post/spring-ioc Spring 最重要的概念是 IOC 和 AOP,本篇文章其实就是要带领大家来分析下 Spring 的 IOC 容器。
700 0
RSS简易阅读器vb.net源代码
上次发了一个自己写的RSS阅读器的源代码http://www.cnblogs.com/aowind/archive/2005/03/16/119838.html这次打包发上来了上面还有用到的那个WEB浏览控件的COM组件http://files.
597 0
将图像转成HTML文件,VB.net源代码
上次发过一个软件,见下文http://www.cnblogs.com/aowind/archive/2005/03/05/113429.html其软件的功能就是将一个图像转成HTML文件,就是用一些自定义的数字通过不同的色彩来表现出这个图像其效果如下:经过小弟研究了一下,在vb.
676 0
silverlight.net官方网站图片切换源码
下午心血来潮,想看看MS的开发人员是如何做silverlight的,出于学习目的把silverlight.net官方首页的图片轮换通过分析html源代码,下载xap后反编译了一下,分析后的源代码略作修改备份于此,希望对大家学习silverlight有用,请勿用于非法商业用途。
860 0
Optional源码分析(未完)
Optional源码分析(未完) Optional 类是Java8的新特性,Optional是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。
782 0
+关注
优惠券活动
阿里云优惠码阿里云推荐券bieryun.com
389
文章
9
问答
文章排行榜
最热
最新
相关电子书
更多
《2021云上架构与运维峰会演讲合集》
立即下载
《零基础CSS入门教程》
立即下载
《零基础HTML入门教程》
立即下载