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等做反向代理
WebAPI基礎
編寫幾個有意義的接口,多搞幾個不同的服務,包含 get、post,方便后面測試、演示。
調試 WebAPI 項目的時候把項目屬性中的【啟動瀏覽器】勾掉,這樣就不會啟動瀏覽器了,講解使用 PostMan 調試 Http 接口。
WebAPI 就是 Restful 風格,請求響應都最好是 json 格式,雖然請求也可以是表單格式,但是最好都用 json 格式請求( contenttype=application/json )的方法體:
{phoneNum:"110",msg:"aaaaaaaaaaaaa"},因此這里只講 json 格式請求的方法。
和.Net Framework 中的 WebAPI 不一樣,如果[HttpPost]、[HttpGet]等標記不加參數,則表示匹配“沒有 Action”,比如 http://localhost:5000/api/SMS/。如果指定[HttpPost("Send_MI")],則匹配 Action 的名字,比如 http://localhost:5000/api/SMS/ Send_MI。如果方法名字和 Action 名字一樣,建議用 nameof。參數:
1) 正確:public void Send_MI(dynamic model)
2) 正確:public void Send_HW(SendSMSRequest model)
3) 錯誤:public void Send_LX(string phoneNum,string msg)
新建解決方案 MS2V
信息服務WebAPI項目 MsgService 新增兩個控制器
EmailController.cs:

[Route("api/[Controller]")] public class EmailController : ControllerBase { [HttpPost(nameof(Send_QQ))] public void Send_QQ(SendEmailRequest model) { Console.WriteLine($"通過QQ郵件接口向{model.Email}發送郵件,標題{model.Title},內容:{model.Body}"); } [HttpPost(nameof(Send_163))] public void Send_163(SendEmailRequest model) { Console.WriteLine($"通過網易郵件接口向{model.Email}發送郵件,標題{model.Title},內容:{model.Body}"); } [HttpPost(nameof(Send_Sohu))] public void Send_Sohu(SendEmailRequest model) { Console.WriteLine($"通過Sohu郵件接口向{model.Email}發送郵件,標題{model.Title},內容:{model.Body}"); } } public class SendEmailRequest { public string Email { get; set; } public string Title { get; set; } public string Body { get; set; } }
SMSController.cs:

