ASP.Net Core的基本配置
.在VS中調試的時候有很多修改Web應用運行端口的方法。但是在開發、調試微服務應用的時候可能需要同時在不同端口上開啟多個服務器的實例,因此下面主要看看如何通過命令行指定Web應用的端口(默認5000)
可以通過設置臨時環境變量ASPNETCORE URLS來改變默認的端口、域名,也就是執行 dotnet xxx.dll之前執行set ASPNETCORE_URLS=http://127.0.0.1:5001來設置環境變量。
如果需要在程序中讀取端口、域名(后續服務治理會用到) ,用ASPNETCORE URLS環境變量就不太方便,可以自定義配置文件, 自己讀取設置。
修改Program.cs
public static IWebHost BuildWebHost(string[] args) { var config = new ConfigurationBuilder() .AddCommandLine(args) .Build(); String ip = config["ip"]; String port = config["port"]; return WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .UseUrls($"http://{ip}:{port}") .Build(); }
然后啟動的時候:
dotnet WebApplication5.dll--ip 127.0.0.1-port 8889
.Net Core因為跨平台,所以可以不依賴於IIS運行了。可以用.Net Core內置的kestrel服務器運行網站,當然真正面對終端用戶訪問的時候一般通過Nginx等做反向代理。
Consul服務治理發現
Consul是注冊中心,服務提供者、服務消費者等都要注冊到Consul中,這樣就可以實, ,現服務提供者、服務消費者的隔離。
除了Consul之外,還有Eureka,Zookeeper等類似軟件。
Consul服務安裝
consul下載地址https://www.consul.io/
運行
consul.exe agent -dev
這是開發環境測試,生產環境要建集群,要至少一台Server,多台Agent consul
監控頁面http://127.0.0.1:8500/consult
主要做三件事:提供服務到ip地址的注冊;提供服務到ip地址列表的查詢;對提供服務方的健康檢查(HealthCheck) ;
.Net Core連接Consul
新建Asp.Net Core WebAPI項目WebApplication4,安裝Consul nuget包
Install-Package Consul
Rest服務的准備
先使用使用默認生成的ValuesController做測試
再提供一個HealthController.cs
[Route("api/Health")] public class HealthController : Controller { [HttpGet] public IActionResult Get() { return Ok("ok"); } }
服務器從命令行中讀取ip和端口
讓Rest服務注冊到Consul中
Startup.cs:
using Consul; public void Configure(IApplicationBuilder app, IHostingEnvironment env) { //... ... app.UseMvc(); String ip = Configuration["ip"];//部署到不同服務器的時候不能寫成127.0.0.1或者0.0.0.0,因為這是讓服務消費者調用的地址 Int32 port = Int32.Parse(Configuration["port"]); //向consul注冊服務 ConsulClient client = new ConsulClient(ConfigurationOverview); Task<WriteResult> result= client.Agent.ServiceRegister(new AgentServiceRegistration() { ID = "apiservice1" + Guid.NewGuid(),//服務編號,不能重復,用Guid最簡單 Name = "apiservice1",//服務的名字 Address = ip,//我的ip地址(可以被其他應用訪問的地址,本地測試可以用127.0.0.1,機房環境中一定要寫自己的內網ip地址) Port = port,//我的端口 Check = new AgentServiceCheck() { DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//服務停止多久后反注冊 Interval =TimeSpan.FromSeconds(10),//健康檢查時間間隔,或者稱為心跳間隔 HTTP =$"http://{ip}:{port}/api/health",//健康檢查地址, Timeout =TimeSpan.FromSeconds(5) } }); } private static void ConfigurationOverview(ConsulClientConfiguration obj) { obj.Address = new Uri("http://127.0.0.1:8500"); obj.Datacenter = "dc1"; }
注意不同實例一定要用不同的Id,即使是相同服務的不同實例也要用不同的ld,上面的代碼用Guid做Id,確保不重復。相同的服務用相同的Name. Address、 Port是供服務消 "費者訪問的服務器地址(或者IP地址)及端口號。Check則是做服務健康檢查的(解釋一下)。
在注冊服務的時候還可以通過AgentServiceRegistration的Tags屬性設置額外的標簽。
通過命令行啟動兩個實例
dotnet WebApplication4.dll --ip 127.0.0.1 --port 5001 dotnet WebApplication4.dll --ip 127.0.0.1 --port 5002
應用停止的時候反注冊。
服務查詢
新建控制台項目queryconsul1,並引用nuget包
using Consul; static void Main(string[] args) { using (ConsulClient consulClient = new ConsulClient(c=>c.Address=new Uri("http://127.0.0.1:8500"))) { //consulClient.Agent.Services()獲取consul中注冊的所有的服務 Dictionary<String,AgentService> services = consulClient.Agent.Services().Result.Response; foreach (KeyValuePair<String, AgentService> kv in services) { Console.WriteLine($"key={kv.Key},{kv.Value.Address},{kv.Value.ID},{kv.Value.Service},{kv.Value.Port}"); } //獲取所有服務名字是"apiservice1"所有的服務 var agentServices = services.Where(s => s.Value.Service.Equals("apiservice1", StringComparison.CurrentCultureIgnoreCase)) .Select(s => s.Value); //根據當前TickCount對服務器個數取模,“隨機”取一個機器出來,避免“輪詢”的負載均衡策略需要計數加鎖問題 var agentService = agentServices.ElementAt(Environment.TickCount%agentServices.Count()); Console.WriteLine($"{agentService.Address},{agentService.ID},{agentService.Service},{agentService.Port}"); } Console.ReadKey(); }
編寫服務消費者
創建類庫RestTools
添加Consul nuget包引用
Install-Package Consul
Install-Package Newtonsoft.Json
創建消息返回類ResponseEntity.cs

public class ResponseEntity<T> { /// <summary> /// 返回狀態碼 /// </summary> public HttpStatusCode StatusCode { get; set; } /// <summary> /// 返回的json反序列化出來的對象 /// </summary> public T Body { get; set; } /// <summary> /// 響應的報文頭 /// </summary> public HttpResponseHeader Headers { get; set; } }
創建轉發消息類RestTemplate.cs

public class RestTemplate { private String consulServerUrl; public RestTemplate(String consulServerUrl) { this.consulServerUrl = consulServerUrl; } /// <summary> /// 獲取服務的一個IP地址 /// </summary> /// <param name="serviceName">consul服務IP</param> /// <returns></returns> private async Task<String> ResolveRootUrlAsync(String serviceName) { using (var consulClient = new ConsulClient(c => c.Address = new Uri(consulServerUrl))) { var services = (await consulClient.Agent.Services()).Response; var agentServices = services.Where(s => s.Value.Service.Equals(serviceName, StringComparison.InvariantCultureIgnoreCase)).Select(s => s.Value); //TODO:注入負載均衡策略 var agentService = agentServices.ElementAt(Environment.TickCount % agentServices.Count()); //根據當前TickCount對服務器個數取模,“隨機”取一個機器出來,避免“輪詢”的負載均衡策略需要計數加鎖問題 return agentService.Address + ":" + agentService.Port; } } /// <summary> /// //把http://apiservice1/api/values轉換為http://192.168.1.1:5000/api/values /// </summary> private async Task<String> ResolveUrlAsync(String url) { Uri uri = new Uri(url); String serviceName = uri.Host;//apiservice1 String realRootUrl = await ResolveRootUrlAsync(serviceName); return uri.Scheme + "://" + realRootUrl + uri.PathAndQuery; } /// <summary> /// Get請求轉換 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="url">請求地址</param> /// <param name="requestHeaders">請求頭</param> /// <returns></returns> public async Task<ResponseEntity<T>> GetForEntityAsync<T>(String url, HttpRequestHeaders requestHeaders = null) { using (HttpClient httpClient=new HttpClient()) { HttpRequestMessage requestMsg = new HttpRequestMessage(); if (requestHeaders!=null) { foreach (var header in requestHeaders) { httpClient.DefaultRequestHeaders.Add(header.Key, header.Value); } } requestMsg.Method = HttpMethod.Get; //http://apiservice1/api/values轉換為http://192.168.1.1:5000/api/values requestMsg.RequestUri = new Uri(await ResolveUrlAsync(url)); var result = await httpClient.SendAsync(requestMsg); ResponseEntity<T> responseEntity = new ResponseEntity<T>(); responseEntity.StatusCode = result.StatusCode; String bodyStr = await result.Content.ReadAsStringAsync(); responseEntity.Body = JsonConvert.DeserializeObject<T>(bodyStr); responseEntity.Headers = responseEntity.Headers; return responseEntity; } } }
編寫控制台進行消費
這里用控制台測試,真實項目中服務消費者通常也是另外一個Web應用。

static void Main(string[] args) { RestTemplate rest = new RestTemplate("http://127.0.0.1:8500"); //RestTemplate把服務的解析和發請求以及響應反序列化幫我們完成 ResponseEntity<String[]> resp = rest.GetForEntityAsync<String[]>("http://apiservice1/api/values").Result; Console.WriteLine(resp.StatusCode); Console.WriteLine(String.Join(",",resp.Body)); Console.ReadKey(); }
測試結果:
解析RestTemplate代碼。主要作用:
1) 根據url到Consul中根據服務的名字解析獲取一個服務實例,把路徑轉換為實際連接的服務器;負載均衡,這里用的是簡單的隨機負載均衡,這樣服務的消費者就不用自己指定要訪問那個服務提供,者了,解耦、負載均衡。
2) 負載均衡還可以根據權重隨機(不同服務器的性能不一樣,這樣注冊服務的時候通過Tags來區,"分),還可以根據消費者IP地址來選擇服務實例(涉及到一致性Hash的優化)等。
3) RestTemplate還負責把響應的ison反序列化返回結果。服務的注冊者、消費者都是網站內部服務器之間的事情,對於終端用戶是不涉及這些的。
終端用戶是不訪問consul的。對終端用戶來講是對的Web服務器, Web服務器是服務的消費者。
簡化服務的注冊
每次啟動、注冊服務都要指定一個端口,本地測試集群的時候可能要啟動多個實例,很麻煩.
在ASP. Net Core中只要設定端口為0,那么服務器會隨機找一個可用的端口綁定(測試一下).,但是沒有找到讀取到這個隨機端口號的方法.因此自己寫:
新建Tools.cs工具類

public class Tools { /// <summary> /// 產生一個介於minPort-maxPort之間的隨機可用端口 /// </summary> /// <param name="minPort"></param> /// <param name="maxPort"></param> /// <returns></returns> public static int GetRandAvailablePort(int minPort = 1024, int maxPort = 65535) { Random r = new Random(); while (true) { int port = r.Next(minPort, maxPort); if (!IsPortInUsed(port)) { return port; } } } /// <summary> /// 判斷port端口是否在使用中 /// </summary> /// <param name="port"></param> /// <returns></returns> private static bool IsPortInUsed(int port) { IPGlobalProperties ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties(); IPEndPoint[] ipsTCP = ipGlobalProperties.GetActiveTcpListeners(); if (ipsTCP.Any(p=>p.Port==port)) { return true; } IPEndPoint[] ipsUDP = ipGlobalProperties.GetActiveUdpListeners(); if (ipsUDP.Any(p=>p.Port==port)) { return true; } TcpConnectionInformation[] tcpConnInfoArray = ipGlobalProperties.GetActiveTcpConnections(); if (tcpConnInfoArray.Any(conn=>conn.LocalEndPoint.Port==port)) { return true; } return false; } }
使用方法

public static IWebHost BuildWebHost(string[] args) { var config = new ConfigurationBuilder() .AddCommandLine(args) .Build(); String ip = config["ip"]; String port = config["port"]; if (port=="0") { port = Tools.GetRandAvailablePort().ToString(); } return WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .UseUrls($"http://{ip}:{port}") .Build(); }
在程序啟動的時候如果port=0或者沒有指定port,則自己調用GetRandAvailablePort獲取可用端口。
熔斷降級
什么是熔斷降級
熔斷器如同電力過載保護器。它可以實現快速失敗,如果它在一段時間內偵測到許多類似的錯誤,會強迫其以后的多個調用快速失敗,不再訪問遠程服務器,從而防止應用程序不斷地嘗試執行可能會失敗的操作,使得應用程序繼續執行而不用等待修正錯誤,或者浪費時間去等到長時間的超時產生。
降級的目的是當某個服務提供者發生故障的時候,向調用方返回一個錯誤響應或者替代響應。舉例子:如視頻播放器請求playsafe的替代方案;加載內容評論時如果出錯,則以緩存中加載或者顯示"評論暫時不可用" 。
Polly簡介
.Net Core中有一個被.Net基金會認可的庫Polly,可以用來簡化熔斷降級的處理。主要功能:重試(Retry) ;斷路器(Circuit-breaker) ;超時檢測(Timeout) ;緩存(Cache) ;,失敗處理(FallBack) ;
官網: https://github. com/App-vNext/Polly
介紹文章: https://www.cnblogs.com/CreateMyself/p/7589397.html
Install-Package Polly -Version 5.9.0
6.0.1對緩存還不支持,因此現在暫時先用5.9.0版本.
Polly簡單使用
使用Policv的靜態方法創建ISyncPolicy實現類對象,創建方法既有同步方法也有異步方法,根 據自己的需要選擇。下面演示同步的,異步的用法類似。
舉例:當發生ArgumentException異常的時候,執行Fallback代碼。
新建pollytest1控制台項目,添加nuget引用
try { ISyncPolicy policy = Policy.Handle<ArgumentException>(ex => ex.Message == "年齡參數錯誤") .Fallback(() => { Console.WriteLine("出錯了"); }); policy.Execute(()=>{ //這里是可能會產生問題的業務系統代碼 Console.WriteLine("開始任務"); throw new ArgumentException("年齡參數錯誤"); //throw new Exception("haha"); //Console.WriteLine("完成任務"); }); } catch (Exception ex) { Console.WriteLine($"未處理異常:{ex}"); }
詳解Polly異常處理
. Handle<Exception> (ex->ex. Message. Contains ("aa"))
參數委托的返回值是boolean類型,如果返回true,就是“這個異常能被我處理”,否則就是“我處理不了" ,會導致未處理異常被拋出。
比如可以實現“我能處理XXX錯誤信息"
Handle<WebException> (ex=>ex. Status==WebExceptionStatus. SendFailure)
獲取異常信息就調用這個重載
public static FallbackPolicy Fallback(this PolicyBuilder policyBuilder, Action fallbackAction, Action<Exception> onFallback); //省略 .Fallback(() =>{},(ex)=> { Console.WriteLine("執行出錯,異常"+ex); });
異常處理的套路
ISyncPolicy policy = Policy.Handle<AException>() .Or<BException>() .Or<CException>() ...... .
CircuitBreaker()/.Fallback()/.Retry()/.RetryForever()/.WaitAndRetry()/.WaitAndRetryForever()
當發生AException或者BException或者......的時候進行CircuitBreaker()/.Fallback()等處理。
這些處理不能簡單的鏈式調用,要用到后面的Wrap。
例如下面這樣是不行的
ISyncPolicy policy = Policy .Handle<Exception>() .Retry(3) .Fallback(()=> { Console.WriteLine("執行出錯"); });//這樣不行 policy.Execute(() => { Console.WriteLine("開始任務"); throw new ArgumentException("Hello world!"); Console.WriteLine("完成任務"); });
重試處理

try { ISyncPolicy policy = Policy.Handle<Exception>() .RetryForever();//一直重試 policy.Execute(() => { Console.WriteLine("開始任務"); if (DateTime.Now.Second % 10 != 0) { throw new Exception("出錯"); } Console.WriteLine("完成任務"); }); } catch (Exception ex) { Console.WriteLine($"未處理異常:{ex}"); } //RetryForever()是一直重試直到成功 //Retry()是重試最多一次; //Retry(n)是重試最多n次; //WaitAndRetry()可以實現“如果出錯等待100ms再試還不行再等150ms秒。。。。”,重載方法很多,一看就懂,不再一一介紹。還有WaitAndRetryForever。
短路保護Circuit Breaker
出現N次連續錯誤,則把“熔斷器”(保險絲)熔斷,等待一段時間,等待這段時間內如果再Execute則直接拋出BrokenCircuitException異常。等待時間過去之后,再執行Execute的時候如果又錯了(一次就夠了),那么繼續熔斷一段時間,否則就回復正常。
這樣就避免一個服務已經不可用了,還是使勁的請求給系統造成更大壓力。

ISyncPolicy policy = Policy.Handle<Exception>() .CircuitBreaker(6, TimeSpan.FromSeconds(5));//連續出錯6次之后熔斷5秒(不會再去嘗試執行業務代碼)。 while (true) { Console.WriteLine("開始Execute"); try { policy.Execute(() => { Console.WriteLine("開始任務"); throw new Exception("出錯"); Console.WriteLine("完成任務"); }); } catch (Exception ex) { Console.WriteLine("execute出錯" + ex.GetType() + ":" + ex.Message); } Thread.Sleep(500); }
策略封裝
可以把多個ISyncPolicy合並到一起執行:
policy3= policy1.Wrap(policy2);
執行policy3就會把policy1、policy2封裝到一起執行
policy9=Policy.Wrap(policy1, policy2, policy3, policy4, policy5);把更多一起封裝。
超時處理
創建一個3秒鍾(注意單位)的超時策略。
ISyncPolicy policy = Policy.Timeout(3, TimeoutStrategy.Pessimistic);
創建一個3秒鍾(注意單位)的超時策略。超時策略一般不能直接用,而是和其他封裝到一起用:
ISyncPolicy policy = Policy.Handle<Exception>() .Fallback(() => { Console.WriteLine("執行出錯"); }); policy = policy.Wrap(Policy.Timeout(2, TimeoutStrategy.Pessimistic)); policy.Execute(() => { Console.WriteLine("開始任務"); Thread.Sleep(5000); Console.WriteLine("完成任務"); });
上面的代碼就是如果執行超過2秒鍾,則直接Fallback,Execute中的代碼也會被強行終止(引發TimeoutRejectedException異常)。
這個的用途:請求網絡接口,避免接口長期沒有響應造成系統卡死。
TimeoutStrategy.Optimistic是主動通知代碼,告訴他“到期了”,由代碼自己決定是不是繼續執行,局限性很大,一般不用。
下面的代碼,如果發生超時,重試最多3次(也就是說一共執行4次哦)。
ISyncPolicy policy = Policy.Handle<TimeoutRejectedException>() .Retry(1); policy = policy.Wrap(Policy.Timeout(3, TimeoutStrategy.Pessimistic)); policy.Execute(() => { Console.WriteLine("開始任務"); Thread.Sleep(5000); Console.WriteLine("完成任務"); });
緩存
緩存的意思就是N秒內只調用一次方法,其他的調用都返回緩存的數據。
目前只支持Polly 5.9.0,不支持最新版
Install-Package Polly.Caching.MemoryCache
功能局限性也大,簡單講一下,后續先不用這個實現緩存原則:別人的好用我就拿來用,不好用我就自己造。
命令空間都寫到代碼中,因為有容易引起混淆的同名類。

//Install-Package Microsoft.Extensions.Caching.Memory Microsoft.Extensions.Caching.Memory.IMemoryCache memoryCache = new Microsoft.Extensions.Caching.Memory.MemoryCache(new Microsoft.Extensions.Caching.Memory.MemoryCacheOptions()); //Install-Package Polly.Caching.MemoryCache Polly.Caching.MemoryCache.MemoryCacheProvider memoryCacheProvider = new Polly.Caching.MemoryCache.MemoryCacheProvider(memoryCache); CachePolicy policy = Policy.Cache(memoryCacheProvider, TimeSpan.FromSeconds(5)); Random rand = new Random(); while (true) { int i = rand.Next(5); Console.WriteLine("產生"+i); var context = new Context("doublecache" + i); int result = policy.Execute(ctx => { Console.WriteLine("Execute計算"+i); return i * 2; },context); Console.WriteLine("計算結果:"+result); Thread.Sleep(500); }
AOP框架基礎
如果直接使用Polly,那么就會造成業務代碼中混雜大量的業務無關代碼。我們使用AOP(如果不了解AOP,請自行參考網上資料)的方式封裝一個簡單的框架,模仿Spring cloud中的Hystrix。
需要先引入一個支持.Net Core的AOP,目前我發現的最好的.Net Core下的AOP框架是AspectCore(國產,動態織入),其他要不就是不支持.Net Core,要不就是不支持對異步方法進行攔截。MVC Filter
GitHub:https://github.com/dotnetcore/AspectCore-Framework
Install-Package AspectCore.Core
新建控制台項目aoptest1,並添加AspectCore.Core包引用
編寫攔截器CustomInterceptorAttribute.cs,一般繼承自AbstractInterceptorAttribute
public class CustomInterceptorAttribute : AbstractInterceptorAttribute { //每個被攔截的方法中執行 public async override Task Invoke(AspectContext context, AspectDelegate next) { try { Console.WriteLine("Before service call"); await next(context); } catch (Exception) { Console.WriteLine("Service threw an exception!"); throw; } finally { Console.WriteLine("After service call"); } } }
編寫需要被代理攔截的類 Person.cs,在要被攔截的方法上標注CustomInterceptorAttribute 。類需要是public類,方法需要是虛!方法,支持異步方法,因為動態代理是動態生成被代理的類的動態子類實現的。
public class Person { [CustomInterceptor] public virtual void Say(string msg) { Console.WriteLine("service calling..."+msg); } }
通過AspectCore創建代理對象
ProxyGeneratorBuilder proxyGeneratorBuilder = new ProxyGeneratorBuilder(); using (IProxyGenerator proxyGenerator=proxyGeneratorBuilder.Build()) { Person p = proxyGenerator.CreateClassProxy<Person>(); p.Say("Hello World"); Console.WriteLine(p.GetType()); Console.WriteLine(p.GetType().BaseType); } Console.ReadKey();
注意p指向的對象是AspectCore生成的Person的動態子類的對象,直接new Person是無法被,攔截的.
執行結果:
創建簡單的熔斷降級框架
新建控制台項目 hystrixtest1
新建類Person.cs
public class Person { public virtual async Task<string> HelloAsync(string name) { Console.WriteLine("hello"+name); return "ok"; } public async Task<string> HelloFallBackAsync(string name) { Console.WriteLine("執行失敗"+name); return "fail"; } }
目標:在執行 HelloAsync 失敗的時候自動執行 HelloFallBackAsync ,達到熔斷降級
編寫HystrixCommandAttribute.cs
[AttributeUsage(AttributeTargets.Method)] public class HystrixCommandAttribute : AbstractInterceptorAttribute { public string FallBackMethod { get; set; } public HystrixCommandAttribute(string fallBackMethod) { this.FallBackMethod = fallBackMethod; } public override async Task Invoke(AspectContext context, AspectDelegate next) { try { await next(context);//執行被攔截的方法 } catch (Exception ex) { /* * context.ServiceMethod 被攔截的方法 * context.ServiceMethod.DeclaringType 被攔截的方法所在的類 * context.Implementation 實際執行的對象 * context.Parameters 方法參數值 * 如果執行失敗,則執行FallBackMethod */ var fallBackMethod = context.ServiceMethod.DeclaringType.GetMethod(this.FallBackMethod); object fallBackResult = fallBackMethod.Invoke(context.Implementation, context.Parameters); context.ReturnValue = fallBackResult; await Task.FromResult(0); } } }
修改Person.cs類

public class Person { [HystrixCommand(nameof(HelloFallBackAsync))] public virtual async Task<string> HelloAsync(string name)//需要是虛方法 { Console.WriteLine("hello"+name); //拋錯 String s = null; //s.ToString(); return "ok"; } public async Task<string> HelloFallBackAsync(string name) { Console.WriteLine("執行失敗"+name); return "fail"; } [HystrixCommand(nameof(AddFall))] public virtual int Add(int i, int j) { //拋錯 String s = null; //s.ToString(); return i + j; } public int AddFall(int i, int j) { return 0; } }
創建代理對象

ProxyGeneratorBuilder proxyGeneratorBuilder = new ProxyGeneratorBuilder(); using (IProxyGenerator proxyGenerator=proxyGeneratorBuilder.Build()) { Person p = proxyGenerator.CreateClassProxy<Person>(); Console.WriteLine(p.HelloAsync("Hello World").Result); Console.WriteLine(p.Add(1,2)); }
執行效果
異常執行效果
細化框架
重試: MaxRetryTimes表示最多重試幾次,如果為0則不重試, RetrvIntervalMilliseconds表示重試間隔的豪秒數;
超時: TimeOutMilliseconds執行超過多少毫秒則認為超時(0表示不檢測超時)緩存:緩存多少豪秒(0表示不緩存) ,用“類名+方法名+所有參數ToString拼接"做緩存Key.
新建控制台項目aspnetcorehystrix1,並添加AspectCore.Core、Polly包引用
Install-Package AspectCore.Core Install-Package Polly
Install-Package Microsoft.Extensions.Caching.Memory
編寫HystrixCommandAttribute.cs

/// <summary> /// 熔斷框架 /// </summary> [AttributeUsage(AttributeTargets.Method)] public class HystrixCommandAttribute : AbstractInterceptorAttribute { #region 屬性 /// <summary> /// 最多重試幾次,如果為0則不重試 /// </summary> public int MaxRetryTimes { get; set; } = 0; /// <summary> /// 重試間隔的毫秒數 /// </summary> public int RetryIntervalMilliseconds { get; set; } = 100; /// <summary> /// 是否啟用熔斷 /// </summary> public bool EnableCircuitBreater { get; set; } = false; /// <summary> /// 熔斷前出現允許錯誤幾次 /// </summary> public int ExceptionAllowedBeforeBreaking { get; set; } = 3; /// <summary> /// 熔斷多長時間(毫秒 ) /// </summary> public int MillisecondsOfBreak { get; set; } = 1000; /// <summary> /// 執行超過多少毫秒則認為超時(0表示不檢測超時) /// </summary> public int TimeOutMilliseconds { get; set; } = 0; /// <summary> /// 緩存多少毫秒(0表示不緩存),用“類名+方法名+所有參數ToString拼接”做緩存Key /// </summary> public int CacheTTLMilliseconds { get; set; } = 0; private Policy policy; //緩存 private static readonly Microsoft.Extensions.Caching.Memory.IMemoryCache memoryCache = new Microsoft.Extensions.Caching.Memory.MemoryCache(new Microsoft.Extensions.Caching.Memory.MemoryCacheOptions()); /// <summary> /// 降級方法名 /// </summary> public string FallBackMethod { get; set; } #endregion #region 構造函數 /// <summary> /// 熔斷框架 /// </summary> /// <param name="fallBackMethod">降級方法名</param> public HystrixCommandAttribute(string fallBackMethod) { this.FallBackMethod = fallBackMethod; } #endregion public override async Task Invoke(AspectContext context, AspectDelegate next) { //一個HystrixCommand中保持一個policy對象即可 //其實主要是CircuitBreaker要求對於同一段代碼要共享一個policy對象 //根據反射原理,同一個方法就對應一個HystrixCommandAttribute,無論幾次調用, //而不同方法對應不同的HystrixCommandAttribute對象,天然的一個policy對象共享 //因為同一個方法共享一個policy,因此這個CircuitBreaker是針對所有請求的。 //Attribute也不會在運行時再去改變屬性的值,共享同一個policy對象也沒問題 lock (this) { if (policy==null) { policy = Policy.Handle<Exception>() .FallbackAsync(async (ctx, t) => { AspectContext aspectContext = (AspectContext)ctx["aspectContext"]; var fallBackMethod = context.ServiceMethod.DeclaringType.GetMethod(this.FallBackMethod); Object fallBackResult = fallBackMethod.Invoke(context.Implementation, context.Parameters); //不能如下這樣,因為這是閉包相關,如果這樣寫第二次調用Invoke的時候context指向的 //還是第一次的對象,所以要通過Polly的上下文來傳遞AspectContext //context.ReturnValue = fallBackResult; aspectContext.ReturnValue = fallBackResult; }, async (ex, t) => { }); if (MaxRetryTimes>0)//重試 { policy = policy.WrapAsync(Policy.Handle<Exception>().WaitAndRetryAsync(MaxRetryTimes, i => TimeSpan.FromMilliseconds(RetryIntervalMilliseconds))); } if (EnableCircuitBreater)//熔斷 { policy = policy.WrapAsync(Policy.Handle<Exception>().CircuitBreakerAsync(ExceptionAllowedBeforeBreaking, TimeSpan.FromMilliseconds(MillisecondsOfBreak))); } if (TimeOutMilliseconds>0)//超時 { policy = policy.WrapAsync(Policy.TimeoutAsync(() => TimeSpan.FromMilliseconds(TimeOutMilliseconds), Polly.Timeout.TimeoutStrategy.Pessimistic)); } } } //把本地調用的AspectContext傳遞給Polly,主要給FallBackMethod中使用,避免閉包的坑 Context pollyCtx = new Context(); pollyCtx["aspectContext"] = context; if (CacheTTLMilliseconds>0) { //用類名+方法名+參數的下划線連接起來作為緩存key string cacheKey = "HystrixMethodCacheManager_Key_" + context.ServiceMethod.DeclaringType + "." + context.ServiceMethod + string.Join("_", context.Parameters); //嘗試去緩存中獲取。如果找到了,則直接用緩存中的值做返回值 if (memoryCache.TryGetValue(cacheKey,out var cacheValue)) { context.ReturnValue = cacheValue; } else { //如果緩存中沒有,則執行實際被攔截的方法 await policy.ExecuteAsync(ctx => next(context), pollyCtx); //存入緩存中 using (var cacheEntry=memoryCache.CreateEntry(cacheKey)) { cacheEntry.Value = context.ReturnValue; cacheEntry.AbsoluteExpiration = DateTime.Now + TimeSpan.FromMilliseconds(CacheTTLMilliseconds); } } } else//如果沒有啟用緩存,就直接執行業務方法 { await policy.ExecuteAsync(ctx => next(context), pollyCtx); } } }
編寫業務類Person.cs

public class Person//需要public類 { [HystrixCommand(nameof(Hello1FallBackAsync), MaxRetryTimes = 3, EnableCircuitBreaker = true)] public virtual async Task<String> HelloAsync(string name)//需要是虛方法 { Console.WriteLine("hello" + name); #region 拋錯 String s = null; s.ToString(); #endregion return "ok" + name; } [HystrixCommand(nameof(Hello2FallBackAsync))] public virtual async Task<string> Hello1FallBackAsync(string name) { Console.WriteLine("Hello降級1" + name); String s = null; s.ToString(); return "fail_1"; } public virtual async Task<string> Hello2FallBackAsync(string name) { Console.WriteLine("Hello降級2" + name); return "fail_2"; } [HystrixCommand(nameof(AddFall))] public virtual int Add(int i, int j) { String s = null; //s.ToString(); return i + j; } public int AddFall(int i, int j) { return 0; } [HystrixCommand(nameof(TestFallBack), CacheTTLMilliseconds = 3000)] public virtual void Test(int i) { Console.WriteLine("Test" + i); } public virtual void TestFallBack(int i) { Console.WriteLine("Test" + i); } }
創建代理對象

ProxyGeneratorBuilder proxyGeneratorBuilder = new ProxyGeneratorBuilder(); using (IProxyGenerator proxyGenerator=proxyGeneratorBuilder.Build()) { Person p = proxyGenerator.CreateClassProxy<Person>(); Console.WriteLine(p.HelloAsync("Hello World").Result); Console.WriteLine(p.Add(1, 2)); while (true) { Console.WriteLine(p.HelloAsync("Hello World").Result); Thread.Sleep(100); } }
測試結果:
正常:
一級熔斷
二級熔斷
結合asp.net core依賴注入
新建WebAPI項目aspnetcorehystrix,
並添加AspectCore.Core、Polly包引用
Install-Package AspectCore.Core
Install-Package Polly
Install-Package Microsoft.Extensions.Caching.Memory
編寫HystrixCommandAttribute.cs

編寫業務類Person.cs

public class Person//需要public類 { [HystrixCommand(nameof(HelloFallBackAsync))] public virtual async Task<string> HelloAsync(string name)//需要是虛方法 { Console.WriteLine("hello" + name); String s = null; s.ToString(); return "ok"; } public async Task<string> HelloFallBackAsync(string name) { Console.WriteLine("執行失敗" + name); return "fail"; } [HystrixCommand(nameof(AddFall))] public virtual int Add(int i, int j) { String s = null; // s.ToArray(); return i + j; } public int AddFall(int i, int j) { return 0; } }
在asp.net core項目中,可以借助於asp.net core的依賴注入,簡化代理類對象的注入,不用再自己調用ProxyGeneratorBuilder 進行代理類對象的注入了。
Install-Package AspectCore.Extensions.DependencyInjection
修改Startup.cs的ConfigureServices方法,把返回值從void改為IServiceProvider
public IServiceProvider ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddScoped<Person>(); return services.BuildAspectCoreServiceProvider(); }
其中services.AddSingleton<Person>();表 示 把Person注 入 。
BuildAspectCoreServiceProvider是讓aspectcore接管注入。
升級一波
當然要通過反射掃描所有Service類,只要類中有標記了CustomInterceptorAttribute的方法都算作服務實現類。為了避免一下子掃描所有類,所以RegisterServices還是手動指定從哪個程序集中加載。

/// <summary> /// 根據特性批量注入 /// </summary> private static void RegisterServices(Assembly assembly, IServiceCollection services) { //遍歷程序集中的所有public類型 foreach (Type type in assembly.GetExportedTypes()) { //判斷類中是否有標注了CustomInterceptorAttribute的方法 bool hasHystrixCommandAttr= type.GetMethods().Any(m => m.GetCustomAttribute(typeof(HystrixCommandAttribute)) != null); if (hasHystrixCommandAttr) { services.AddSingleton(type); } } }
public IServiceProvider ConfigureServices(IServiceCollection services) { services.AddMvc(); RegisterServices(this.GetType().Assembly, services); return services.BuildAspectCoreServiceProvider(); }
Ocelot網關
現有微服務的幾點不足:
1)對於在微服務體系中、和Consul通訊的微服務來講,使用服務名即可訪問。但是對於手機、web端等外部訪問者仍然需要和N多服務器交互,需要記憶他們的服務器地址、端口號等。一旦內部發生修改,很麻煩,而且有時候內部服務器是不希望外界直接訪問的。
2)各個業務系統的人無法自由的維護自己負責的服務器;
3)現有的微服務都是“我家大門常打開”,沒有做權限校驗。如果把權限校驗代碼寫到每個微服務上,那么開發工作量太大。
4)很難做限流、收費等。
ocelot 中文文檔:https://blog.csdn.net/sD7O95O/article/details/79623654
資料:http://www.csharpkit.com/apigateway.html
騰訊.Net大隊長“張善友”是項目主力開發人員之一。
先搞兩個短信、郵件假的服務器(這里用WebAPI代替)
新建 smsservice1 WebAPI項目,並創建SMSController.cs
[Route("api/[Controller]")] public class SMSController : Controller { [Route("Send")] public bool Send(string msg) { Console.WriteLine("發送短信"+msg); return true; } }
新建 emailservice1 WebAPI項目,並創建EmailController.cs
[Route("api/[controller]")] public class EmailController : Controller { [Route("Send")] public bool Send(string msg) { Console.WriteLine("發送郵件" + msg); return true; } }
Ocelot基本配置
Ocelot就是一個提供了請求路由、安全驗證等功能的API網關微服務。
建一個 ocelotserver1 WebAPI項目,然后把默認生成的Controller刪除,添加 Ocelot Nuget包引用
Install-Package Ocelot
項目根目錄下創建configuration.json

{ "ReRoutes": [ { "DownstreamPathTemplate": "/api/sms/{url}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 5001 } ], "UpstreamPathTemplate": "/sms/{url}", "UpstreamHttpMethod": [ "Get", "Post" ], "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10, "TimeoutValue": 5000 } }, { "DownstreamPathTemplate": "/api/email/{url}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 5002 } ], "UpstreamPathTemplate": "/youjian/{url}", "UpstreamHttpMethod": [ "Get", "Post" ], "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10, "TimeoutValue": 5000 } } ] }
修改Program.cs
public static IWebHost BuildWebHost(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .ConfigureAppConfiguration(conf => { conf.AddJsonFile("configuration.json", optional: false, reloadOnChange: true); }) .Build();
修改Startup.cs
public void ConfigureServices(IServiceCollection services) { //services.AddMvc(); services.AddOcelot(Configuration); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } //app.UseMvc(); app.UseOcelot().Wait();//不要忘了寫Wait }
然后將smsservice1與emailservice1以環境變量的方式啟動(這里用cmd啟動)
set ASPNETCORE_URLS=http://127.0.0.1:5001 dotnet smsservice1.dll set ASPNETCORE_URLS=http://127.0.0.1:5002 dotnet emailservice1.dll
注意:powershell和cmd啟動方式不同
# Unix:
ASPNETCORE_URLS="https://*:5123" dotnet run
# Windows PowerShell:
$env:ASPNETCORE_URLS="https://*:5123" ; dotnet run
# Windows CMD (note: no quotes):
SET ASPNETCORE_URLS=https://*:5123 && dotnet run
接下來啟動ocelotserver1
然后訪問http://127.0.0.1:5000/youjian/Send?msg=aaa的時候就會訪問http://127.0.0.1:5002/api/email/Send?msg=aaa
Ocelot+Consul
上面的配置還是把服務的ip地址寫死了,Ocelot可以和Consul通訊,通過服務名字來配置。
准備Consul
我們首先先啟動Consul
consul.exe agent -dev
我們可以新建一個 smsservice2 WebAPI用來測試,然后添加Consul引用
Install-Package Consul
然后新建SMSController控制器
[Route("api/[Controller]")] public class SMSController : Controller { [Route("Send")] public bool Send(string msg) { Console.WriteLine("發送短信" + msg); return true; } }
添加健康檢查HealthController控制器
[Route("api/[controller]")] public class HealthController : Controller { [HttpGet] public IActionResult Get() { return Ok("ok"); } }
修改Program.cs來設置啟動的IP與端口號
public static IWebHost BuildWebHost(string[] args) { var config = new ConfigurationBuilder() .AddCommandLine(args) .Build(); String ip = config["ip"]; String port = config["port"]; return WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .UseUrls($"http://{ip}:{port}") .Build(); }
然后在Startup.cs進行Consul注冊

public void ConfigureServices(IServiceCollection services) { services.AddMvc(); String ip = Configuration["ip"];//部署到不同服務器的時候不能寫成127.0.0.1或者0.0.0.0,因為這是讓服務消費者調用的地址 Int32 port = Int32.Parse(Configuration["port"]); //向consul注冊服務 ConsulClient client = new ConsulClient(config=>config.Address= new Uri("http://127.0.0.1:8500")); Task<WriteResult> result = client.Agent.ServiceRegister(new AgentServiceRegistration() { ID = "daunxin2" + Guid.NewGuid(),//服務編號,不能重復,用Guid最簡單 Name = "daunxin2",//服務的名字 Address = ip,//我的ip地址(可以被其他應用訪問的地址,本地測試可以用127.0.0.1,機房環境中一定要寫自己的內網ip地址) Port = port,//我的端口 Check = new AgentServiceCheck() { DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//服務停止多久后反注冊 Interval = TimeSpan.FromSeconds(10),//健康檢查時間間隔,或者稱為心跳間隔 HTTP = $"http://{ip}:{port}/api/health",//健康檢查地址, Timeout = TimeSpan.FromSeconds(5) } }); }
分別啟動兩個實例5001和5002
dotnet smsservice2.dll --ip 127.0.0.1 --port 5001 dotnet smsservice2.dll --ip 127.0.0.1 --port 5002
准備Ocelot
創建新的 ocelotserver2 WebAPI項目然后把默認生成的Controller刪除,添加 Ocelot Nuget包引用
Install-Package Ocelot
項目根目錄下創建configuration.json

{ "ReRoutes": [ { "DownstreamPathTemplate": "/api/sms/{url}", "DownstreamScheme": "http", "UpstreamPathTemplate": "/daunxin/{url}", "UpstreamHttpMethod": [ "Get" ], "ServiceName": "duanxin2", "LoadBalancerOptions": "LeastConnection", "UseServiceDiscovery": true } ], "GlobalConfiguration": { "ServiceDiscoveryProvider": { "Host": "localhost", "Port": 8500 } } }
修改Program.cs
public static IWebHost BuildWebHost(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .ConfigureAppConfiguration(conf => { conf.AddJsonFile("configuration.json", optional: false, reloadOnChange: true); }) .Build();
修改Startup.cs
public void ConfigureServices(IServiceCollection services) { //services.AddMvc(); services.AddOcelot(Configuration); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } //app.UseMvc(); app.UseOcelot().Wait();//不要忘了寫Wait }
接下來啟動ocelotserver2,用postman進行調試
dotnet ocelotserver2.dll
訪問http://localhost:5000/daunxin/send?msg=hello即可
表示只要是/daunxin/開頭的(http://localhost:5000/daunxin/send?msg=hello等)都會轉給后端的服務名為"duanxin2"的一台服務器,轉發的路徑是"/{url}"。
"LoadBalancer":"LeastConnection"表示負載均衡算法是“最少連接數”,如果改為RoundRobin就是“輪詢”。
ServiceDiscoveryProvider是Consul服務器的配置。
"UpstreamHttpMethod":["Get"]表示只轉發Get請求,可以添加"Post"等。
(*)也支持Eureka進行服務的注冊、查找(http://ocelot.readthedocs.io/en/latest/features/servicediscovery.html),也支持訪問Service Fabric中的服務(http://ocelot.readthedocs.io/en/latest/features/servicefabric.html)。
限流
官方文檔地址:http://ocelot.readthedocs.io/en/latest/features/ratelimiting.html
要配置到每個路由規則上
參數說明:

"RateLimitOptions": { "ClientWhitelist": [], //不受限制的白名單 "EnableRateLimiting": true, //啟用限流 "Period": "30s", //統計時間段:1s、1m、1h、1d "PeriodTimespan": 10, //一旦碰到一次“超限”,多少秒后重新記數可以重新請求。 "Limit": 5 //指定時間段內最多請求次數 }
我們打開上面的 ocelotserver2 對其配置文件configuration.json進行修改(增加限流配置):
{ "ReRoutes": [ { "DownstreamPathTemplate": "/api/sms/{url}", "DownstreamScheme": "http", "UpstreamPathTemplate": "/daunxin/{url}", "UpstreamHttpMethod": [ "Get" ], "ServiceName": "duanxin2", "LoadBalancerOptions": "RoundRobin", "UseServiceDiscovery": true, "RateLimitOptions": { "ClientWhitelist": [], //不受限制的白名單 "EnableRateLimiting": true, //啟用限流 "Period": "30s", //統計時間段:1s、1m、1h、1d "PeriodTimespan": 10, //一旦碰到一次“超限”,多少秒后重新記數可以重新請求。 "Limit": 5 //指定時間段內最多請求次數 } } ], "GlobalConfiguration": { "ServiceDiscoveryProvider": { "Host": "localhost", "Port": 8500 } } }
然后重啟ocelotserver2服務
dotnet ocelotserver2.dll
訪問http://localhost:5000/daunxin/send?msg=hello即可,我們連續訪問5+次
如果要實現自定義的限流規則,比如不同級別用戶的限速方式不一樣,就要自己寫MiddleWare。
簡單的請求緩存
官方地址:http://ocelot.readthedocs.io/en/latest/features/caching.html
只支持get,Region是用來調用api手動清理緩存用的。只要url不變,就會緩存。可以這樣測試:

public string Get(int id) { return "value" + id + DateTime.Now; }
QOS(熔斷器)
官方文檔:http://ocelot.readthedocs.io/en/latest/features/qualityofservice.html
Ocelot給后端服務器傳數據
修改 ocelotserver2 建立中間件,寫到Startup.cs的Configure:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } var configuration = new OcelotPipelineConfiguration { PreErrorResponderMiddleware = async (ctx, next) => { //String token = ctx.HttpContext.Request.Headers["token"].FirstOrDefault();//這里可以進行接收的客戶端token解析轉發 ctx.HttpContext.Request.Headers.Add("X-Hello", "666"); await next.Invoke(); } }; //app.UseMvc(); //app.UseOcelot().Wait();//不要忘了寫Wait app.UseOcelot(configuration).Wait(); }
修改 smsservice2 的SMSController進行接收header:

[Route("api/[Controller]")] public class SMSController : Controller { [Route("Send")] public bool Send(string msg) { string value = Request.Headers["X-Hello"]; Console.WriteLine($"x-hello={value}"); Console.WriteLine("發送短信" + msg); return true; } }
重啟smsservice2與ocelotserver2。測試結果:
JWT+Ocelot驗證
JWT算法簡介
內部Restful接口可以“我家大門常打開”,但是如果要給app等使用的接口,則需要做權限校驗,不能誰都隨便調用。
最基本的檢查就是“登錄之后才能調用,而且只能調用自己有權限調用的接口”。
Restful接口不是web網站,App中很難直接處理SessionId,而且Cookie有跨域訪問的限制,所以一般不能直接用后端Web框架內置的Session機制。但是可以用類似Session的機制,用戶登錄之后返回一個類似SessionId的東西,服務器端把SessionId和用戶的信息對應關系保存到Redis等地方,客戶端把SessionId保存起來,以后每次請求的時候都帶着這個SessionId。
用類似Session這種機制的壞處:需要集中的Session機制服務器;不可以在nginx、CDN等靜態文件處理服務器上校驗權限;每次都要根據SessionId去Redis服務器獲取用戶信息,效率低;JWT(Json Web Token)是現在流行的一種對Restful接口進行驗證的機制。
JWT的特點:把用戶信息放到一個JWT字符串中,用戶信息部分是明文的,再加上一部分簽名區域,簽名部分是服務器對於“明文部分+秘鑰”加密的,這個加密信息只有服務器端才能解析。用戶端只是存儲、轉發這個JWT字符串。如果客戶端篡改了明文部分,那么服務器端解密時候會報錯。
JWT由三塊組成,可以把用戶名、用戶Id等保存到Payload部分
注意Payload和Header部分都是Base64編碼,可以輕松的Base64解碼回來。因此Payload部分約等於是明文的,因此不能在Payload中保存不能讓別人看到的機密信息。雖然說Payload部分約等於是明文的,但是不用擔心Payload被篡改,因為Signature部分是根據header+payload+secretKey進行加密算出來的,如果Payload被篡改,就可以根據Signature解密時候校驗。
用JWT做權限驗證的好處:無狀態,更有利於分布式系統,不需要集中的Session機制服務器;可以在nginx、CDN等靜態文件處理服務器上校驗權限;獲取用戶信息直接從JWT中就可以讀取,效率高;
.Net中使用JWT算法
新建 jwttest1 控制台項目,添加 jwt 包引用
Install-Package jwt
加密
var payload = new Dictionary<string, object> { { "UserId", 123 }, { "UserName", "admin" } }; var secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";//不要泄露(這是服務器端秘鑰) IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); IJsonSerializer serializer = new JsonNetSerializer(); IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder); var token = encoder.Encode(payload, secret); Console.WriteLine(token);
解密
var token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJVc2VySWQiOjEyMywiVXNlck5hbWUiOiJhZG1pbiJ9.Qjw1epD5P6p4Yy2yju3-fkq28PddznqRj3ESfALQy_U"; var secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk"; try { IJsonSerializer serializer = new JsonNetSerializer(); IDateTimeProvider provider = new UtcDateTimeProvider(); IJwtValidator validator = new JwtValidator(serializer, provider); IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder); var json = decoder.Decode(token, secret, verify: true); Console.WriteLine(json); } catch (FormatException) { Console.WriteLine("Token format invalid"); } catch (TokenExpiredException) { Console.WriteLine("Token has expired"); } catch (SignatureVerificationException) { Console.WriteLine("Token has invalid signature"); }
過期時間
在payload中增加一個名字為exp的值,值為過期時間和1970 / 1 / 1 00:00:00 相差的秒數
使用JWT實現Ocelot的驗證
搭建token頒發服務器
新建WebAPI項目 JWTTokenServer1 並添加JWT引用
Install-Package jwt
新建通用返回類 APIResult.cs

public class APIResult<T> { public int Code { get; set; } public T Data { get; set; } public String Message { get; set; } }
新建Api控制器 AuthController

[Route("api/[Controller]")] public class AuthController : Controller { [HttpGet] [Route(nameof(RequestToken))] public APIResult<string> RequestToken(string userName, string password) { APIResult<string> result = new APIResult<string>(); if (userName == "wyt" && password == "123")//todo:連數據庫 { var payload = new Dictionary<string, object> { { "UserName", userName }, { "UserId", 666 } }; var secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";//不要泄露 IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); IJsonSerializer serializer = new JsonNetSerializer(); IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder); var token = encoder.Encode(payload, secret); result.Code = 0; result.Data = token; } else { result.Code = -1; result.Message = "username or password error"; } return result; } }
以5001端口啟動,用postman進行測試
set ASPNETCORE_URLS=http://127.0.0.1:5001
正確回復:
Ocelot配置
新建WebAPI項目 calcservice3 作為業務服務器
將項目以環境變量方式啟動
set ASPNETCORE_URLS=http://127.0.0.1:5002
新建WebAPI項目 ocelotserver3 作為Ocelot服務器,添加 Ocelot Nuget包引用
Install-Package Ocelot
項目根目錄下創建configuration.json

/* 認證服務器 5001端口 業務服務器 5002端口 Oclot服務器 5000端口 */ { "ReRoutes": [ { "DownstreamPathTemplate": "/{url}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 5001 } ], "UpstreamPathTemplate": "/auth/{url}", "UpstreamHttpMethod": [ "Get", "Post" ], "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10, "TimeoutValue": 5000 } }, { "DownstreamPathTemplate": "/{url}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 5002 } ], "UpstreamPathTemplate": "/calc/{url}", "UpstreamHttpMethod": [ "Get", "Post" ], "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10, "TimeoutValue": 5000 } } ] }
如果認證服務器注冊到Consul,這里也可以按照服務名的方式注冊
修改Program.cs
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureAppConfiguration(conf =>
{
conf.AddJsonFile("configuration.json", optional: false, reloadOnChange: true);
})
.Build();
修改Startup.cs
public void ConfigureServices(IServiceCollection services) { //services.AddMvc(); services.AddOcelot(Configuration); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } //app.UseMvc(); app.UseOcelot().Wait();//不要忘了寫Wait }
然后將ocelotserver3以環境變量的方式啟動(這里用cmd啟動)
set ASPNETCORE_URLS=http://127.0.0.1:5000
然后分別通過ocelotserver3訪問認證服務器和業務服務器
Ocelot中間件驗證token合法性
在中 ocelotserver3 中添加jwt引用
Install-Package jwt
修改Startup.cs中的Configure方法,插入中間件。在后端服務器中就可以從請求圖中讀取"X-UserName"獲取登錄用戶名
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } var configuration = new OcelotPipelineConfiguration { PreErrorResponderMiddleware = async (ctx, next) => { if (!ctx.HttpContext.Request.Path.Value.StartsWith("/auth"))//不以auth開頭的一律校驗 { String token = ctx.HttpContext.Request.Headers["token"].FirstOrDefault(); if (string.IsNullOrWhiteSpace(token)) { ctx.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; using (StreamWriter writer = new StreamWriter(ctx.HttpContext.Response.Body)) { writer.Write("token required"); } return; } var secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk"; try { IJsonSerializer serializer = new JsonNetSerializer(); IDateTimeProvider provider = new UtcDateTimeProvider(); IJwtValidator validator = new JwtValidator(serializer, provider); IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder); var json = decoder.Decode(token, secret, verify: true); Console.WriteLine(json); dynamic payload = JsonConvert.DeserializeObject<dynamic>(json); string userName = payload.UserName; ctx.HttpContext.Request.Headers.Add("X-UserName", userName);//將解析出來的用戶名傳輸給后端服務器。 } catch (TokenExpiredException) { ctx.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; using (StreamWriter writer = new StreamWriter(ctx.HttpContext.Response.Body)) { writer.Write("Token has expired"); } } catch (SignatureVerificationException) { ctx.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; using (StreamWriter writer = new StreamWriter(ctx.HttpContext.Response.Body)) { writer.Write("Token has invalid signature"); } } } await next.Invoke(); } }; //app.UseMvc(); //app.UseOcelot().Wait();//不要忘了寫Wait app.UseOcelot(configuration).Wait();//不要忘了寫Wait }
測試
訪問 http://127.0.0.1:5000/calc/api/values
訪問http://localhost:5000/auth/api/auth/RequestToken?userName=wyt&password=123獲取token
使用token訪問http://127.0.0.1:5000/calc/api/values
篡改token后進行訪問http://127.0.0.1:5000/calc/api/values
Ocelot+Identity Server
實際做項目的時候接口安全沒必要自己寫,可以推薦用identity server簡化開發。
搭建identity server認證服務器
新建一個空的WebAPI項目 ID4.IdServer
Install-Package IdentityServer4
首先編寫一個提供應用列表、賬號列表的Config類

public class Config { /// <summary> /// 返回應用列表 /// </summary> /// <returns></returns> public static IEnumerable<ApiResource> GetApiResources() { List<ApiResource> resources = new List<ApiResource>(); //ApiResource第一個參數是應用的名字,第二個參數是顯示名字 resources.Add(new ApiResource("chatapi", "我的聊天軟件")); resources.Add(new ApiResource("rpandroidapp", "安卓app")); resources.Add(new ApiResource("bdxcx", "百度小程序")); return resources; } /// <summary> /// 返回賬號列表 /// </summary> /// <returns></returns> public static IEnumerable<Client> GetClients() { List<Client> clients = new List<Client>(); clients.Add(new Client { ClientId = "wyt",//用戶名 AllowedGrantTypes = GrantTypes.ClientCredentials, ClientSecrets = { new Secret("123321".Sha256())//秘鑰 }, AllowedScopes = { "chatapi", "rpandroidapp" }//這個賬號支持訪問哪些應用 }); return clients; } }
如果允許在數據庫中配置賬號等信息,那么可以從數據庫中讀取然后返回這些內容。
修改 Startup.cs
public void ConfigureServices(IServiceCollection services) { services.AddIdentityServer() .AddDeveloperSigningCredential() .AddInMemoryApiResources(Config.GetApiResources()) .AddInMemoryClients(Config.GetClients()); //services.AddMvc(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { //if (env.IsDevelopment()) //{ // app.UseDeveloperExceptionPage(); //} //app.UseMvc(); app.UseIdentityServer(); }
然后修改Program.cs在9500端口啟動
public static IWebHost BuildWebHost(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .UseUrls("http://127.0.0.1:9500") .Build();
在postman里發出請求,獲取token
http://localhost:9500/connect/token,發Post請求,表單請求內容(注意不是報文頭):client_id=wyt client_secret=123321 grant_type=client_credentials
把返回的access_token留下來后面用(注意有有效期)。
搭建Ocelot服務器項目
新建WebAPI項目 calcservice3 作為業務服務器
將項目以環境變量方式啟動
set ASPNETCORE_URLS=http://127.0.0.1:5002
新建WebAPI項目 ocelot_id4server ,並安裝Ocelot包
Install-Package Ocelot
編寫配置文件Ocelot.json

{ "ReRoutes": [ { "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 5002 } ], "DownstreamPathTemplate": "/{url}", "UpstreamPathTemplate": "/chat1/{url}", "UpstreamHttpMethod": [ "Get","Post" ], "ReRouteIsCaseSensitive": false, "DownstreamScheme": "http", "AuthenticationOptions": { "AuthenticationProviderKey": "ChatKey", "AllowedScopes": [] } } ] }
把/chat1訪問的都轉給http:// localhost:5002這個后端服務器。
Program.cs中加載Ocelot.json
public static IWebHost BuildWebHost(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .ConfigureAppConfiguration((hostingContext, builder) => { builder.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) .AddJsonFile("Ocelot.json").AddEnvironmentVariables(); }) .Build();
修改Startup.cs
public void ConfigureServices(IServiceCollection services) { //services.AddMvc(); services.AddAuthentication()//對配置文件中使用ChatKey配置了AuthenticationProviderKey=ChatKey的路由規則使用如下的驗證方式 .AddIdentityServerAuthentication("ChatKey", o=> {//IdentityService認證服務的地址 o.Authority = "http://127.0.0.1:9500";//!!!!!!!!!!!!!!!!!(切記,這里不可用localhost) o.ApiName = "chatapi";//要連接的應用的名字 o.RequireHttpsMetadata = false; o.SupportedTokens = SupportedTokens.Both; o.ApiSecret = "123321";//秘鑰 }); services.AddOcelot(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { //if (env.IsDevelopment()) //{ // app.UseDeveloperExceptionPage(); //} //app.UseMvc(); app.UseOcelot().Wait(); ` }
9500端口啟動認證服務器
10000端口啟動Ocelot服務器http://localhost:10000/chat1/api/values/1
在請求頭(不是報文體)里加上:Authorization="Bearer "+上面identityserver返回的accesstoken
Thrift高效通訊
什么是RPC
Restful采用Http進行通訊,優點是開放、標准、簡單、兼容性升級容易;缺點是性能略低。在QPS高(QPS(Query Per Second)每秒查詢率)或者對響應時間要求苛刻的服務上,可以用RPC(Remote Procedure Call)—遠程過程調用,RPC由於采用二進制傳輸、TCP通訊,所以通常性能更好。
.Net Core下的RPC(遠程方法調用)框架有gRPC、Thrift等,可以類比.Net Framework下的.Net Remoting、WCF(TCP Binding)。gRPC、Thrift等都支持主流的編程語言。性能:Thirft(大約10倍)>gRPC>Http。數據匯總自網上,自己沒測,因為性能和業務數據的特點有關,不談業務場景、業務數據的性能測試都是“僅供參考”。並不是gRPC,並不是Http不好,沒有絕對的好與壞。
RPC雖然效率略高,但是耦合性強,如果兼容性處理不好的話,一旦服務器端接口升級,客戶端就要更新,即使是增加一個參數,而rest則比較靈活。
最佳實踐:對內一些性能要求高的場合用RPC,對內其他場合以及對外用Rest。比如web服務器和視頻轉碼服務器之間通訊可以用restful就夠了,轉賬接口用RPC性能會更高
Thrift基本使用
參考資料:https://www.cnblogs.com/focus-lei/p/8889389.html
1、下載thrift http://thrift.apache.org/
把thrift-***.exe解壓到磁盤,改名為thrift.exe(用起來方便一些)
2、編寫一個UserService.thrift文件(IDL(中間定義語言))
namespace csharp ThriftTest1.Contract service UserService{ SaveResult Save(1:User user) User Get(1:i32 id) list<User> GetAll() } enum SaveResult { SUCCESS = 0, FAILED = 1, } struct User { 1: required i64 Id; 2: required string Name; 3: required i32 Age; 4: optional bool IsVIP; 5: optional string Remark; }
service定義的是服務類,enum是枚舉,struct是傳入或者傳出的復雜數據類型(支持對象級聯)。
語法規范http://thrift.apache.org/docs/idl
根據thrift語法生成C#代碼
thrift.exe -gen csharp UserService.thrift
創建一個類庫項目 ThriftTest1.Contract,作為客戶端和服務器之間的共用協議,把上一步生成的代碼放進項目。
項目nuget安裝apache-thrift-netcore:
Install-Package apache-thrift-netcore
然后將生成的文件拷貝到項目中,並重新生成項目
創建服務器端項目 ThriftTest1.Server,建一個控制台項目(放到 web 項目中或者在 Linux中用守護進程運行起來(SuperVisor等,類似Windows下的“Windows服務”)也可以)。
ThriftTest1.Server項目引用ThriftTest1.Contract
編寫實現類UserServiceImpl.cs:

public class UserServiceImpl : UserService.Iface { public User Get(int id) { User u = new User(); u.Id = id; u.Name = "用戶" + id; u.Age = 6; return u; } public List<User> GetAll() { List<User> list = new List<User>(); list.Add(new User { Id = 1, Name = "wyt", Age = 18, Remark = "hello" }); list.Add(new User { Id = 2, Name = "wyt2", Age = 6 }); return list; } public SaveResult Save(User user) { Console.WriteLine($"保存用戶,{user.Id}"); return SaveResult.SUCCESS; } }
修改Program.cs

class Program { static void Main(string[] args) { TServerTransport transport = new TServerSocket(8800); var processor = new ThriftTest1.Contract.UserService.Processor(new UserServiceImpl()); TServer server = new TThreadPoolServer(processor, transport); server.Serve(); Console.WriteLine("Hello World!"); } }
創建客戶端項目 ThriftTest1.Client,建一個控制台項目(放到 web 項目中或者在 Linux中用守護進程運行起來(SuperVisor等,類似Windows下的“Windows服務”)也可以)。
ThriftTest1.Server項目引用ThriftTest1.Contract
修改Program.cs

class Program { static void Main(string[] args) { using (TTransport transport = new TSocket("localhost", 8800)) using (TProtocol protocol = new TBinaryProtocol(transport)) using (var clientUser = new UserService.Client(protocol)) { transport.Open(); User u = clientUser.Get(1); Console.WriteLine($"{u.Id},{u.Name}"); } Console.ReadKey(); } }
分別啟動:
一個服務器中放多個服務
0.9.1之前只支持一個服務器一個服務,這也是建議的做法。之后支持多路服務在thrift中增加一個服務
service CalcService{ i32 Add(1:i32 i1,2:i32 i2) }
服務器:
新增實現類CalcServiceImpl.cs

public class CalcServiceImpl : CalcService.Iface { public int Add(int i1, int i2) { return i1 + i2; } }
修改Program.cs

class Program { static void Main(string[] args) { TServerTransport transport = new TServerSocket(8800); var processorUserService = new ThriftTest1.Contract.UserService.Processor(new UserServiceImpl()); var processorCalcService = new ThriftTest1.Contract.CalcService.Processor(new CalcServiceImpl()); var processorMulti = new TMultiplexedProcessor(); processorMulti.RegisterProcessor("userService", processorUserService); processorMulti.RegisterProcessor("calcService", processorCalcService); TServer server = new TThreadPoolServer(processorMulti, transport); server.Serve(); Console.WriteLine("Hello World!"); } }
客戶端:
修改Program.cs

class Program { static void Main(string[] args) { using (TTransport transport = new TSocket("localhost", 8800)) using (TProtocol protocol = new TBinaryProtocol(transport)) using (var protocolUserService = new TMultiplexedProtocol(protocol,"userService")) using (var clientUser = new UserService.Client(protocolUserService)) using (var protocolCalcService = new TMultiplexedProtocol(protocol,"calcService")) using (var clientCalc = new CalcService.Client(protocolCalcService)) { transport.Open(); User u = clientUser.Get(1); Console.WriteLine($"{u.Id},{u.Name}"); Console.WriteLine(clientCalc.Add(1, 2)); } Console.ReadKey(); } }
分別啟動:
https://www.cnblogs.com/focus-lei/p/8889389.html
(*)新版:thrift.exe -gen netcore UserService.thrift
貌似支持還不完善(http://www.cnblogs.com/zhaiyf/p/8351361.html )還不能用,編譯也有問題,值得期待的是:支持異步。
Java 等其他語言的融入
和使用Restful做服務一樣,Java也可以調用、也可以做Thrift服務,演示一下java調用c#寫的Thrift服務的例子
Java編譯器版本需要>=1.6
Maven(thrift maven版本一定要和生成代碼的thrift的版本一致):

<dependency> <groupId>org.apache.thrift</groupId> <artifactId>libthrift</artifactId> <version>0.11.0</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.5</version> </dependency>
在thrift的IDL文件中加入一行(各個語言的namespace等參數可以共存)
namespace java com.rupeng.thriftTest1.contract 就可以控制生成的java類的報名,最好按照java的命名規范來。
thrift.exe -gen java UserService.thrift
產生java代碼
Java代碼:

import org.apache.thrift.protocol.TBinaryProtocol; import org.apache.thrift.protocol.TProtocol; import org.apache.thrift.transport.TSocket; import org.apache.thrift.transport.TTransport; public class Main { public static void main(String[] args) throws Exception { System.out.println("客戶端啟動...."); TTransport transport = new TSocket("localhost", 8800, 30000); TProtocol protocol = new TBinaryProtocol(transport); UserService.Client client = new UserService.Client(protocol); transport.open(); User result = client.Get(1); System.out.println(result.getAge()+result.getName()+result.getRemark()); } }
也可以用Java寫服務器,C#調用。當然別的語言也可以。
接口設計原則“API design is like sex: Make one mistake and support it for the rest of your life”
Thrift+Consul 服務發現
注冊和發現和Rest方式沒有什么區別。
consul支持tcp健康監測:https://www.consul.io/docs/agent/checks.html
因為 Thrift 一般不對外,所以一般不涉及和 API 網關結合的問題
不是所有項目都適合微服務架構,互聯網項目及結構復雜的企業信息系統才可以考慮微服務架構。
設計微服務架構,模塊拆分的原則:可以獨立運行,盡量服務間不要依賴,即使依賴層級也不要太深,不要想着還要 join。按業務划分、按模塊划分。
擴展知識
1、 分布式跟蹤、日志服務、監控等對微服務來說非常重要
2、 gRPC 另外一個 RPC 框架,gRPC 的.Net Core 支持異步。
3、 https://github.com/neuecc/MagicOnion 可以參考下這位日本 mvp 寫的 grpc 封裝,不需要定義接口文件。
4、 nanofabric https://github.com/geffzhang/NanoFabric 簡單分析
5、 Surging https://github.com/dotnetcore/surging
6、 service fabric https://azure.microsoft.com/zh-cn/documentation/learning-paths/service-fabric/
7、 Spring Cloud 入門視頻:http://www.rupeng.com/Courses/Chapter/755
8、 steeltoe http://steeltoe.io/
9、 限流算法 https://mp.weixin.qq.com/s/bck0Q2lDj_J9pLhFEhqm9w
10、https://github.com/PolicyServer/PolicyServer.Local 認證 + 授權 是兩個服務, identityserver 解決了認證 ,PolicyServer 解決授權
11、CSharpKit 微服務工具包 http://www.csharpkit.com/
12、如鵬網.Net 提高班 http://www.rupeng.com