当前位置: 移动技术网 > IT编程>开发语言>.net > 浅谈ASP.NET Core中间件实现分布式 Session

浅谈ASP.NET Core中间件实现分布式 Session

2017年11月17日  | 移动技术网IT编程  | 我要评论

天佑和阿哲怎么了,矿石收音机论坛,漆黑的

1.1. 中间件原理

1.1.1. 什么是中间件

中间件是段代码用于处理请求和响应,通常多个中间件链接起来形成管道,由每个中间件自己来决定是否要调用下一个中间件。

1.1.2. 中间件执行过程

举一个示例来演示中间件的执行过程(分别有三个中间件:日志记录、权限验证和路由):当请求进入应用程序时,执行执行日志记录的中间件,它记录请求属性并调用链中的下一个中间件权限验证,如果权限验证通过则将控制权传递给下一个中间件,不通过则设置401 http代码并返回响应,响应传递给日志中间件进行返回。

1.1.3. 中间件的配置

中间件配置主要是用runmapuse方法进行配置;简单的中间件可以直接使用匿名方法就可以搞定,如下代码:

app.run(async (context,next) =>
    {
      await context.response.writeasync("environment " + env);
      await next();
    });

如果想重用中间件,就需要单独封装到一个类中进行调用。

1.2. 依赖注入中间件

在实际项目中,中间件往往需要调用其它对象的方法。所以要创建对象之间的依赖,由于asp.net core 内置的依赖注入系统,写程序的时候可以创建更优雅的代码。

首先需要要在ioc容器中注册类,就是startup类中的configureservices方法中进行注册,configureservices方法会在configure方法之前被执行。以便在用中间件时所有依赖都准备好了。

现在有一个greeter类:

public class greeter : igreeter
{
  public string greet()
  {
    return "hello from greeter!";
  }
}

public interface igreeter
{
  string greet();
}

第一步在configureservices方法中进行注册

public void configureservices(iservicecollection services)
{
  services.addtransient<igreeter, greeter>();
}

笔者这里使用的是addtransient进行注册,该方法在每次请求时创建该类的新实例。可以选择其它方法:addsingleton,addscoped或简单的add(所有在幕后前使用)。整个di系统在官方文档中有所描述。

在注册了依赖项后,就可以使用它们了。iapplicationbuilder实例允许在configure方法中有一个requestservices属性用于获取greeter实例。由于已经注册了这个igreeter接口,所以不需要将中间件与具体的greeter实现相结合。

app.use(async (ctx, next) =>
  {
    igreeter greeter = ctx.requestservices.getservice<igreeter>();
    await ctx.response.writeasync(greeter.greet());
    await next();
  });

如果greeter类有一个参数化的构造函数,它的依赖关系也必须在其中注册configureservices

中间件可以很容易解决依赖关系。可以向中间件构造函数添加其他参数:

public class mymiddleware
{
  private readonly requestdelegate _next;
  private readonly igreeter _greeter;

  public mymiddleware(requestdelegate next, igreeter greeter)
  {
    _next = next;
    greeter = greeter;
  }

  public async task invoke(httpcontext context)
  {
    await context.response.writeasync(_greeter.greet());
    await _next(context);
  }
}

或者,可以将此依赖关系添加到invoke方法中:

public async task invoke(httpcontext context, igreeter greeter)
{
  await context.response.writeasync(greeter.greet());
  await _next(context);
}

如果di系统知道这些参数的类型,则在类被实例化时,它们将被自动解析。很简单!

1.3. cookies和session中间件

1.3.1. session

http是一个无状态协议,web服务器将每一个请求都视为独立请求。并且不保存之前请求中用户的值。

session 状态是asp.net core提供的一个功能,它可以在用户通应用访问网络服务器的时候保存和存储用户数据。由服务器上的字典和散列表组成,session状态通过浏览器的请求中得到,session的数据保存到缓存中。

asp.net core通过包含session id的cookie来维护会话状态,每个请求都会携带此session id。

microsoft.aspnetcore.session包中提供的中间件用来管理session状态。要启用session中间件,startup类里面需要做以下几个操作:

  1. 使用任何一个实现了idistributedcache接口的服务来启用内存缓存,
  2. 设置addsession回调,由于addsession是在microsoft.aspnetcore.session包内实现的,所以必须在nuget中添加microsoft.aspnetcore.session包
  3. usesession回调