[Route("api/[Controller]")] public class SMSController : ControllerBase { //發請求,報文體為{phoneNum:"110",msg:"aaaaaaaaaaaaa"}, [HttpPost(nameof(Send_MI))] public void Send_MI(dynamic model) { Console.WriteLine($"通過小米短信接口向{model.phoneNum}發送短信{model.msg}"); } [HttpPost(nameof(Send_LX))] public void Send_LX(SendSMSRequest model) { Console.WriteLine($"通過聯想短信接口向{model.PhoneNum}發送短信{model.Msg}"); } [HttpPost(nameof(Send_HW))] public void Send_HW(SendSMSRequest model) { Console.WriteLine($"通過華為短信接口向{model.PhoneNum}發送短信{model.Msg}"); } } public class SendSMSRequest { public string PhoneNum { get; set; } public string Msg { get; set; } }
新增類文件Product.cs:

public class Product { public long Id { get; set; } public string Name { get; set; } public double Price { get; set; } public string Description { get; set; } }
新增控制器ProductController.cs:

[Route("api/[Controller]")] public class ProductController : ControllerBase { //這顯然是為了demo,這樣放到內存中不能“集群” private static List<Product> products = new List<Product>(); static ProductController() { products.Add(new Product { Id = 1, Name = "T430筆記本", Price = 8888, Description = "CPU i7標壓版,1T硬盤" }); products.Add(new Product { Id = 2, Name = "華為Mate10", Price = 3888, Description = "大猩猩屏幕,多點觸摸" }); products.Add(new Product { Id = 3, Name = "天梭手表", Price = 9888, Description = "瑞士經典款,可好了" }); } [HttpGet] public IEnumerable<Product> Get() { //string name = this.User.Identity.Name;//讀取的就是"Name"這個特殊的Claims的值 //string userId = this.User.FindFirst("UserId").Value; //string realName = this.User.FindFirst("RealName").Value; //string email = this.User.FindFirst("Email").Value; //Console.WriteLine($"name={name},userId={userId},realName={realName},email={email}"); //System.Console.WriteLine("Get請求過來了" + DateTime.Now); return products; } [HttpGet("{id}")] public Product Get(int id) { var product = products.SingleOrDefault(p => p.Id == id); if (product == null) { Response.StatusCode = 404; } return product; } [HttpPost] public void Add(Product model) { if (products.Any(p => p.Id == model.Id)) { Response.StatusCode = (int)HttpStatusCode.Conflict;//通過狀態碼而非響應體報錯,是restful風格 return; } products.Add(model); } [HttpDelete("{id}")] public void Delete(int id) { var product = products.SingleOrDefault(p => p.Id == id); if (product != null) { products.Remove(product); } } }
然后再把兩個項目配置通過命令行讀取ip、port自定義監聽的ip、端口。
修改他們的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(); }
Consul服務治理發現
Consul([ˈkɒnsl],康搜)是注冊中心,服務提供者、服務消費者等都要注冊到Consul中,這樣就可以實現服務提供者、服務消費者的隔離。
除了Consul之外,還有Eureka、Zookeeper等類似軟件。
用DNS舉例來理解Consul。consul是存儲服務名稱與IP和端口對應關系的服務器。
consul服務器安裝
consul下載地址https://www.consul.io/
運行
consul.exe agent -dev
這是開發環境測試,生產環境要建集群,要至少一台Server,多台Agent。
開發環境中consul重啟后數據就會丟失。
consul的監控頁面http://127.0.0.1:8500/consult主要做三件事:提供服務到ip地址的注冊;提供服務到ip地址列表的查詢;對提供服務方的健康檢查(HealthCheck);
.Net Core連接Consul
打開之前新建的WebAPI項目 MsgService 與 ProductService ,安裝Consul nuget包
Install-Package Consul
先使用使用默認生成的ValuesController做測試
在以上兩個項目中新建一個控制器用來做Consul健康檢查。HealthController.cs

[Route("api/[Controller]")] public class HealthController : Controller { [HttpGet] public IActionResult Get() { return Ok("ok"); } }
服務注冊Consul及注銷
Startup.cs:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime applicationLifetime) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseMvc(); string ip = Configuration["ip"]; int port = Convert.ToInt32(Configuration["port"]); string serviceName = "MsgService"; string serviceId = serviceName + Guid.NewGuid(); using (var client = new ConsulClient(ConsulConfig)) {//注冊服務到Consul client.Agent.ServiceRegister(new AgentServiceRegistration() { ID = serviceId,//服務編號,不能重復,用Guid最簡單 Name = serviceName,//服務的名字 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) } }).Wait();//Consult客戶端的所有方法幾乎都是異步方法,但是都沒按照規范加上Async后綴,所以容易誤導。記得調用后要Wait()或者await } //程序正常退出的時候從Consul注銷服務//要通過方法參數注入IApplicationLifetime applicationLifetime.ApplicationStopped.Register(() => { using (var client = new ConsulClient(ConsulConfig)) { client.Agent.ServiceDeregister(serviceId).Wait(); } }); } private void ConsulConfig(ConsulClientConfiguration c) { c.Address = new Uri("http://127.0.0.1:8500"); c.Datacenter = "dc1"; }
也支持tcp探測,很顯然也可以把普通TCP服務注冊到Consul,因為Consul中注冊的只是服務名字、ip地址、端口號,具體服務怎么實現、怎么調用Consul不管。
注意不同實例一定要用不同的Id,即使是相同服務的不同實例也要用不同的Id,上面的代碼用Guid做Id,確保不重復。相同的服務用相同的Name。Address、Port是供服務消費者訪問的服務器地址(或者IP地址)及端口號。Check則是做服務健康檢查的(解釋一下)。
在注冊服務的時候還可以通過AgentServiceRegistration的Tags屬性設置額外的標簽。
查看節點狀態
consul operator raft list-peers
通過命令行啟動兩個實例:
dotnet MsgService.dll --ip 127.0.0.1 --port 5001 dotnet ProductService.dll --ip 127.0.0.1 --port 5002
打開Consul的Web頁面服務已經注冊進來了,注意剛開始啟動的時候,有短暫的Failing是正常的。服務正常結束(Ctrl+C)會觸發ApplicationStopped,正常注銷。即使非正常結束也沒關系,Consul健康檢查過一會發現服務器死掉后也會主動注銷。
如果服務器剛剛崩潰,但是還買來得及注銷,消費的使用者可能就會拿到已經崩潰的實例,這個問題通過后面講的重試等策略解決。
服務只會注冊ip、端口,consul只會保存服務名、ip、端口這些信息,至於服務提供什么接口、方法、參數,consul不管,需要消費者知道服務的這些細節。
多個服務應用就注冊多個就可以。Consul中可能注冊多個服務,一個服務有多個服務器實例。上面講的就是服務治理:服務的注冊、注銷、健康檢查。
編寫服務消費者
這里用控制台測試,真實項目中服務消費者同時也可能是另外一個Web應用(比如Web服務器調用短信服務器發短信)。
下面就是打印出所有Consul登記在冊的服務實例.
新建控制台項目 服務消費者1 安裝Consul nuget包
Install-Package Consul
using (var consulClient = new ConsulClient(c => c.Address = new Uri("http://127.0.0.1:8500"))) { var services = consulClient.Agent.Services().Result.Response; foreach (var service in services.Values) { Console.WriteLine($"id={service.ID},name={service.Service},ip={service.Address},port={service.Port}"); } }
下面的代碼使用當前 TickCount 進行取模的方式達到隨機獲取一台服務器實例的效果,這叫做“客戶端負載均衡”:
using (var consulClient = new ConsulClient(c => c.Address = new Uri("http://127.0.0.1:8500"))) { var services = consulClient.Agent.Services().Result.Response.Values.Where(s => s.Service.Equals("MsgService", StringComparison.OrdinalIgnoreCase)); if (!services.Any()) { Console.WriteLine("找不到服務的實例"); } else { var service = services.ElementAt(Environment.TickCount % services.Count()); Console.WriteLine($"{service.Address}:{service.Port}"); } }
當然在一個毫秒之類會所有請求都壓給一台服務器,基本就夠用了。也可以自己寫隨機、輪詢等客戶端負載均衡算法,也可以自己實現按不同權重分配(注冊時候 Tags 帶上配置、權重等信息)等算法。
首先編寫一個 RestTemplateCore 類庫項目(模仿 Spring Cloud 中的 RestTemplate)
GitHub 地址:https://github.com/yangzhongke/RuPeng.RestTemplateCore
Nuget 地址:https://www.nuget.org/packages/RestTemplateCore
nuget 安裝:Consul、Newtonsoft.Json
Install-Package Consul
Install-Package Newtonsoft.Json
新建Rest響應結果類RestResponse.cs

/// <summary> /// Rest響應結果 /// </summary> public class RestResponse { /// <summary> /// 響應狀態碼 /// </summary> public HttpStatusCode StatusCode { get; set; } /// <summary> /// 響應的報文頭 /// </summary> public HttpResponseHeaders Headers { get; set; } }
新建帶響應報文的Rest響應結果類RestResponseWithBody.cs

/// <summary> /// 帶響應報文的Rest響應結果,而且json報文會被自動反序列化 /// </summary> /// <typeparam name="T"></typeparam> public class RestResponseWithBody<T>: RestResponse { /// <summary> /// 響應報文體json反序列化的內容 /// </summary> public T Body { get; set; } }
新建解析類RestTemplate.cs

/// <summary> /// 會自動到Consul中解析服務的Rest客戶端,能把"http://ProductService/api/Product/"這樣的虛擬地址 /// 按照客戶端負載均衡算法解析為http://192.168.1.10:8080/api/Product/這樣的真實地址 /// </summary> public class RestTemplate { public String ConsulServerUrl { get; set; } = "http://127.0.0.1:8500"; private HttpClient httpClient; public RestTemplate(HttpClient httpClient) { this.httpClient = httpClient; } /// <summary> /// 獲取服務的第一個實現地址 /// </summary> /// <param name="consulClient"></param> /// <param name="serviceName"></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.Values .Where(s => s.Service.Equals(serviceName, StringComparison.OrdinalIgnoreCase)); if (!services.Any()) { throw new ArgumentException($"找不到服務{serviceName }的任何實例"); } else { //根據當前時鍾毫秒數對可用服務個數取模,取出一台機器使用 var service = services.ElementAt(Environment.TickCount % services.Count()); return $"{service.Address}:{service.Port}"; } } } //把http://apiservice1/api/values轉換為http://192.168.1.1:5000/api/values private async Task<String> ResolveUrlAsync(String url) { Uri uri = new Uri(url); String serviceName = uri.Host;//apiservice1 String realRootUrl = await ResolveRootUrlAsync(serviceName);//查詢出來apiservice1對應的服務器地址192.168.1.1:5000 //uri.Scheme=http,realRootUrl =192.168.1.1:5000,PathAndQuery=/api/values 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<RestResponseWithBody<T>> GetForEntityAsync<T>(String url, HttpRequestHeaders requestHeaders = null) { using (HttpRequestMessage requestMsg = new HttpRequestMessage()) { if (requestHeaders != null) { foreach (var header in requestHeaders) { requestMsg.Headers.Add(header.Key, header.Value); } } requestMsg.Method = System.Net.Http.HttpMethod.Get; //http://apiservice1/api/values轉換為http://192.168.1.1:5000/api/values requestMsg.RequestUri = new Uri(await ResolveUrlAsync(url)); RestResponseWithBody<T> respEntity = await SendForEntityAsync<T>(requestMsg); return respEntity; } } /// <summary> /// 發出Post請求 /// </summary> /// <typeparam name="T">響應報文反序列類型</typeparam> /// <param name="url">請求路徑</param> /// <param name="body">請求數據,將會被json序列化后放到請求報文體中</param> /// <param name="requestHeaders">請求額外的報文頭信息</param> /// <returns></returns> public async Task<RestResponseWithBody<T>> PostForEntityAsync<T>(String url, object body = null, HttpRequestHeaders requestHeaders = null) { using (HttpRequestMessage requestMsg = new HttpRequestMessage()) { if (requestHeaders != null) { foreach (var header in requestHeaders) { requestMsg.Headers.Add(header.Key, header.Value); } } requestMsg.Method = System.Net.Http.HttpMethod.Post; //http://apiservice1/api/values轉換為http://192.168.1.1:5000/api/values requestMsg.RequestUri = new Uri(await ResolveUrlAsync(url)); requestMsg.Content = new StringContent(JsonConvert.SerializeObject(body)); requestMsg.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); RestResponseWithBody<T> respEntity = await SendForEntityAsync<T>(requestMsg); return respEntity; } } /// <summary> /// 發出Post請求 /// </summary> /// <param name="url">請求路徑</param> /// <param name="requestHeaders">請求額外的報文頭信息</param> /// <returns></returns> public async Task<RestResponse> PostAsync(String url, object body = null, HttpRequestHeaders requestHeaders = null) { using (HttpRequestMessage requestMsg = new HttpRequestMessage()) { if (requestHeaders != null) { foreach (var header in requestHeaders) { requestMsg.Headers.Add(header.Key, header.Value); } } requestMsg.Method = System.Net.Http.HttpMethod.Post; //http://apiservice1/api/values轉換為http://192.168.1.1:5000/api/values requestMsg.RequestUri = new Uri(await ResolveUrlAsync(url)); requestMsg.Content = new StringContent(JsonConvert.SerializeObject(body)); requestMsg.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); var resp = await SendAsync(requestMsg); return resp; } } /// <summary> /// 發出Put請求 /// </summary> /// <typeparam name="T">響應報文反序列類型</typeparam> /// <param name="url">請求路徑</param> /// <param name="body">請求數據,將會被json序列化后放到請求報文體中</param> /// <param name="requestHeaders">請求額外的報文頭信息</param> /// <returns></returns> public async Task<RestResponseWithBody<T>> PutForEntityAsync<T>(String url, object body = null, HttpRequestHeaders requestHeaders = null) { using (HttpRequestMessage requestMsg = new HttpRequestMessage()) { if (requestHeaders != null) { foreach (var header in requestHeaders) { requestMsg.Headers.Add(header.Key, header.Value); } } requestMsg.Method = System.Net.Http.HttpMethod.Put; //http://apiservice1/api/values轉換為http://192.168.1.1:5000/api/values requestMsg.RequestUri = new Uri(await ResolveUrlAsync(url)); requestMsg.Content = new StringContent(JsonConvert.SerializeObject(body)); requestMsg.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); RestResponseWithBody<T> respEntity = await SendForEntityAsync<T>(requestMsg); return respEntity; } } /// <summary> /// 發出Put請求 /// </summary> /// <param name="url">請求路徑</param> /// <param name="body">請求數據,將會被json序列化后放到請求報文體中</param> /// <param name="requestHeaders">請求額外的報文頭信息</param> /// <returns></returns> public async Task<RestResponse> PutAsync(String url, object body = null, HttpRequestHeaders requestHeaders = null) { using (HttpRequestMessage requestMsg = new HttpRequestMessage()) { if (requestHeaders != null) { foreach (var header in requestHeaders) { requestMsg.Headers.Add(header.Key, header.Value); } } requestMsg.Method = System.Net.Http.HttpMethod.Put; //http://apiservice1/api/values轉換為http://192.168.1.1:5000/api/values requestMsg.RequestUri = new Uri(await ResolveUrlAsync(url)); requestMsg.Content = new StringContent(JsonConvert.SerializeObject(body)); requestMsg.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); var resp = await SendAsync(requestMsg); return resp; } } /// <summary> /// 發出Delete請求 /// </summary> /// <typeparam name="T">響應報文反序列類型</typeparam> /// <param name="url">請求路徑</param> /// <param name="requestHeaders">請求額外的報文頭信息</param> /// <returns></returns> public async Task<RestResponseWithBody<T>> DeleteForEntityAsync<T>(String url, HttpRequestHeaders requestHeaders = null) { using (HttpRequestMessage requestMsg = new HttpRequestMessage()) { if (requestHeaders != null) { foreach (var header in requestHeaders) { requestMsg.Headers.Add(header.Key, header.Value); } } requestMsg.Method = System.Net.Http.HttpMethod.Delete; //http://apiservice1/api/values轉換為http://192.168.1.1:5000/api/values requestMsg.RequestUri = new Uri(await ResolveUrlAsync(url)); RestResponseWithBody<T> respEntity = await SendForEntityAsync<T>(requestMsg); return respEntity; } } /// <summary> /// 發出Delete請求 /// </summary> /// <param name="url">請求路徑</param> /// <param name="requestHeaders">請求額外的報文頭信息</param> /// <returns></returns> public async Task<RestResponse> DeleteAsync(String url, HttpRequestHeaders requestHeaders = null) { using (HttpRequestMessage requestMsg = new HttpRequestMessage()) { if (requestHeaders != null) { foreach (var header in requestHeaders) { requestMsg.Headers.Add(header.Key, header.Value); } } requestMsg.Method = System.Net.Http.HttpMethod.Delete; //http://apiservice1/api/values轉換為http://192.168.1.1:5000/api/values requestMsg.RequestUri = new Uri(await ResolveUrlAsync(url)); var resp = await SendAsync(requestMsg); return resp; } } /// <summary> /// 發出Http請求 /// </summary> /// <typeparam name="T">響應報文反序列類型</typeparam> /// <param name="requestMsg">請求數據</param> /// <returns></returns> public async Task<RestResponseWithBody<T>> SendForEntityAsync<T>(HttpRequestMessage requestMsg) { var result = await httpClient.SendAsync(requestMsg); RestResponseWithBody<T> respEntity = new RestResponseWithBody<T>(); respEntity.StatusCode = result.StatusCode; respEntity.Headers = respEntity.Headers; String bodyStr = await result.Content.ReadAsStringAsync(); if (!string.IsNullOrWhiteSpace(bodyStr)) { respEntity.Body = JsonConvert.DeserializeObject<T>(bodyStr); } return respEntity; } /// <summary> /// 發出Http請求 /// </summary> /// <param name="requestMsg">請求數據</param> /// <returns></returns> public async Task<RestResponse> SendAsync(HttpRequestMessage requestMsg) { var result = await httpClient.SendAsync(requestMsg); RestResponse response = new RestResponse(); response.StatusCode = result.StatusCode; response.Headers = result.Headers; return response; } }
編寫控制台項目 consultest1 做測試

class Program { static void Main(string[] args) { using (HttpClient httpClient = new HttpClient()) { RestTemplate rest = new RestTemplate(httpClient); Console.WriteLine("---查詢數據---------"); var ret1 =rest.GetForEntityAsync<Product[]>("http://ProductService/api/Product/").Result; Console.WriteLine(ret1.StatusCode); if (ret1.StatusCode == System.Net.HttpStatusCode.OK) { foreach (var p in ret1.Body) { Console.WriteLine($"id={p.Id},name={p.Name}"); } } Console.WriteLine("---新增數據---------"); Product newP = new Product(); newP.Id = 888; newP.Name = "辛增"; newP.Price = 88.8; var ret = rest.PostAsync("http://ProductService/api/Product/", newP).Result; Console.WriteLine(ret.StatusCode); } Console.ReadKey(); } } class Product { public long Id { get; set; } public string Name { get; set; } public double Price { get; set; } public string Description { get; set; } }
熔斷、降級
什么是熔斷降級
熔斷就是“保險絲”。當出現某些狀況時,切斷服務,從而防止應用程序不斷地嘗試執行可能會失敗的操作給系統造成“雪崩”,或者大量的超時等待導致系統卡死。
降級的目的是當某個服務提供者發生故障的時候,向調用方返回一個錯誤響應或者替代響應。舉例子:調用聯通接口服務器發送短信失敗之后,改用移動短信服務器發送,如果移動短信服務器也失敗,則改用電信短信服務器,如果還失敗,則返回“失敗”響應;在從推薦商品服務器加載數據的時候,如果失敗,則改用從緩存中加載,如果緩存中也加載失敗,則返回一些本地替代數據。
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 6.0.1
Polly 的策略由“故障”和“動作”兩部分組成,“故障”包括異常、超時、返回值錯誤等情況,“動作”包括 FallBack(降級)、重試(Retry)、熔斷(Circuit-breaker)等。
策略用來執行可能會有有故障的業務代碼,當業務代碼出現“故障”中情況的時候就執行“動作”。
由於實際業務代碼中故障情況很難重現出來,所以 Polly 這一些都是用一些無意義的代碼模擬出來。
Polly 也支持請求緩存“數據不變化則不重復自行代碼”,但是和新版本兼容不好,而且功能局限性很大,因此這里不講。
由於調試器存在,看不清楚 Polly 的執行過程,因此本節都用【開始執行(不調試)】
Polly 簡單使用
使用Policy的靜態方法創建ISyncPolicy實現類對象,創建方法既有同步方法也有異步方法,根據自己的需要選擇。下面先演示同步的,異步的用法類似。
新建控制台項目 pollytest1
舉例:當發生ArgumentException異常的時候,執行Fallback代碼。

Policy policy = Policy.Handle<ArgumentException>()//故障 .Fallback(() =>//動作 { Console.WriteLine("出錯了"); }); policy.Execute(() =>//在策略中執行業務代碼 { //這里是可能會產生問題的業務系統代碼 Console.WriteLine("開始執行"); throw new ArgumentException(); Console.WriteLine("執行結束"); });
如果沒有被Handle處理的異常,則會導致未處理異常被拋出。還可以用Fallback的其他重載獲取異常信息:
Policy policy = Policy.Handle<ArgumentException>()//故障 .Fallback(() =>//動作 { Console.WriteLine("出錯了"); },ex=> { Console.WriteLine("詳細異常對象" + ex); }); policy.Execute(() =>//在策略中執行業務代碼 { //這里是可能會產生問題的業務系統代碼 Console.WriteLine("開始執行"); throw new ArgumentException(); Console.WriteLine("執行結束"); });
如果Execute中的代碼是帶返回值的,那么只要使用帶泛型的Policy<T>類即可:
Policy<string> policy = Policy<string>.Handle<Exception>() //故障 .Fallback(() =>//動作 { Console.WriteLine("執行出錯"); return "降級的值"; }); string value = policy.Execute(() => { Console.WriteLine("開始任務"); throw new Exception("Hello world!"); Console.WriteLine("完成任務"); return "正常的值"; }); Console.WriteLine("返回值:" + value);
FallBack的重載方法也非常多,有的異常可以直接提供降級后的值。
(*)異常中還可以通過lambda表達式對異常判斷“滿足***條件的異常我才處理”,簡單看看試試重載即可。還可以多個Or處理各種不同的異常。
(*)還可以用HandleResult等判斷返回值進行故障判斷等,我感覺沒太大必要。
重試處理
Policy policy = Policy .Handle<Exception>().RetryForever(); policy.Execute(() => { Console.WriteLine("開始任務"); if (DateTime.Now.Second % 10 != 0) { throw new Exception("出錯"); } Console.WriteLine("完成任務"); });
RetryForever()是一直重試直到成功
Retry()是重試最多一次;
Retry(n) 是重試最多n次;
WaitAndRetry()可以實現“如果出錯等待100ms再試還不行再等150ms秒。。。。”,重載方法很多,不再一一介紹。
還有WaitAndRetryForever。
短路保護 Circuit Breaker
出現N次連續錯誤,則把“熔斷器”(保險絲)熔斷,等待一段時間,等待這段時間內如果再Execute 則直接拋出BrokenCircuitException異常,根本不會再去嘗試調用業務代碼。等待時間過去之后,再執行Execute的時候如果又錯了(一次就夠了),那么繼續熔斷一段時間,否則就恢復正常。這樣就避免一個服務已經不可用了,還是使勁的請求給系統造成更大壓力。
Policy 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); } Thread.Sleep(500); }
其計數的范圍是policy對象,所以如果想整個服務器全局對於一段代碼做短路保護,則需要共用一個policy對象。
策略封裝
可以把多個ISyncPolicy合並到一起執行:
policy3= policy1.Wrap(policy2);
執行policy3就會把policy1、policy2封裝到一起執行
policy9=Policy.Wrap(policy1, policy2, policy3, policy4, policy5);
把更多一起封裝。
超時處理
這些處理不能簡單的鏈式調用,要用到Wrap。例如下面實現“出現異常則重試三次,如果還出錯就FallBack”這樣是不行的
注意Wrap是有包裹順序的,內層的故障如果沒有被處理則會拋出到外層。
下面代碼實現了“出現異常則重試三次,如果還出錯就FallBack”
Policy policyRetry = Policy.Handle<Exception>() .Retry(3); Policy policyFallback = Policy.Handle<Exception>() .Fallback(() => { Console.WriteLine("降級"); }); //Wrap:包裹。policyRetry在里面,policyFallback裹在外面。 //如果里面出現了故障,則把故障拋出來給外面 Policy policy = policyFallback.Wrap(policyRetry); policy.Execute(() => { Console.WriteLine("開始任務"); if (DateTime.Now.Second % 10 != 0) { throw new Exception("出錯"); } Console.WriteLine("完成任務"); });
Timeout是定義超時故障。
Policy policy = Policy.Timeout(3, TimeoutStrategy.Pessimistic);// 創建一個3秒鍾(注意單位)的超時策略。
Timeout生成的Policy要和其他Policy一起Wrap使用。超時策略一般不能直接用,而是和其他封裝到一起用:
Policy 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。 這個的用途:請求網絡接口,避免接口長期沒有響應造成系統卡死。
Polly 的異步用法
所有方法都用Async方法即可,Handle由於只是定義異常,所以不需要異常方法:
帶返回值的例子:

