1.1. 中間件原理
1.1.1. 什么是中間件
中間件是段代碼用於處理請求和響應,通常多個中間件鏈接起來形成管道,由每個中間件自己來決定是否要調用下一個中間件。
1.1.2. 中間件執行過程
舉一個示例來演示中間件的執行過程(分別有三個中間件:日志記錄、權限驗證和路由):當請求進入應用程序時,執行執行日志記錄的中間件,它記錄請求屬性並調用鏈中的下一個中間件權限驗證,如果權限驗證通過則將控制權傳遞給下一個中間件,不通過則設置401 HTTP代碼並返回響應,響應傳遞給日志中間件進行返回。
1.1.3. 中間件的配置
中間件配置主要是用Run
、Map
和Use
方法進行配置,三者的不同參見上篇ASP.NET Core 運行原理剖析;簡單的中間件可以直接使用匿名方法就可以搞定,如下代碼:
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
類里面需要做以下幾個操作:
- 使用任何一個實現了
IDistributedCache
接口的服務來啟用內存緩存, - 設置
AddSession
回調,由於AddSession
是在Microsoft.AspNetCore.Session
包內實現的,所以必須在Nuget中添加Microsoft.AspNetCore.Session
包 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.Redis
和StackExchange.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項目,直接在瀏覽器訪問:http://localhost:5001
以上是用Redis實現分布式Session示例。
1.4. 總結
本節講解了中間件的運行原理及配置過程,中間件之間對象依賴關系的配置和平時項目中常用到Session的配置問題。並在實際代碼展示了怎樣使用中間件實現分布式Session。
作者:xdpie 出處: http://www.cnblogs.com/vipyoumay/p/7771237.html