.在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