Policy<byte[]> policy = Policy<byte[]>.Handle<Exception>() .FallbackAsync(async c => { Console.WriteLine("執行出錯"); return new byte[0]; }, async r => { Console.WriteLine(r.Exception); }); policy = policy.WrapAsync(Policy.TimeoutAsync(20, TimeoutStrategy.Pessimistic, async (context, timespan, task) => { Console.WriteLine("timeout"); })); var bytes = await policy.ExecuteAsync(async () => { Console.WriteLine("開始任務"); HttpClient httpClient = new HttpClient(); var result = await httpClient.GetByteArrayAsync("http://static.rupeng.com/upload/chatimage/20183/07EB793A4C247A654B31B4D14EC64BCA.png"); Console.WriteLine("完成任務"); return result; }); Console.WriteLine("bytes長度" + bytes.Length);
沒返回值的例子

Policy policy = Policy .Handle<Exception>() .FallbackAsync(async c => { Console.WriteLine("執行出錯"); }, async ex => {//對於沒有返回值的,這個參數直接是異常 Console.WriteLine(ex); }); policy = policy.WrapAsync(Policy.TimeoutAsync(3, TimeoutStrategy.Pessimistic, async (context, timespan, task) => { Console.WriteLine("timeout"); })); await policy.ExecuteAsync(async () => { Console.WriteLine("開始任務"); await Task.Delay(5000);//注意不能用Thread.Sleep(5000); Console.WriteLine("完成任務"); });
AOP 框架基礎
要求懂的知識:AOP、Filter、反射(Attribute)。
如果直接使用 Polly,那么就會造成業務代碼中混雜大量的業務無關代碼。我們使用 AOP (如果不了解 AOP,請自行參考網上資料)的方式封裝一個簡單的框架,模仿 Spring cloud 中的 Hystrix。
需要先引入一個支持.Net Core 的 AOP,目前我發現的最好的.Net Core 下的 AOP 框架是AspectCore(國產,動態織入),其他要不就是不支持.Net Core,要不就是不支持對異步方法進行攔截。MVC Filter
GitHub:https://github.com/dotnetcore/AspectCore-Framework
這里只介紹和我們相關的用法:
新建控制台項目 aoptest1 ,並添加nuget引用
Install-Package 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"); } } }
AspectContext的屬性的含義:
Implementation 實際動態創建的Person子類的對象。
ImplementationMethod就是Person子類的Say方法
Parameters 方法的參數值。
Proxy==Implementation:當前場景下
ProxyMethod==ImplementationMethod:當前場景下
ReturnValue返回值
ServiceMethod是Person的Say方法
編寫需要被代理攔截的類
在要被攔截的方法上標注CustomInterceptorAttribute 。類需要是public類,方法需要是虛方法,支持異步方法,因為動態代理是動態生成被代理的類的動態子類實現的。
建立測試類 Person.cs

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("rupeng.com"); }
注意p指向的對象是AspectCore生成的Person的動態子類的對象,直接new Person是無法被攔截的。
創建簡單的熔斷降級框架
新建控制台項目 MyHystrix1 添加nuget引用
Install-Package AspectCore.Core
編寫熔斷降級框架 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實際執行的對象p //context.Parameters方法參數值 //如果執行失敗,則執行FallBackMethod var fallBackMethod = context.ServiceMethod.DeclaringType.GetMethod(this.FallBackMethod); Object fallBackResult = fallBackMethod.Invoke(context.Implementation, context.Parameters); context.ReturnValue = fallBackResult; } } }
新建測試類 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; } }
要達到的目標是:參與降級的方法參數要一樣。當HelloAsync執行出錯的時候執行HelloFallBackAsync方法。
創建代理對象
ProxyGeneratorBuilder proxyGeneratorBuilder = new ProxyGeneratorBuilder(); using (IProxyGenerator proxyGenerator = proxyGeneratorBuilder.Build()) { Person p = proxyGenerator.CreateClassProxy<Person>(); Console.WriteLine(p.HelloAsync("yzk").Result); Console.WriteLine(p.Add(1, 2)); }
上面的Person代碼還支持多次降級,方法上標注[HystrixCommand]並且virtual即可:

