.在VS中調試的時候有不少修改Web應用運行端口的方法。可是在開發、調試微服務應用的時候可能須要同時在不一樣端口上開啓多個服務器的實例,所以下面主要看看如何經過命令行指定Web應用的端口(默認5000)html
能夠經過設置臨時環境變量ASPNETCORE URLS來改變默認的端口、域名,也就是執行 dotnet xxx.dll以前執行set ASPNETCORE_URLS=http://127.0.0.1:5001來設置環境變量。java
若是須要在程序中讀取端口、域名(後續服務治理會用到) ,用ASPNETCORE URLS環境變量就不太方便,能夠自定義配置文件, 本身讀取設置。android
修改Program.csnginx
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(); }
而後啓動的時候:git
dotnet WebApplication5.dll--ip 127.0.0.1-port 8889
.Net Core由於跨平臺,因此能夠不依賴於IIS運行了。能夠用.Net Core內置的kestrel服務器運行網站,固然真正面對終端用戶訪問的時候通常經過Nginx等作反向代理。github
Consul是註冊中心,服務提供者、服務消費者等都要註冊到Consul中,這樣就能夠實, ,現服務提供者、服務消費者的隔離。web
除了Consul以外,還有Eureka,Zookeeper等相似軟件。算法
consul下載地址https://www.consul.io/shell
運行數據庫
consul.exe agent -dev
這是開發環境測試,生產環境要建集羣,要至少一臺Server,多臺Agent consul
監控頁面http://127.0.0.1:8500/consult
主要作三件事:提供服務到ip地址的註冊;提供服務到ip地址列表的查詢;對提供服務方的健康檢查(HealthCheck) ;
新建Asp.Net Core WebAPI項目WebApplication4,安裝Consul nuget包
Install-Package Consul
先使用使用默認生成的ValuesController作測試
再提供一個HealthController.cs
[Route("api/Health")] public class HealthController : Controller { [HttpGet] public IActionResult Get() { return Ok("ok"); } }
服務器從命令行中讀取ip和端口
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(); }
添加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的替代方案;加載內容評論時若是出錯,則以緩存中加載或者顯示"評論暫時不可用" 。
.Net Core中有一個被.Net基金會承認的庫Polly,能夠用來簡化熔斷降級的處理。主要功能:重試(Retry) ;斷路器(Circuit-breaker) ;超時檢測(Timeout) ;緩存(Cache) ;,失敗處理(FallBack) ;
官網: https://github. com/App-vNext/Polly
介紹文章: http://www.javashuo.com/article/p-opmxxgua-ee.html
Install-Package Polly -Version 5.9.0
6.0.1對緩存還不支持,所以如今暫時先用5.9.0版本.
使用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}"); }
. 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。
出現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就會把policy一、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); }
若是直接使用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
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 ,達到熔斷降級
[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); } } }
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
/// <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); } } }
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); } }
測試結果:
正常:
一級熔斷
二級熔斷
新建WebAPI項目aspnetcorehystrix,
並添加AspectCore.Core、Polly包引用
Install-Package AspectCore.Core
Install-Package Polly
Install-Package Microsoft.Extensions.Caching.Memory
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(); }
現有微服務的幾點不足:
1)對於在微服務體系中、和Consul通信的微服務來說,使用服務名便可訪問。可是對於手機、web端等外部訪問者仍然須要和N多服務器交互,須要記憶他們的服務器地址、端口號等。一旦內部發生修改,很麻煩,並且有時候內部服務器是不但願外界直接訪問的。
2)各個業務系統的人沒法自由的維護本身負責的服務器;
3)現有的微服務都是「我家大門常打開」,沒有作權限校驗。若是把權限校驗代碼寫到每一個微服務上,那麼開發工做量太大。
4)很難作限流、收費等。
ocelot 中文文檔:http://www.javashuo.com/article/p-osgdmesl-nh.html
資料: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就是一個提供了請求路由、安全驗證等功能的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
上面的配置仍是把服務的ip地址寫死了,Ocelot能夠和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
建立新的 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; }
官方文檔:http://ocelot.readthedocs.io/en/latest/features/qualityofservice.html
修改 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。測試結果:
內部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中就能夠讀取,效率高;
新建 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 相差的秒數
新建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
正確回覆:
新建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訪問認證服務器和業務服務器
在中 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
實際作項目的時候接口安全不必本身寫,能夠推薦用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留下來後面用(注意有有效期)。
新建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
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性能會更高
參考資料:http://www.javashuo.com/article/p-nvuouklh-bd.html
一、下載thrift http://thrift.apache.org/
把thrift-***.exe解壓到磁盤,更名爲thrift.exe(用起來方便一些)
二、編寫一個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 )還不能用,編譯也有問題,值得期待的是:支持異步。
和使用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」
註冊和發現和Rest方式沒有什麼區別。
consul支持tcp健康監測:https://www.consul.io/docs/agent/checks.html
由於 Thrift 通常不對外,因此通常不涉及和 API 網關結合的問題
不是全部項目都適合微服務架構,互聯網項目及結構複雜的企業信息系統才能夠考慮微服務架構。
設計微服務架構,模塊拆分的原則:能夠獨立運行,儘可能服務間不要依賴,即便依賴層級也不要太深,不要想着還要 join。按業務劃分、按模塊劃分。
一、 分佈式跟蹤、日誌服務、監控等對微服務來講很是重要
二、 gRPC 另一個 RPC 框架,gRPC 的.Net Core 支持異步。
三、 https://github.com/neuecc/MagicOnion 能夠參考下這位日本 mvp 寫的 grpc 封裝,不須要定義接口文件。
四、 nanofabric https://github.com/geffzhang/NanoFabric 簡單分析
五、 Surging https://github.com/dotnetcore/surging
六、 service fabric https://azure.microsoft.com/zh-cn/documentation/learning-paths/service-fabric/
七、 Spring Cloud 入門視頻:http://www.rupeng.com/Courses/Chapter/755
八、 steeltoe http://steeltoe.io/
九、 限流算法 https://mp.weixin.qq.com/s/bck0Q2lDj_J9pLhFEhqm9w
十、https://github.com/PolicyServer/PolicyServer.Local 認證 + 受權 是兩個服務, identityserver 解決了認證 ,PolicyServer 解決受權
十一、CSharpKit 微服務工具包 http://www.csharpkit.com/
十二、如鵬網.Net 提升班 http://www.rupeng.com