具体示例代码如下:

using microsoft.aspnetcore.builder;
using microsoft.extensions.dependencyinjection;
using system;

public class startup
{
  public void configureservices(iservicecollection services)
  {
    services.addmvc();

    // 添加一个内存缓存
    services.adddistributedmemorycache();

    services.addsession(options =>
    {
      // 设置10秒钟session过期来测试
      options.idletimeout = timespan.fromseconds(10);
      options.cookie.httponly = true;
    });
  }

  public void configure(iapplicationbuilder app)
  {
    app.usesession();
    app.usemvcwithdefaultroute();
  }
}

上面代码中idletimeout属性用来确定用户多久没有操作时丢弃session。此属性和cookie超时无关,通过session中间件的每个请求都会重置超时时间。

1.3.2. session保存到redis中

实现分布式session方法官方提供有redis、sql server等。但是sql server效率对于这种以key/value获取值的方式远远不及redis效率高,所以这里笔者选用redis来作示例实现分布式session。

准备redis

由于目前redis还不支持windows,所以大家在安装redis的时候准备一台linux操作系统,笔者这里的系统是ubuntu 16.04;下载及安装方式可以参考官方示例。

安装成功以后启动redis 服务,如果看到以下信息,就代表redis启动成功:

相关配置

首先需要用nuget安装包microsoft.extensions.caching.redis,安装成功以后就可以在app.csproj文件中可以看到。

在configure方法中添加app.usesession();然后再configureservices添加redis服务

public void configureservices(iservicecollection services){
  services.adddistributedrediscache(options=>{
    options.configuration="127.0.0.1"; //多个redis服务器:{redisip}:{redis端口},{redisip}:{redis端口}
    options.instancename="sampleinstance";
  });
  services.addmvc();
  services.addsession();
}

以上代码中笔者只用一个redis服务器来作测试,实际项目中需要多个redis服务器;配置方法如:options.configuration="地址1:端口,地址2:端口";,这里笔者并没有给端口而是用的默认端口6379

完整代码

startup.cs

using system.collections.generic;
using system.linq;
using system.threading.tasks;
using microsoft.aspnetcore.builder;
using microsoft.aspnetcore.hosting;
using microsoft.extensions.configuration;
using microsoft.extensions.dependencyinjection;
using microsoft.extensions.caching.redis;
using microsoft.extensions.caching.distributed;

namespace app{  
  public class startup{    
    public startup(iconfiguration configuration)    
    {      
      configuration = configuration;    
    }
    public iconfiguration configuration { get; }

    public void configureservices(iservicecollection services){           
      services.adddistributedrediscache(options =>{        
        options.configuration = "127.0.0.1";        
        options.instancename = "sampleinstance";      
      });      
      services.addmvc();      
      services.addsession();    
    }

    public void configure(iapplicationbuilder app, ihostingenvironment env){      
      if (env.isdevelopment())
      {        
        app.usedeveloperexceptionpage();      
      }      
      else      
      {        
        app.useexceptionhandler("/home/error");      
      }
      app.usesession();
      app.usestaticfiles();
      app.usemvc(routes =>{        
        routes.maproute(name: "default",template: "{controller=home}/{action=index}/{id?}");      
      });    
    }  
  }
}

homecontroler.cs

public class homecontroller : controller  
{    
  public iactionresult index()    
  {      
    httpcontext.session.set("apptest",encoding.utf8.getbytes("apptestvalue"));
    return view();    
  }
  public iactionresult showredis()    
  {      
    byte[] temp;
    if(httpcontext.session.trygetvalue("apptest",out temp))
    {        
      viewdata["redis"]=encoding.utf8.getstring(temp);      
    }      
    return view();    
  }
}

index页面只做一件事给session设置值:"apptestvalue",showredis页面显示session值。

showredis.cshtml

redis session value:viewdata["redis"]

演示结果

现在开始运行页面,首先直接进入到showredis页面,session值显示为空

当点击setsessionvalue以后,再次回到showredis页面,session就值显示出来了

看到apptestvalue代表session值已经存到redis里面,怎样证明apptestvalue值是从redis里面取到呢?接下来就证明给大家看。

1.3.3. 实现分布session