public class Person//需要public類 { [HystrixCommand(nameof(Hello1FallBackAsync))] public virtual async Task<string> HelloAsync(string name)//需要是虛方法 { Console.WriteLine("hello" + name); String s = null; s.ToString(); return "ok"; } [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; } }
細化框架
github最新地址 https://github.com/yangzhongke/RuPeng.HystrixCore
Nuget地址:https://www.nuget.org/packages/RuPeng.HystrixCore
重試:MaxRetryTimes表示最多重試幾次,如果為0則不重試,RetryIntervalMilliseconds 表示重試間隔的毫秒數;
熔斷:EnableCircuitBreaker是否啟用熔斷,ExceptionsAllowedBeforeBreaking表示熔斷前出現允許錯誤幾次,MillisecondsOfBreak表示熔斷多長時間(毫秒);超時:TimeOutMilliseconds執行超過多少毫秒則認為超時(0表示不檢測超時)緩存CacheTTLMilliseconds 緩存多少毫秒(0 表示不緩存),用“類名+方法名+所有參數值ToString拼接”做緩存Key(唯一的要求就是參數的類型ToString對於不同對象一定要不一樣)。
由於CircuitBreaker要求同一段代碼必須共享同一個Policy對象。而方法上標注的Attribute 對於這個方法來講就是唯一的對象,一個方法對應一個方法上標注的Attribute對象。一般我們熔斷控制是針對一個方法,一個方法無論是通過幾個 Person 對象調用,無論是誰調用,只要全局出現ExceptionsAllowedBeforeBreaking次錯誤,就會熔斷,這是我框架的實現,你如果認為不合理,你自己改去。我們在Attribute上聲明一個Policy的成員變量,這樣一個方法就對應一個Policy對象。
Install-Package Microsoft.Extensions.Caching.Memory
新建類庫 RuPeng.HystrixCore 添加nuget引用
Install-Package AspectCore.Core
Install-Package Polly
編寫熔斷降級框架 HystrixCommandAttribute.cs

