.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里的数据,并没有真正的存储下来,会出现丢失的情况。
对于以上疑问,不知道是个人理解不足,还是在设计的时候出于别的考虑。欢迎在评论区多多沟通交流,希望能从大家那里得到更好的解释和答案。
.NET Core Session源码探究的更多相关文章
- 浅谈.Net Core DependencyInjection源码探究
前言 相信使用过Asp.Net Core开发框架的人对自带的DI框架已经相当熟悉了,很多刚开始接触.Net Core的时候觉得不适应,主要就是因为Core默认集成它的原因.它是Asp.Net ...
- .NET Core HttpClient源码探究
前言 在之前的文章我们介绍过HttpClient相关的服务发现,确实HttpClient是目前.NET Core进行Http网络编程的的主要手段.在之前的介绍中也看到了,我们使用了一个很重要的 ...
- .Net Core Configuration源码探究
前言 上篇文章我们演示了为Configuration添加Etcd数据源,并且了解到为Configuration扩展自定义数据源还是非常简单的,核心就是把数据源的数据按照一定的规则读取到指定的字 ...
- Mybatis日志源码探究
一.项目搭建 1.pom.xml <dependencies> <dependency> <groupId>log4j</groupId> <ar ...
- spring-cloud-sleuth+zipkin源码探究
1. spring-cloud-sleuth+zipkin源码探究 1.1. 前言 粗略看了下spring cloud sleuth core源码,发现内容真的有点多,它支持了很多类型的链路追踪, ...
- spring-boot-2.0.3之quartz集成,数据源问题,源码探究
前言 开心一刻 着火了,他报警说:119吗,我家发生火灾了. 119问:在哪里? 他说:在我家. 119问:具体点. 他说:在我家的厨房里. 119问:我说你现在的位置. 他说:我趴在桌子底下. 11 ...
- Django session 源码流程
流程 Django session源码流程 首先执行的是SessionMiddleware的init方法 import_module(settings.SESSION_ENGINE) 导入了一个 dj ...
- Vue源码探究-全局API
Vue源码探究-全局API 本篇代码位于vue/src/core/global-api/ Vue暴露了一些全局API来强化功能开发,API的使用示例官网上都有说明,无需多言.这里主要来看一下全局API ...
- Vue源码探究-事件系统
Vue源码探究-事件系统 本篇代码位于vue/src/core/instance/events.js 紧跟着生命周期之后的就是继续初始化事件相关的属性和方法.整个事件系统的代码相对其他模块来说非常简短 ...
随机推荐
- Python 的十万个为什么?
随着 Python 在近些年的火爆,网上出现了很多这个方向的公众号和博客,文章也层出不穷. 受到此风气的影响,我也把自己"培养"成了一名技术博主,写作近两年来,陆陆续续写过不少的系 ...
- nexus 启用ldap认证
使用自己搭建的openldap 使用用户中心的openldap 修改完后,重启服务 # cd /opt/sonarqube-6.7.3/bin/linux-x86-64/ && ./s ...
- react+express实现跨域
1. 首先复习一下跨域的几种主要方式: a. jsonp b. cors c. 代理服务(开发环境下常用) 2. 代理服务器:可实现转发请求.即浏览器在3000端口发出请求,通过代理转发,将请求发送给 ...
- Python+Selenium+Chrome 的一个案例
第一步,下载chromeDrive:http://npm.taobao.org/mirrors/chromedriver(我下载的是2.43版本的chromedriver_win32.zip) 下载之 ...
- 阿里云wordpress轻量应用服务器升级php版本
目录 脚本升级 php.ini没有加载 升级完后只能最大只能上传2m的文件的问题 脚本升级 用大佬写的脚本: https://yq.aliyun.com/articles/717769?spm=a2c ...
- Linux系统rabbitmq安装
rabbitmq消息队列(Message Queue)是一种应用间的通信方式,消息发送后可以立即返回,由消息系统来确保消息的可靠传递.消息发布者只管把消息发布到 MQ 中而不用管谁来取,消息使用者只管 ...
- poj3635 优先队列+打标记+广搜
After going through the receipts from your car trip through Europe this summer, you realised that th ...
- 一文带你学会基于SpringAop实现操作日志的记录
前言 大家好,这里是经典鸡翅,今天给大家带来一篇基于SpringAop实现的操作日志记录的解决的方案.大家可能会说,切,操作日志记录这么简单的东西,老生常谈了.不! 网上的操作日志一般就是记录操作人, ...
- Mysql的一次查询的过程
1.用户发起请求,这里往往时多线程并发访问 2.去数据库线程池拿数据库链接,如果没有线程池,每次访问都要和数据库建立一次连接,非常耗时,效率低下 3.数据库层面上来说,可能会有多个系统同时访问它,所以 ...
- 用Linux感觉低效吗?来看看这几个技巧!
Linux已经成为目前最火的操作系统之一,尽管现在的Linux用户很多,但很多使用Linux的同学发现,他们在Linux下的工作效率并不高,那么这是为什么呢?其实使用Linux也可以很舒适,通过一 ...