前面已经将session保存到redis中,但是大家不清楚这个值是否是真的保存到redis里面去了还是在项目内存中;所以这里就实现在两个不的应用程序(或两台不同的机器)中共享session,也就是实现分布式session,分布式即代表了不同的机器不同的应用程序,但往往有下面的一种尴尬的情况,就算是每个http请求时都携带了相同的cookie值。

造成这个的问题的原因是每个机器上面的asp.net core的应用程序的密钥是不一样的,所以没有办法得到前一个应用程序保存的session数据;为了解决这个问题,.net core团队为提供了microsoft.aspnetcore.dataprotection.azurestorage和microsoft.aspnetcore.dataprotection.redis包将密钥保存到azure或redis中。这里选择将密钥保存到redis。

利用microsoft.aspnetcore.dataprotection.redis包提供的persistkeystoredis重载方法将密钥保存到redis里面去。所以这里需要在configureservices方法中添adddataprotection()

var redis = connectionmultiplexer.connect("127.0.0.1:6379");
  services.adddataprotection()
    .setapplicationname("session_application_name")
    .persistkeystoredis(redis, "dataprotection-keys");

下面演示怎样实现分布式session

配置步骤

同时创建两个项目,分别为app1和app2

添加microsoft.aspnetcore.dataprotection.redisstackexchange.redis.strongname包

由于在同一台机器上,asp.net core程序默认启动的时候端口为5000,由于app1已经占用了,所以将app2的启端口设置为5001

完整代码

app1项目

startup.cs

using system.collections.generic;
using system.linq;
using system.threading.tasks;
using microsoft.aspnetcore.builder;
using microsoft.aspnetcore.hosting;
using microsoft.extensions.configuration;
using microsoft.extensions.dependencyinjection;
using microsoft.extensions.caching.redis;
using microsoft.extensions.caching.distributed;

namespace app1{  
  public class startup{    
    public startup(iconfiguration configuration)    
    {      
      configuration = configuration;    
    }
    public iconfiguration configuration { get; }

    public void configureservices(iservicecollection services){
      var redis = connectionmultiplexer.connect("127.0.0.1:6379");
      services.adddataprotection()
        .setapplicationname("session_application_name")
        .persistkeystoredis(redis, "dataprotection-keys");     
      services.adddistributedrediscache(options =>{        
        options.configuration = "127.0.0.1";        
        options.instancename = "sampleinstance";      
      });      
      services.addmvc();      
      services.addsession();    
    }

    public void configure(iapplicationbuilder app, ihostingenvironment env){      
      if (env.isdevelopment())
      {        
        app.usedeveloperexceptionpage();      
      }      
      else      
      {        
        app.useexceptionhandler("/home/error");      
      }
      app.usesession();
      app.usestaticfiles();
      app.usemvc(routes =>{        
        routes.maproute(name: "default",template: "{controller=home}/{action=index}/{id?}");      
      });    
    }  
  }
}

homecontroler.cs

public class homecontroller : controller  
{    
  public iactionresult index()    
  {      
    httpcontext.session.set("app1test",encoding.utf8.getbytes("app1testvalue"));
    return view();    
  }
  public iactionresult showredis()    
  {      
    byte[] temp;
    if(httpcontext.session.trygetvalue("app1test",out temp))
    {        
      viewdata["redis"]=encoding.utf8.getstring(temp);      
    }      
    return view();    
  }
}

showredis.cshtml

redis session value:viewdata["redis"]

app2项目

startup.cs
配置同app1配置一样。

homecontroler.cs

public class homecontroller : controller  
{    
  public iactionresult index()    
  {      
    byte[] temp;
    if(httpcontext.session.trygetvalue("app1test",out temp))
    {        
      viewdata["redis"]=encoding.utf8.getstring(temp);      
    } 
    return view();    
  }
}

index.cshtml

viewdata["redis"]

运行效果

app1 项目

首次打开进入showredis页面,session值为空

点击setsessionvalue以后,再回到showredis页面:

app2项目,直接在浏览器访问:

以上是用redis实现分布式session示例。 

1.4. 总结

本节讲解了中间件的运行原理及配置过程,中间件之间对象依赖关系的配置和平时项目中常用到session的配置问题。并在实际代码展示了怎样使用中间件实现分布式session。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网