[AttributeUsage(AttributeTargets.Method)] public class HystrixCommandAttribute : AbstractInterceptorAttribute { /// <summary> /// 最多重試幾次,如果為0則不重試 /// </summary> public int MaxRetryTimes { get; set; } = 0; /// <summary> /// 重試間隔的毫秒數 /// </summary> public int RetryIntervalMilliseconds { get; set; } = 100; /// <summary> /// 是否啟用熔斷 /// </summary> public bool EnableCircuitBreaker { get; set; } = false; /// <summary> /// 熔斷前出現允許錯誤幾次 /// </summary> public int ExceptionsAllowedBeforeBreaking { 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 static ConcurrentDictionary<MethodInfo, Policy> policies = new ConcurrentDictionary<MethodInfo, Policy>(); private static readonly Microsoft.Extensions.Caching.Memory.IMemoryCache memoryCache = new Microsoft.Extensions.Caching.Memory.MemoryCache(new Microsoft.Extensions.Caching.Memory.MemoryCacheOptions()); /// <summary> /// /// </summary> /// <param name="fallBackMethod">降級的方法名</param> public HystrixCommandAttribute(string fallBackMethod) { this.FallBackMethod = fallBackMethod; } public string FallBackMethod { get; set; } public override async Task Invoke(AspectContext context, AspectDelegate next) { //一個HystrixCommand中保持一個policy對象即可 //其實主要是CircuitBreaker要求對於同一段代碼要共享一個policy對象 //根據反射原理,同一個方法的MethodInfo是同一個對象,但是對象上取出來的HystrixCommandAttribute //每次獲取的都是不同的對象,因此以MethodInfo為Key保存到policies中,確保一個方法對應一個policy實例 policies.TryGetValue(context.ServiceMethod, out Policy policy); lock (policies)//因為Invoke可能是並發調用,因此要確保policies賦值的線程安全 { if (policy == null) { policy = Policy.NoOpAsync();//創建一個空的Policy if (EnableCircuitBreaker) { policy = policy.WrapAsync(Policy.Handle<Exception>().CircuitBreakerAsync(ExceptionsAllowedBeforeBreaking, TimeSpan.FromMilliseconds(MillisecondsOfBreak))); } if (TimeOutMilliseconds > 0) { policy = policy.WrapAsync(Policy.TimeoutAsync(() => TimeSpan.FromMilliseconds(TimeOutMilliseconds), Polly.Timeout.TimeoutStrategy.Pessimistic)); } if (MaxRetryTimes > 0) { policy = policy.WrapAsync(Policy.Handle<Exception>().WaitAndRetryAsync(MaxRetryTimes, i => TimeSpan.FromMilliseconds(RetryIntervalMilliseconds))); } Policy policyFallBack = 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) => { }); policy = policyFallBack.WrapAsync(policy); //放入 policies.TryAdd(context.ServiceMethod, policy); } } //把本地調用的AspectContext傳遞給Polly,主要給FallbackAsync中使用,避免閉包的坑 Context pollyCtx = new Context(); pollyCtx["aspectContext"] = context; //Install-Package Microsoft.Extensions.Caching.Memory 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); } } }
新建WebAPI項目 hystrixTest1 用於測試添加RuPeng.HystrixCore引用護着nuget引用,但是安裝AspectCore.Extensions.DependencyInjection nuget包
Install-Package AspectCore.Extensions.DependencyInjection
新建測試類 Person.cs

public class Person//需要public類 { [HystrixCommand(nameof(Hello1FallBackAsync))] public virtual async Task<string> HelloAsync(string name)//需要是虛方法 { Console.WriteLine("hello" + name); String s = null; s.ToString(); return "ok"; } [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; } }
修改Startup.cs,這里不再動態創建Person類,而是通過依賴注入和程序集注入
public IServiceProvider ConfigureServices(IServiceCollection services) { services.AddMvc(); // services.AddSingleton<Person>(); RegisterServices(this.GetType().Assembly, services); return services.BuildAspectCoreServiceProvider(); } /// <summary> /// 掃描asm程序集中所有的public類,對於類看看是否含有標注了HystrixCommand的方法 /// 如果有,則AddSingleton到services /// </summary> /// <param name="asm"></param> /// <param name="services"></param> private void RegisterServices(Assembly asm, IServiceCollection services) { foreach (Type type in asm.GetExportedTypes()) { //判斷type類型中是否有至少一個方法含有HystrixCommandAttribute bool hasHystrixCmd = type.GetMethods().Any(m => m.GetCustomAttribute(typeof(HystrixCommandAttribute)) != null); //type.GetMethods().Where(m => m.GetCustomAttribute(typeof(HystrixCommandAttribute)) != null).Count(); if (hasHystrixCmd) { services.AddSingleton(type); } } }
修改ValuesController.cs

private Person p; public ValuesController(Person p) { this.p = p; } // GET api/values [HttpGet] public async Task<IEnumerable<string>> Get() { /* ProxyGeneratorBuilder proxyGeneratorBuilder = new ProxyGeneratorBuilder(); using (IProxyGenerator proxyGenerator = proxyGeneratorBuilder.Build()) { //Person p = new Person(); Person p = proxyGenerator.CreateClassProxy<Person>(); await p.HelloAsync("rupeng.com"); }*/ await p.HelloAsync("rupeng.com"); return new string[] { "value1", "value2" }; }
沒必要、也不可能把所有Polly都封裝到Hystrix中。框架不是萬能的,不用過度框架,過度框架帶來的復雜度陡增,從人人喜歡變成人人恐懼。
Ocelot API網關(API GateWay)
現有微服務的幾點不足:
1) 對於在微服務體系中、和 Consul 通訊的微服務來講,使用服務名即可訪問。但是對於手機、web 端等外部訪問者仍然需要和 N 多服務器交互,需要記憶他們的服務器地址、端口號等。一旦內部發生修改,很麻煩,而且有時候內部服務器是不希望外界直接訪問的。
2) 各個業務系統的人無法自由的維護自己負責的服務器;
3) 現有的微服務都是“我家大門常打開”,沒有做權限校驗。如果把權限校驗代碼寫到每個微服務上,那么開發工作量太大。
4) 很難做限流、收費等。
ocelot 中文文檔:https://blog.csdn.net/sD7O95O/article/details/79623654
資料:http://www.csharpkit.com/apigateway.html
官網:https://github.com/ThreeMammals/Ocelot
騰訊.Net 大隊長“張善友”是項目主力開發人員之一。
Ocelot 基本配置
Ocelot 就是一個提供了請求路由、安全驗證等功能的 API 網關微服務。建一個空的aspnet core空項目 ocelottest1 。
Install-Package Ocelot
項目根目錄下創建 configuration.json

{ "ReRoutes": [ { "DownstreamPathTemplate": "/api/{url}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 5001 } ], "UpstreamPathTemplate": "/MsgService/{url}", "UpstreamHttpMethod": [ "Get", "Post" ] }, { "DownstreamPathTemplate": "/api/{url}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 5003 } ], "UpstreamPathTemplate": "/ProductService/{url}", "UpstreamHttpMethod": [ "Get", "Post" ] } ] }
Program.cs的CreateWebHostBuilder中
public static IWebHost BuildWebHost(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .UseUrls("http://127.0.0.1:8888") .ConfigureAppConfiguration((hostingContext, builder) => { builder.AddJsonFile("configuration.json", false, true); }) .Build();
Startup.cs中通過構造函數注入一個IConfiguration;
public class Startup { private IConfiguration Configuration;
public Startup(IConfiguration Configuration)
{
this.Configuration = Configuration;
}
// This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { 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.Run(async (context) => //{ // await context.Response.WriteAsync("Hello World!"); //}); app.UseOcelot().Wait();//不要忘了寫Wait } }
這樣當訪問http://127.0.0.1:8888/MsgService/sms/Send_LX的時候就會訪問 http://127.0.0.1:5001/api/sms/Send_LX
UpstreamHttpMethod表示對什么樣的請求類型做轉發。
Ocelot+Consul
新啟動一個服務方便后面做負載均衡
dotnet MsgService.dll --ip 127.0.0.1 --port 5002
上面的配置還是把服務的ip地址寫死了,Ocelot可以和Consul通訊,通過服務名字來配置。 只要改配置文件即可configuration.json

{ "ReRoutes": [ { "DownstreamPathTemplate": "/api/{url}", "DownstreamScheme": "http", "UpstreamPathTemplate": "/MsgService/{url}", "UpstreamHttpMethod": [ "Get", "Post" ], "ServiceName": "MsgService", "LoadBalancerOptions": { "Type": "RoundRobin" }, "UseServiceDiscovery": true } ], "GlobalConfiguration": { "ServiceDiscoveryProvider": { "Host": "localhost", "Port": 8500 } } }
訪問 http://127.0.0.1:8888/MsgService/sms/Send_LX 即可,請求報文體
{"phoneNum":"1234567890","msg":"您有新短信"}
表示只要是/MsgService/開頭的都會轉給后端的服務名為" MsgService "的一台服務器,轉發的路徑是"/api/{url}"。
LoadBalancerOptions 中"LeastConnection"表示負載均衡算法是“選擇當前最少連接數的服務器”,如果改為 RoundRobin 就是“輪詢”。
ServiceDiscoveryProvider 是 Consul 服務器的配置。
Ocelot 因為是流量中樞,也是可以做集群的。
(*) 也 支 持 Eureka 進行服務的注冊 、 查 找(http://ocelot.readthedocs.io/en/latest/features/servicediscovery.html),也支持訪問 Service Fabric 中的服務(http://ocelot.readthedocs.io/en/latest/features/servicefabric.html)。
Ocelot 其他功能簡單介紹
限流
文檔:http://ocelot.readthedocs.io/en/latest/features/ratelimiting.html 需要和 Identity Server 一起使用,其他的限速是針對 clientId 限速,而不是針對 ip 限速。比如我調用微博的api開發了一個如鵬版新浪微博,我的 clientid 是 rpwb,然后限制了 1 秒鍾只能調用 1000 次,那么所有用如鵬版微博這個 app 的所有用戶加在一起,在一秒鍾之內,不能累計超過 1000 次。目前開放式 api 的限流都是這個套路。
如果要做針對 ip 的限速等,要自己在 Ocelot 前面架設 Nginx 來實現。
請求緩存
http://ocelot.readthedocs.io/en/latest/features/caching.html 只支持 get,只要 url 不變,就會緩存。
QOS(熔斷器)
http://ocelot.readthedocs.io/en/latest/features/qualityofservice.html
JWT算法
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 相差的秒數
不用秘鑰解析數據
payload 因為 payload 部分是明文的,所以在不知道秘鑰的時候也可以用 Decode、DecodeToObject 等不需要秘鑰的方法把payload部分解析出來。

var token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJVc2VySWQiOjEyMywiVXNlck5hbWUiOiJhZG1pbiJ9.Qjw1epD5P6p4Yy2yju3-fkq28PddznqRj3ESfALQy_U"; 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); Console.WriteLine(json); } catch (FormatException) { Console.WriteLine("Token format invalid"); } catch (TokenExpiredException) { Console.WriteLine("Token has expired"); }
Ocelot+Identity Server
用JWT機制實現驗證的原理如下圖:認證服務器負責頒發Token(相當於JWT值)和校驗Token的合法性。
相關概念
API 資源(API Resource):微博服務器接口、斗魚彈幕服務器接口、斗魚直播接口就是API 資源。
客戶端(Client):Client 就是官方微博 android 客戶端、官方微博 ios 客戶端、第三方微博客戶端、微博助手等。
身份資源(Identity Resource):就是用戶。
一個用戶可能使用多個客戶端訪問服務器;一個客戶端也可能服務多個用戶。
封禁了一個客戶端,所有用戶都不能使用這個這個客戶端訪問服務器,但是可以使用其他客戶端訪問;封禁了一個用戶,這個用戶在所有設備上都不能訪問,但是不影響其他用戶。
搭建 identity server 認證服務器
新建一個空的 web 項目 ID4.IdServer
Install-Package IdentityServer4
首先編寫一個提供應用列表、賬號列表的 Config.cs 類

public class Config { /// <summary> /// 返回應用列表 /// </summary> /// <returns></returns> public static IEnumerable<ApiResource> GetApiResources() { List<ApiResource> resources = new List<ApiResource>(); //ApiResource第一個參數是應用的名字,第二個參數是描述 resources.Add(new ApiResource("MsgAPI", "消息服務API")); resources.Add(new ApiResource("ProductAPI", "產品API")); return resources; } /// <summary> /// 返回賬號列表 /// </summary> /// <returns></returns> public static IEnumerable<Client> GetClients() { List<Client> clients = new List<Client>(); clients.Add(new Client { ClientId = "clientPC1",//API賬號、客戶端Id AllowedGrantTypes = GrantTypes.ClientCredentials,//認證方式 ClientSecrets = { new Secret("123321".Sha256())//秘鑰 }, AllowedScopes = { "MsgAPI", "ProductAPI" }//這個賬號支持訪問哪些應用 }); return clients; } }
如果允許在數據庫中配置賬號等信息,那么可以從數據庫中讀取然后返回這些內容。疑問待解。
修改Startup.cs
public void ConfigureServices(IServiceCollection services) { services.AddIdentityServer() .AddDeveloperSigningCredential() .AddInMemoryApiResources(Config.GetApiResources()) .AddInMemoryClients(Config.GetClients()); } // 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.Run(async (context) => //{ // await context.Response.WriteAsync("Hello World!"); //}); app.UseIdentityServer(); }
然后在 9500 端口啟動
在 postman 里發出請求,獲取 token http://localhost:9500/connect/token,發 Post 請求,表單請求內容(注意不是報文頭):
client_id=clientPC1 client_secret=123321 grant_type=client_credentials
把返回的 access_token 留下來后面用(注意有有效期)。 注意,其實不應該讓客戶端直接去申請 token,這只是咱演示,后面講解正確做法。
搭建 Ocelot 服務器項目
新建空 Web 項目,項目名 ID4.Ocelot1
nuget 安裝 IdentityServer4、Ocelot
Install-Package IdentityServer4
Install-Package Ocelot
編寫配置文件 Ocelot.json(注意設置【如果較新則】)

{ "ReRoutes": [ { "DownstreamPathTemplate": "/api/{url}", "DownstreamScheme": "http", "UpstreamPathTemplate": "/MsgService/{url}", "UpstreamHttpMethod": [ "Get", "Post" ], "ServiceName": "MsgService", "LoadBalancerOptions": { "Type": "RoundRobin" }, "UseServiceDiscovery": true, "AuthenticationOptions": { "AuthenticationProviderKey": "MsgKey", "AllowedScopes": [] } }, { "DownstreamPathTemplate": "/api/{url}", "DownstreamScheme": "http", "UpstreamPathTemplate": "/ProductService/{url}", "UpstreamHttpMethod": [ "Get", "Post" ], "ServiceName": "ProductService", "LoadBalancerOptions": { "Type": "RoundRobin" }, "UseServiceDiscovery": true, "AuthenticationOptions": { "AuthenticationProviderKey": "ProductKey", "AllowedScopes": [] } } ], "GlobalConfiguration": { "ServiceDiscoveryProvider": { "Host": "localhost", "Port": 8500 } } }
把/MsgService 訪問的都轉給消息后端服務器(使用Consul進行服務發現)。也可以把Identity Server配置到Ocelot,但是我們不做,后邊會講為什么不放。
在Program.cs 的 CreateWebHostBuilder 中加載 Ocelot.json
public static IWebHost BuildWebHost(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .ConfigureAppConfiguration((hostingContext, builder)=> { builder.AddJsonFile("Ocelot.json", false, true); }) .Build();
修改 Startup.cs 讓 Ocelot 能夠訪問 Identity Server 進行 Token 的驗證
public void ConfigureServices(IServiceCollection services) { //指定Identity Server的信息 Action<IdentityServerAuthenticationOptions> isaOptMsg = o => { o.Authority = "http://localhost:9500"; o.ApiName = "MsgAPI";//要連接的應用的名字 o.RequireHttpsMetadata = false; o.SupportedTokens = SupportedTokens.Both; o.ApiSecret = "123321";//秘鑰 }; Action<IdentityServerAuthenticationOptions> isaOptProduct = o => { o.Authority = "http://localhost:9500"; o.ApiName = "ProductAPI";//要連接的應用的名字 o.RequireHttpsMetadata = false; o.SupportedTokens = SupportedTokens.Both; o.ApiSecret = "123321";//秘鑰 }; //對配置文件中使用ChatKey配置了AuthenticationProviderKey=MsgKey //的路由規則使用如下的驗證方式 services.AddAuthentication() .AddIdentityServerAuthentication("MsgKey", isaOptMsg) .AddIdentityServerAuthentication("ProductKey", isaOptProduct); 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.Run(async (context) => //{ // await context.Response.WriteAsync("Hello World!"); //}); app.UseOcelot().Wait(); }
很顯然我們可以讓不同的服務采用不同的Identity Server。
啟動 Ocelot 服務器,然后向 ocelot 請求/MsgService/sms/Send_LX(報文體還是要傳 json 數據),在請求頭(不是報文體)里加上:Authorization="Bearer "+上面 identityserver 返回的 accesstoken
如果返回 401,那就是認證錯誤。
Ocelot 會把 Authorization 值傳遞給后端服務器,這樣在后端服務器可以用 IJwtDecoder 的這個不傳遞 key 的重載方法 IDictionary<string, object> DecodeToObject(string token),就可以在不驗證的情況下獲取 client_id 等信息。
也可以把 Identity Server 通過 Consul 進行服務治理。
Ocelot+Identity Server 實現了接口的權限驗證,各個業務系統不需要再去做驗證。
不能讓客戶端請求 token
上面是讓客戶端去請求 token,如果項目中這么搞的話,就把 client_id 特別是 secret 泄露給普通用戶的。正確的做法應該是,開發一個 token 服務,由這個服務來向 identity Server 請求 token,客戶端向 token 服務發請求,把 client_id、secret 藏到這個 token 服務器上。當然這個服務器也要經過 Ocelot 轉發。這個做起來很簡單,就不演示了。放到下面一起演示。
用戶名密碼登錄
如果 Api 和用戶名、密碼無關(比如系統內部之間 API 的調用),那么上面那樣做就可以了,但是有時候需要用戶身份驗證的(比如 Android 客戶端)。也就是在請求 token 的時候還要驗證用戶名密碼,在服務中還可以獲取登錄用戶信息。
修改認證系統
修改的地方:
ID4.IdServer 項目中增加類 ProfileService.cs

public class ProfileService : IProfileService { public async Task GetProfileDataAsync(ProfileDataRequestContext context) { var claims = context.Subject.Claims.ToList(); context.IssuedClaims = claims.ToList(); } public async Task IsActiveAsync(IsActiveContext context) { context.IsActive = true; } }
增加類 ResourceOwnerPasswordValidator.cs

public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator { public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context) { //根據context.UserName和context.Password與數據庫的數據做校驗,判斷是否合法 if (context.UserName == "yzk" && context.Password == "123") { context.Result = new GrantValidationResult( subject: context.UserName, authenticationMethod: "custom", claims: new Claim[] { new Claim("Name", context.UserName), new Claim("UserId", "111"), new Claim("RealName", "楊中科"), new Claim("Email", "yzk365@qq.com") }); } else { //驗證失敗 context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "invalid custom credential"); } } }
當然這里的用戶名密碼是寫死的,可以在項目中連接自己的用戶數據庫進行驗證。claims 中可以放入多組用戶的信息,這些信息都可以在業務系統中獲取到。
修改一下Config.cs,主要是把GetClients中的AllowedGrantTypes屬性值改為GrantTypes.ResourceOwnerPassword,並且在AllowedScopes中加入IdentityServerConstants.StandardScopes.OpenId, //必須要添加,否則報forbidden錯誤IdentityServerConstants.StandardScopes.Profile
修改后的 Config.cs
public class Config { /// <summary> /// 返回應用列表 /// </summary> /// <returns></returns> public static IEnumerable<ApiResource> GetApiResources() { List<ApiResource> resources = new List<ApiResource>(); //ApiResource第一個參數是應用的名字,第二個參數是描述 resources.Add(new ApiResource("MsgAPI", "消息服務API")); resources.Add(new ApiResource("ProductAPI", "產品API")); return resources; } /// <summary> /// 返回賬號列表 /// </summary> /// <returns></returns> public static IEnumerable<Client> GetClients() { List<Client> clients = new List<Client>(); clients.Add(new Client { ClientId = "clientPC1",//API賬號、客戶端Id AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,//認證方式 ClientSecrets = { new Secret("123321".Sha256())//秘鑰 }, AllowedScopes = { "MsgAPI", "ProductAPI", IdentityServerConstants.StandardScopes.OpenId, //必須要添加,否則報forbidden錯誤 IdentityServerConstants.StandardScopes.Profile }//這個賬號支持訪問哪些應用 }); return clients; } }
Startup.cs 的 ConfigureServices 修改為
public void ConfigureServices(IServiceCollection services) { var idResources = new List<IdentityResource> { new IdentityResources.OpenId(), //必須要添加,否則報無效的 scope 錯誤 new IdentityResources.Profile() }; services.AddIdentityServer() .AddDeveloperSigningCredential() .AddInMemoryIdentityResources(idResources) .AddInMemoryApiResources(Config.GetApiResources()) .AddInMemoryClients(Config.GetClients()) .AddResourceOwnerValidator<ResourceOwnerPasswordValidator>() .AddProfileService<ProfileService>(); }
主要是增加了 AddInMemoryIdentityResources 、 AddResourceOwnerValidator 、AddProfileService
修改業務系統
以 MsgService 為例,安裝nuget包
Install-Package IdentityServer4.AccessTokenValidation
然后 Startup.cs 的 ConfigureServices 中增加
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddAuthentication("Bearer") .AddIdentityServerAuthentication(options => { options.Authority = "http://localhost:9500";//identity server 地址 options.RequireHttpsMetadata = false; }); }
Startup.cs 的 Configure 中增加
app.UseAuthentication(); //一定要放在app.UseMvc();上部
app.UseMvc();
請求測試
請求 ID4.IdServer認證服務的token 把報文頭中的 grant_type 值改為 password,報文頭增加 username、password 為用戶名、密碼。
像之前一樣用返回的 access_token傳遞給請求的Authorization 中,在業務系統的 User中就可以獲取到 ResourceOwnerPasswordValidator 中為用戶設置的 claims 等信息了。
[HttpPost(nameof(Send_LX))] public void Send_LX(SendSMSRequest model) { string name = this.User.Identity.Name;//讀取的就是"Name"這個特殊的 Claims 的值 string userId = this.User.FindFirst("UserId").Value; string realName = this.User.FindFirst("RealName").Value; string email = this.User.FindFirst("Email").Value; Console.WriteLine($"name={name},userId={userId},realName={realName},email={email}"); Console.WriteLine($"通過聯想短信接口向{model.PhoneNum}發送短信{model.Msg}"); }
獨立登錄服務器
解決上面提到的“不能讓客戶端接觸到 client_id、secret 的問題”
開發一個WebAPI應用 LoginService
新建請求體類RequestTokenParam.cs

public class RequestTokenParam { public string username { get; set; } public string password { get; set; } }
新建登錄控制器 LoginController

[Produces("application/json")] [Route("api/Login")] public class LoginController : Controller { [HttpPost] public async Task<ActionResult> RequestToken(RequestTokenParam model) { Dictionary<string, string> dict = new Dictionary<string, string>(); dict["client_id"] = "clientPC1"; dict["client_secret"] = "123321"; dict["grant_type"] = "password"; dict["username"] = model.username; dict["password"] = model.password; //由登錄服務器向IdentityServer發請求獲取Token using (HttpClient http = new HttpClient()) using (var content = new FormUrlEncodedContent(dict)) { var msg = await http.PostAsync("http://localhost:9500/connect/token", content); string result = await msg.Content.ReadAsStringAsync(); return Content(result, "application/json"); } } }
修改Program.cs使用6000端口
修改項目 ID4.Ocelot1 配置文件Ocelot.json,新增配置進行登錄網關處理
{ "ReRoutes": [ { "DownstreamPathTemplate": "/api/{url}", "DownstreamScheme": "http", "UpstreamPathTemplate": "/MsgService/{url}", "UpstreamHttpMethod": [ "Get", "Post" ], "ServiceName": "MsgService", "LoadBalancerOptions": { "Type": "RoundRobin" }, "UseServiceDiscovery": true, "AuthenticationOptions": { "AuthenticationProviderKey": "MsgKey", "AllowedScopes": [] } }, { "DownstreamPathTemplate": "/api/{url}", "DownstreamScheme": "http", "UpstreamPathTemplate": "/ProductService/{url}", "UpstreamHttpMethod": [ "Get", "Post" ], "ServiceName": "ProductService", "LoadBalancerOptions": { "Type": "RoundRobin" }, "UseServiceDiscovery": true, "AuthenticationOptions": { "AuthenticationProviderKey": "ProductKey", "AllowedScopes": [] } }, { "DownstreamPathTemplate": "/api/{url}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 6008 } ], "UpstreamPathTemplate": "/LoginService/{url}", "UpstreamHttpMethod": [ "Get", "Post" ] } ], "GlobalConfiguration": { "ServiceDiscoveryProvider": { "Host": "localhost", "Port": 8500 } } }
這樣客戶端只要向 LoginService 的 /api/Login/ 發請求帶上 json 報文體{username:"yzk",password:"123"}即可。客戶端就不知道 client_secret 這些機密信息了。
把 LoginService 配置到 Ocelot 中。
參考文章:https://www.cnblogs.com/jaycewu/p/7791102.html
Thrift高效通訊
什么是 RPC
Restful 采用 Http 進行通訊,優點是開放、標准、簡單、兼容性升級容易;缺點是性能略低。在 QPS 高或者對響應時間要求苛刻的服務上,可以用 RPC(Remote Procedure Call),RPC 由於采用二進制傳輸、TCP 通訊,所以通常性能更好。
.Net Core 下的 RPC(遠程方法調用)框架有 gRPC、Thrift 等,都支持主流的編程語言。
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:

修改Program.cs

創建客戶端項目 ThriftTest1.Client,建一個控制台項目(放到 web 項目中或者在 Linux中用守護進程運行起來(SuperVisor等,類似Windows下的“Windows服務”)也可以)。
ThriftTest1.Server項目引用ThriftTest1.Contract
修改Program.cs

分別啟動:
一個服務器中放多個服務
0.9.1之前只支持一個服務器一個服務,這也是建議的做法。之后支持多路服務在thrift中增加一個服務
service CalcService{ i32 Add(1:i32 i1,2:i32 i2) }
服務器:
新增實現類CalcServiceImpl.cs

修改Program.cs

客戶端:
修改Program.cs

分別啟動:
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的版本一致):

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

也可以用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
源碼位置:https://download.csdn.net/download/qq_25153485/10538936