ASP.Net Core的基本配置
.在VS中调试的时候有很多修改Web应用运行端口的方法。但是在开发、调试微服务应用的时候可能需要同时在不同端口上开启多个服务器的实例,因此下面主要看看如何通过命令行指定Web应用的端口(默认5000)
可以通过设置临时环境变量ASPNETCORE URLS来改变默认的端口、域名,也就是执行 dotnet xxx.dll之前执行set ASPNETCORE_URLS=http://127.0.0.1:5001来设置环境变量。
如果需要在程序中读取端口、域名(后续服务治理会用到) ,用ASPNETCORE URLS环境变量就不太方便,可以自定义配置文件, 自己读取设置。
修改Program.cs
public static IWebHost BuildWebHost(string[] args) { var config = new ConfigurationBuilder() .AddCommandLine(args) .Build(); String ip = config["ip"]; String port = config["port"]; return WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .UseUrls($"http://{ip}:{port}") .Build(); }
然后启动的时候:
dotnet WebApplication5.dll--ip 127.0.0.1-port 8889
.Net Core因为跨平台,所以可以不依赖于IIS运行了。可以用.Net Core内置的kestrel服务器运行网站,当然真正面对终端用户访问的时候一般通过Nginx等做反向代理。
Consul服务治理发现
Consul是注册中心,服务提供者、服务消费者等都要注册到Consul中,这样就可以实, ,现服务提供者、服务消费者的隔离。
除了Consul之外,还有Eureka,Zookeeper等类似软件。
Consul服务安装
consul下载地址https://www.consul.io/
运行
consul.exe agent -dev
这是开发环境测试,生产环境要建集群,要至少一台Server,多台Agent consul
监控页面http://127.0.0.1:8500/consult
主要做三件事:提供服务到ip地址的注册;提供服务到ip地址列表的查询;对提供服务方的健康检查(HealthCheck) ;
.Net Core连接Consul
新建Asp.Net Core WebAPI项目WebApplication4,安装Consul nuget包
Install-Package Consul
Rest服务的准备
先使用使用默认生成的ValuesController做测试
再提供一个HealthController.cs
[Route("api/Health")] public class HealthController : Controller { [HttpGet] public IActionResult Get() { return Ok("ok"); } }
服务器从命令行中读取ip和端口
让Rest服务注册到Consul中
Startup.cs:
using Consul; public void Configure(IApplicationBuilder app, IHostingEnvironment env) { //... ... app.UseMvc(); String ip = Configuration["ip"];//部署到不同服务器的时候不能写成127.0.0.1或者0.0.0.0,因为这是让服务消费者调用的地址 Int32 port = Int32.Parse(Configuration["port"]); //向consul注册服务 ConsulClient client = new ConsulClient(ConfigurationOverview); Task<WriteResult> result= client.Agent.ServiceRegister(new AgentServiceRegistration() { ID = "apiservice1" + Guid.NewGuid(),//服务编号,不能重复,用Guid最简单 Name = "apiservice1",//服务的名字 Address = ip,//我的ip地址(可以被其他应用访问的地址,本地测试可以用127.0.0.1,机房环境中一定要写自己的内网ip地址) Port = port,//我的端口 Check = new AgentServiceCheck() { DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//服务停止多久后反注册 Interval =TimeSpan.FromSeconds(10),//健康检查时间间隔,或者称为心跳间隔 HTTP =$"http://{ip}:{port}/api/health",//健康检查地址, Timeout =TimeSpan.FromSeconds(5) } }); } private static void ConfigurationOverview(ConsulClientConfiguration obj) { obj.Address = new Uri("http://127.0.0.1:8500"); obj.Datacenter = "dc1"; }
注意不同实例一定要用不同的Id,即使是相同服务的不同实例也要用不同的ld,上面的代码用Guid做Id,确保不重复。相同的服务用相同的Name. Address、 Port是供服务消 "费者访问的服务器地址(或者IP地址)及端口号。Check则是做服务健康检查的(解释一下)。
在注册服务的时候还可以通过AgentServiceRegistration的Tags属性设置额外的标签。
通过命令行启动两个实例
dotnet WebApplication4.dll --ip 127.0.0.1 --port 5001 dotnet WebApplication4.dll --ip 127.0.0.1 --port 5002
应用停止的时候反注册。
服务查询
新建控制台项目queryconsul1,并引用nuget包
using Consul; static void Main(string[] args) { using (ConsulClient consulClient = new ConsulClient(c=>c.Address=new Uri("http://127.0.0.1:8500"))) { //consulClient.Agent.Services()获取consul中注册的所有的服务 Dictionary<String,AgentService> services = consulClient.Agent.Services().Result.Response; foreach (KeyValuePair<String, AgentService> kv in services) { Console.WriteLine($"key={kv.Key},{kv.Value.Address},{kv.Value.ID},{kv.Value.Service},{kv.Value.Port}"); } //获取所有服务名字是"apiservice1"所有的服务 var agentServices = services.Where(s => s.Value.Service.Equals("apiservice1", StringComparison.CurrentCultureIgnoreCase)) .Select(s => s.Value); //根据当前TickCount对服务器个数取模,“随机”取一个机器出来,避免“轮询”的负载均衡策略需要计数加锁问题 var agentService = agentServices.ElementAt(Environment.TickCount%agentServices.Count()); Console.WriteLine($"{agentService.Address},{agentService.ID},{agentService.Service},{agentService.Port}"); } Console.ReadKey(); }
编写服务消费者
创建类库RestTools
添加Consul nuget包引用
Install-Package Consul
Install-Package Newtonsoft.Json
创建消息返回类ResponseEntity.cs
public class ResponseEntity<T> { /// <summary> /// 返回状态码 /// </summary> public HttpStatusCode StatusCode { get; set; } /// <summary> /// 返回的json反序列化出来的对象 /// </summary> public T Body { get; set; } /// <summary> /// 响应的报文头 /// </summary> public HttpResponseHeader Headers { get; set; } }
创建转发消息类RestTemplate.cs
public class RestTemplate { private String consulServerUrl; public RestTemplate(String consulServerUrl) { this.consulServerUrl = consulServerUrl; } /// <summary> /// 获取服务的一个IP地址 /// </summary> /// <param name="serviceName">consul服务IP</param> /// <returns></returns> private async Task<String> ResolveRootUrlAsync(String serviceName) { using (var consulClient = new ConsulClient(c => c.Address = new Uri(consulServerUrl))) { var services = (await consulClient.Agent.Services()).Response; var agentServices = services.Where(s => s.Value.Service.Equals(serviceName, StringComparison.InvariantCultureIgnoreCase)).Select(s => s.Value); //TODO:注入负载均衡策略 var agentService = agentServices.ElementAt(Environment.TickCount % agentServices.Count()); //根据当前TickCount对服务器个数取模,“随机”取一个机器出来,避免“轮询”的负载均衡策略需要计数加锁问题 return agentService.Address + ":" + agentService.Port; } } /// <summary> /// //把http://apiservice1/api/values转换为http://192.168.1.1:5000/api/values /// </summary> private async Task<String> ResolveUrlAsync(String url) { Uri uri = new Uri(url); String serviceName = uri.Host;//apiservice1 String realRootUrl = await ResolveRootUrlAsync(serviceName); return uri.Scheme + "://" + realRootUrl + uri.PathAndQuery; } /// <summary> /// Get请求转换 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="url">请求地址</param> /// <param name="requestHeaders">请求头</param> /// <returns></returns> public async Task<ResponseEntity<T>> GetForEntityAsync<T>(String url, HttpRequestHeaders requestHeaders = null) { using (HttpClient httpClient=new HttpClient()) { HttpRequestMessage requestMsg = new HttpRequestMessage(); if (requestHeaders!=null) { foreach (var header in requestHeaders) { httpClient.DefaultRequestHeaders.Add(header.Key, header.Value); } } requestMsg.Method = HttpMethod.Get; //http://apiservice1/api/values转换为http://192.168.1.1:5000/api/values requestMsg.RequestUri = new Uri(await ResolveUrlAsync(url)); var result = await httpClient.SendAsync(requestMsg); ResponseEntity<T> responseEntity = new ResponseEntity<T>(); responseEntity.StatusCode = result.StatusCode; String bodyStr = await result.Content.ReadAsStringAsync(); responseEntity.Body = JsonConvert.DeserializeObject<T>(bodyStr); responseEntity.Headers = responseEntity.Headers; return responseEntity; } } }
编写控制台进行消费
这里用控制台测试,真实项目中服务消费者通常也是另外一个Web应用。
static void Main(string[] args) { RestTemplate rest = new RestTemplate("http://127.0.0.1:8500"); //RestTemplate把服务的解析和发请求以及响应反序列化帮我们完成 ResponseEntity<String[]> resp = rest.GetForEntityAsync<String[]>("http://apiservice1/api/values").Result; Console.WriteLine(resp.StatusCode); Console.WriteLine(String.Join(",",resp.Body)); Console.ReadKey(); }
测试结果:
解析RestTemplate代码。主要作用:
1) 根据url到Consul中根据服务的名字解析获取一个服务实例,把路径转换为实际连接的服务器;负载均衡,这里用的是简单的随机负载均衡,这样服务的消费者就不用自己指定要访问那个服务提供,者了,解耦、负载均衡。
2) 负载均衡还可以根据权重随机(不同服务器的性能不一样,这样注册服务的时候通过Tags来区,"分),还可以根据消费者IP地址来选择服务实例(涉及到一致性Hash的优化)等。
3) RestTemplate还负责把响应的ison反序列化返回结果。服务的注册者、消费者都是网站内部服务器之间的事情,对于终端用户是不涉及这些的。
终端用户是不访问consul的。对终端用户来讲是对的Web服务器, Web服务器是服务的消费者。
简化服务的注册
每次启动、注册服务都要指定一个端口,本地测试集群的时候可能要启动多个实例,很麻烦.
在ASP. Net Core中只要设定端口为0,那么服务器会随机找一个可用的端口绑定(测试一下).,但是没有找到读取到这个随机端口号的方法.因此自己写:
新建Tools.cs工具类
public class Tools { /// <summary> /// 产生一个介于minPort-maxPort之间的随机可用端口 /// </summary> /// <param name="minPort"></param> /// <param name="maxPort"></param> /// <returns></returns> public static int GetRandAvailablePort(int minPort = 1024, int maxPort = 65535) { Random r = new Random(); while (true) { int port = r.Next(minPort, maxPort); if (!IsPortInUsed(port)) { return port; } } } /// <summary> /// 判断port端口是否在使用中 /// </summary> /// <param name="port"></param> /// <returns></returns> private static bool IsPortInUsed(int port) { IPGlobalProperties ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties(); IPEndPoint[] ipsTCP = ipGlobalProperties.GetActiveTcpListeners(); if (ipsTCP.Any(p=>p.Port==port)) { return true; } IPEndPoint[] ipsUDP = ipGlobalProperties.GetActiveUdpListeners(); if (ipsUDP.Any(p=>p.Port==port)) { return true; } TcpConnectionInformation[] tcpConnInfoArray = ipGlobalProperties.GetActiveTcpConnections(); if (tcpConnInfoArray.Any(conn=>conn.LocalEndPoint.Port==port)) { return true; } return false; } }
使用方法
public static IWebHost BuildWebHost(string[] args) { var config = new ConfigurationBuilder() .AddCommandLine(args) .Build(); String ip = config["ip"]; String port = config["port"]; if (port=="0") { port = Tools.GetRandAvailablePort().ToString(); } return WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .UseUrls($"http://{ip}:{port}") .Build(); }
在程序启动的时候如果port=0或者没有指定port,则自己调用GetRandAvailablePort获取可用端口。
熔断降级
什么是熔断降级
熔断器如同电力过载保护器。它可以实现快速失败,如果它在一段时间内侦测到许多类似的错误,会强迫其以后的多个调用快速失败,不再访问远程服务器,从而防止应用程序不断地尝试执行可能会失败的操作,使得应用程序继续执行而不用等待修正错误,或者浪费时间去等到长时间的超时产生。
降级的目的是当某个服务提供者发生故障的时候,向调用方返回一个错误响应或者替代响应。举例子:如视频播放器请求playsafe的替代方案;加载内容评论时如果出错,则以缓存中加载或者显示"评论暂时不可用" 。
Polly简介
.Net Core中有一个被.Net基金会认可的库Polly,可以用来简化熔断降级的处理。主要功能:重试(Retry) ;断路器(Circuit-breaker) ;超时检测(Timeout) ;缓存(Cache) ;,失败处理(FallBack) ;
官网: https://github. com/App-vNext/Polly
介绍文章: https://www.cnblogs.com/CreateMyself/p/7589397.html
Install-Package Polly -Version 5.9.0
6.0.1对缓存还不支持,因此现在暂时先用5.9.0版本.
Polly简单使用
使用Policv的静态方法创建ISyncPolicy实现类对象,创建方法既有同步方法也有异步方法,根 据自己的需要选择。下面演示同步的,异步的用法类似。
举例:当发生ArgumentException异常的时候,执行Fallback代码。
新建pollytest1控制台项目,添加nuget引用
try { ISyncPolicy policy = Policy.Handle<ArgumentException>(ex => ex.Message == "年龄参数错误") .Fallback(() => { Console.WriteLine("出错了"); }); policy.Execute(()=>{ //这里是可能会产生问题的业务系统代码 Console.WriteLine("开始任务"); throw new ArgumentException("年龄参数错误"); //throw new Exception("haha"); //Console.WriteLine("完成任务"); }); } catch (Exception ex) { Console.WriteLine($"未处理异常:{ex}"); }
详解Polly异常处理
. Handle<Exception> (ex->ex. Message. Contains ("aa"))
参数委托的返回值是boolean类型,如果返回true,就是“这个异常能被我处理”,否则就是“我处理不了" ,会导致未处理异常被抛出。
比如可以实现“我能处理XXX错误信息"
Handle<WebException> (ex=>ex. Status==WebExceptionStatus. SendFailure)
获取异常信息就调用这个重载
public static FallbackPolicy Fallback(this PolicyBuilder policyBuilder, Action fallbackAction, Action<Exception> onFallback); //省略 .Fallback(() =>{},(ex)=> { Console.WriteLine("执行出错,异常"+ex); });
异常处理的套路
ISyncPolicy policy = Policy.Handle<AException>() .Or<BException>() .Or<CException>() ...... .
CircuitBreaker()/.Fallback()/.Retry()/.RetryForever()/.WaitAndRetry()/.WaitAndRetryForever()
当发生AException或者BException或者......的时候进行CircuitBreaker()/.Fallback()等处理。
这些处理不能简单的链式调用,要用到后面的Wrap。
例如下面这样是不行的
ISyncPolicy policy = Policy .Handle<Exception>() .Retry(3) .Fallback(()=> { Console.WriteLine("执行出错"); });//这样不行 policy.Execute(() => { Console.WriteLine("开始任务"); throw new ArgumentException("Hello world!"); Console.WriteLine("完成任务"); });
重试处理
try { ISyncPolicy policy = Policy.Handle<Exception>() .RetryForever();//一直重试 policy.Execute(() => { Console.WriteLine("开始任务"); if (DateTime.Now.Second % 10 != 0) { throw new Exception("出错"); } Console.WriteLine("完成任务"); }); } catch (Exception ex) { Console.WriteLine($"未处理异常:{ex}"); } //RetryForever()是一直重试直到成功 //Retry()是重试最多一次; //Retry(n)是重试最多n次; //WaitAndRetry()可以实现“如果出错等待100ms再试还不行再等150ms秒。。。。”,重载方法很多,一看就懂,不再一一介绍。还有WaitAndRetryForever。
短路保护Circuit Breaker
出现N次连续错误,则把“熔断器”(保险丝)熔断,等待一段时间,等待这段时间内如果再Execute则直接抛出BrokenCircuitException异常。等待时间过去之后,再执行Execute的时候如果又错了(一次就够了),那么继续熔断一段时间,否则就回复正常。
这样就避免一个服务已经不可用了,还是使劲的请求给系统造成更大压力。
ISyncPolicy policy = Policy.Handle<Exception>() .CircuitBreaker(6, TimeSpan.FromSeconds(5));//连续出错6次之后熔断5秒(不会再去尝试执行业务代码)。 while (true) { Console.WriteLine("开始Execute"); try { policy.Execute(() => { Console.WriteLine("开始任务"); throw new Exception("出错"); Console.WriteLine("完成任务"); }); } catch (Exception ex) { Console.WriteLine("execute出错" + ex.GetType() + ":" + ex.Message); } Thread.Sleep(500); }
策略封装
可以把多个ISyncPolicy合并到一起执行:
policy3= policy1.Wrap(policy2);
执行policy3就会把policy1、policy2封装到一起执行
policy9=Policy.Wrap(policy1, policy2, policy3, policy4, policy5);把更多一起封装。
超时处理
创建一个3秒钟(注意单位)的超时策略。
ISyncPolicy policy = Policy.Timeout(3, TimeoutStrategy.Pessimistic);
创建一个3秒钟(注意单位)的超时策略。超时策略一般不能直接用,而是和其他封装到一起用:
ISyncPolicy policy = Policy.Handle<Exception>() .Fallback(() => { Console.WriteLine("执行出错"); }); policy = policy.Wrap(Policy.Timeout(2, TimeoutStrategy.Pessimistic)); policy.Execute(() => { Console.WriteLine("开始任务"); Thread.Sleep(5000); Console.WriteLine("完成任务"); });
上面的代码就是如果执行超过2秒钟,则直接Fallback,Execute中的代码也会被强行终止(引发TimeoutRejectedException异常)。
这个的用途:请求网络接口,避免接口长期没有响应造成系统卡死。
TimeoutStrategy.Optimistic是主动通知代码,告诉他“到期了”,由代码自己决定是不是继续执行,局限性很大,一般不用。
下面的代码,如果发生超时,重试最多3次(也就是说一共执行4次哦)。
ISyncPolicy policy = Policy.Handle<TimeoutRejectedException>() .Retry(1); policy = policy.Wrap(Policy.Timeout(3, TimeoutStrategy.Pessimistic)); policy.Execute(() => { Console.WriteLine("开始任务"); Thread.Sleep(5000); Console.WriteLine("完成任务"); });
缓存
缓存的意思就是N秒内只调用一次方法,其他的调用都返回缓存的数据。
目前只支持Polly 5.9.0,不支持最新版
Install-Package Polly.Caching.MemoryCache
功能局限性也大,简单讲一下,后续先不用这个实现缓存原则:别人的好用我就拿来用,不好用我就自己造。
命令空间都写到代码中,因为有容易引起混淆的同名类。
//Install-Package Microsoft.Extensions.Caching.Memory Microsoft.Extensions.Caching.Memory.IMemoryCache memoryCache = new Microsoft.Extensions.Caching.Memory.MemoryCache(new Microsoft.Extensions.Caching.Memory.MemoryCacheOptions()); //Install-Package Polly.Caching.MemoryCache Polly.Caching.MemoryCache.MemoryCacheProvider memoryCacheProvider = new Polly.Caching.MemoryCache.MemoryCacheProvider(memoryCache); CachePolicy policy = Policy.Cache(memoryCacheProvider, TimeSpan.FromSeconds(5)); Random rand = new Random(); while (true) { int i = rand.Next(5); Console.WriteLine("产生"+i); var context = new Context("doublecache" + i); int result = policy.Execute(ctx => { Console.WriteLine("Execute计算"+i); return i * 2; },context); Console.WriteLine("计算结果:"+result); Thread.Sleep(500); }
AOP框架基础
如果直接使用Polly,那么就会造成业务代码中混杂大量的业务无关代码。我们使用AOP(如果不了解AOP,请自行参考网上资料)的方式封装一个简单的框架,模仿Spring cloud中的Hystrix。
需要先引入一个支持.Net Core的AOP,目前我发现的最好的.Net Core下的AOP框架是AspectCore(国产,动态织入),其他要不就是不支持.Net Core,要不就是不支持对异步方法进行拦截。MVC Filter
GitHub:https://github.com/dotnetcore/AspectCore-Framework
Install-Package AspectCore.Core
新建控制台项目aoptest1,并添加AspectCore.Core包引用
编写拦截器CustomInterceptorAttribute.cs,一般继承自AbstractInterceptorAttribute
public class CustomInterceptorAttribute : AbstractInterceptorAttribute { //每个被拦截的方法中执行 public async override Task Invoke(AspectContext context, AspectDelegate next) { try { Console.WriteLine("Before service call"); await next(context); } catch (Exception) { Console.WriteLine("Service threw an exception!"); throw; } finally { Console.WriteLine("After service call"); } } }
编写需要被代理拦截的类 Person.cs,在要被拦截的方法上标注CustomInterceptorAttribute 。类需要是public类,方法需要是虚!方法,支持异步方法,因为动态代理是动态生成被代理的类的动态子类实现的。
public class Person { [CustomInterceptor] public virtual void Say(string msg) { Console.WriteLine("service calling..."+msg); } }
通过AspectCore创建代理对象
ProxyGeneratorBuilder proxyGeneratorBuilder = new ProxyGeneratorBuilder(); using (IProxyGenerator proxyGenerator=proxyGeneratorBuilder.Build()) { Person p = proxyGenerator.CreateClassProxy<Person>(); p.Say("Hello World"); Console.WriteLine(p.GetType()); Console.WriteLine(p.GetType().BaseType); } Console.ReadKey();
注意p指向的对象是AspectCore生成的Person的动态子类的对象,直接new Person是无法被,拦截的.
执行结果:
创建简单的熔断降级框架
新建控制台项目 hystrixtest1
新建类Person.cs
public class Person { public virtual async Task<string> HelloAsync(string name) { Console.WriteLine("hello"+name); return "ok"; } public async Task<string> HelloFallBackAsync(string name) { Console.WriteLine("执行失败"+name); return "fail"; } }
目标:在执行 HelloAsync 失败的时候自动执行 HelloFallBackAsync ,达到熔断降级
编写HystrixCommandAttribute.cs
[AttributeUsage(AttributeTargets.Method)] public class HystrixCommandAttribute : AbstractInterceptorAttribute { public string FallBackMethod { get; set; } public HystrixCommandAttribute(string fallBackMethod) { this.FallBackMethod = fallBackMethod; } public override async Task Invoke(AspectContext context, AspectDelegate next) { try { await next(context);//执行被拦截的方法 } catch (Exception ex) { /* * context.ServiceMethod 被拦截的方法 * context.ServiceMethod.DeclaringType 被拦截的方法所在的类 * context.Implementation 实际执行的对象 * context.Parameters 方法参数值 * 如果执行失败,则执行FallBackMethod */ var fallBackMethod = context.ServiceMethod.DeclaringType.GetMethod(this.FallBackMethod); object fallBackResult = fallBackMethod.Invoke(context.Implementation, context.Parameters); context.ReturnValue = fallBackResult; await Task.FromResult(0); } } }
修改Person.cs类
public class Person { [HystrixCommand(nameof(HelloFallBackAsync))] public virtual async Task<string> HelloAsync(string name)//需要是虚方法 { Console.WriteLine("hello"+name); //抛错 String s = null; //s.ToString(); return "ok"; } public async Task<string> HelloFallBackAsync(string name) { Console.WriteLine("执行失败"+name); return "fail"; } [HystrixCommand(nameof(AddFall))] public virtual int Add(int i, int j) { //抛错 String s = null; //s.ToString(); return i + j; } public int AddFall(int i, int j) { return 0; } }
创建代理对象
ProxyGeneratorBuilder proxyGeneratorBuilder = new ProxyGeneratorBuilder(); using (IProxyGenerator proxyGenerator=proxyGeneratorBuilder.Build()) { Person p = proxyGenerator.CreateClassProxy<Person>(); Console.WriteLine(p.HelloAsync("Hello World").Result); Console.WriteLine(p.Add(1,2)); }
执行效果
异常执行效果
细化框架
重试: MaxRetryTimes表示最多重试几次,如果为0则不重试, RetrvIntervalMilliseconds表示重试间隔的豪秒数;
超时: TimeOutMilliseconds执行超过多少毫秒则认为超时(0表示不检测超时)缓存:缓存多少豪秒(0表示不缓存) ,用“类名+方法名+所有参数ToString拼接"做缓存Key.
新建控制台项目aspnetcorehystrix1,并添加AspectCore.Core、Polly包引用
Install-Package AspectCore.Core Install-Package Polly
Install-Package Microsoft.Extensions.Caching.Memory
编写HystrixCommandAttribute.cs
/// <summary> /// 熔断框架 /// </summary> [AttributeUsage(AttributeTargets.Method)] public class HystrixCommandAttribute : AbstractInterceptorAttribute { #region 属性 /// <summary> /// 最多重试几次,如果为0则不重试 /// </summary> public int MaxRetryTimes { get; set; } = 0; /// <summary> /// 重试间隔的毫秒数 /// </summary> public int RetryIntervalMilliseconds { get; set; } = 100; /// <summary> /// 是否启用熔断 /// </summary> public bool EnableCircuitBreater { get; set; } = false; /// <summary> /// 熔断前出现允许错误几次 /// </summary> public int ExceptionAllowedBeforeBreaking { get; set; } = 3; /// <summary> /// 熔断多长时间(毫秒 ) /// </summary> public int MillisecondsOfBreak { get; set; } = 1000; /// <summary> /// 执行超过多少毫秒则认为超时(0表示不检测超时) /// </summary> public int TimeOutMilliseconds { get; set; } = 0; /// <summary> /// 缓存多少毫秒(0表示不缓存),用“类名+方法名+所有参数ToString拼接”做缓存Key /// </summary> public int CacheTTLMilliseconds { get; set; } = 0; private Policy policy; //缓存 private static readonly Microsoft.Extensions.Caching.Memory.IMemoryCache memoryCache = new Microsoft.Extensions.Caching.Memory.MemoryCache(new Microsoft.Extensions.Caching.Memory.MemoryCacheOptions()); /// <summary> /// 降级方法名 /// </summary> public string FallBackMethod { get; set; } #endregion #region 构造函数 /// <summary> /// 熔断框架 /// </summary> /// <param name="fallBackMethod">降级方法名</param> public HystrixCommandAttribute(string fallBackMethod) { this.FallBackMethod = fallBackMethod; } #endregion public override async Task Invoke(AspectContext context, AspectDelegate next) { //一个HystrixCommand中保持一个policy对象即可 //其实主要是CircuitBreaker要求对于同一段代码要共享一个policy对象 //根据反射原理,同一个方法就对应一个HystrixCommandAttribute,无论几次调用, //而不同方法对应不同的HystrixCommandAttribute对象,天然的一个policy对象共享 //因为同一个方法共享一个policy,因此这个CircuitBreaker是针对所有请求的。 //Attribute也不会在运行时再去改变属性的值,共享同一个policy对象也没问题 lock (this) { if (policy==null) { policy = Policy.Handle<Exception>() .FallbackAsync(async (ctx, t) => { AspectContext aspectContext = (AspectContext)ctx["aspectContext"]; var fallBackMethod = context.ServiceMethod.DeclaringType.GetMethod(this.FallBackMethod); Object fallBackResult = fallBackMethod.Invoke(context.Implementation, context.Parameters); //不能如下这样,因为这是闭包相关,如果这样写第二次调用Invoke的时候context指向的 //还是第一次的对象,所以要通过Polly的上下文来传递AspectContext //context.ReturnValue = fallBackResult; aspectContext.ReturnValue = fallBackResult; }, async (ex, t) => { }); if (MaxRetryTimes>0)//重试 { policy = policy.WrapAsync(Policy.Handle<Exception>().WaitAndRetryAsync(MaxRetryTimes, i => TimeSpan.FromMilliseconds(RetryIntervalMilliseconds))); } if (EnableCircuitBreater)//熔断 { policy = policy.WrapAsync(Policy.Handle<Exception>().CircuitBreakerAsync(ExceptionAllowedBeforeBreaking, TimeSpan.FromMilliseconds(MillisecondsOfBreak))); } if (TimeOutMilliseconds>0)//超时 { policy = policy.WrapAsync(Policy.TimeoutAsync(() => TimeSpan.FromMilliseconds(TimeOutMilliseconds), Polly.Timeout.TimeoutStrategy.Pessimistic)); } } } //把本地调用的AspectContext传递给Polly,主要给FallBackMethod中使用,避免闭包的坑 Context pollyCtx = new Context(); pollyCtx["aspectContext"] = context; if (CacheTTLMilliseconds>0) { //用类名+方法名+参数的下划线连接起来作为缓存key string cacheKey = "HystrixMethodCacheManager_Key_" + context.ServiceMethod.DeclaringType + "." + context.ServiceMethod + string.Join("_", context.Parameters); //尝试去缓存中获取。如果找到了,则直接用缓存中的值做返回值 if (memoryCache.TryGetValue(cacheKey,out var cacheValue)) { context.ReturnValue = cacheValue; } else { //如果缓存中没有,则执行实际被拦截的方法 await policy.ExecuteAsync(ctx => next(context), pollyCtx); //存入缓存中 using (var cacheEntry=memoryCache.CreateEntry(cacheKey)) { cacheEntry.Value = context.ReturnValue; cacheEntry.AbsoluteExpiration = DateTime.Now + TimeSpan.FromMilliseconds(CacheTTLMilliseconds); } } } else//如果没有启用缓存,就直接执行业务方法 { await policy.ExecuteAsync(ctx => next(context), pollyCtx); } } }
编写业务类Person.cs
public class Person//需要public类 { [HystrixCommand(nameof(Hello1FallBackAsync), MaxRetryTimes = 3, EnableCircuitBreaker = true)] public virtual async Task<String> HelloAsync(string name)//需要是虚方法 { Console.WriteLine("hello" + name); #region 抛错 String s = null; s.ToString(); #endregion return "ok" + name; } [HystrixCommand(nameof(Hello2FallBackAsync))] public virtual async Task<string> Hello1FallBackAsync(string name) { Console.WriteLine("Hello降级1" + name); String s = null; s.ToString(); return "fail_1"; } public virtual async Task<string> Hello2FallBackAsync(string name) { Console.WriteLine("Hello降级2" + name); return "fail_2"; } [HystrixCommand(nameof(AddFall))] public virtual int Add(int i, int j) { String s = null; //s.ToString(); return i + j; } public int AddFall(int i, int j) { return 0; } [HystrixCommand(nameof(TestFallBack), CacheTTLMilliseconds = 3000)] public virtual void Test(int i) { Console.WriteLine("Test" + i); } public virtual void TestFallBack(int i) { Console.WriteLine("Test" + i); } }
创建代理对象
ProxyGeneratorBuilder proxyGeneratorBuilder = new ProxyGeneratorBuilder(); using (IProxyGenerator proxyGenerator=proxyGeneratorBuilder.Build()) { Person p = proxyGenerator.CreateClassProxy<Person>(); Console.WriteLine(p.HelloAsync("Hello World").Result); Console.WriteLine(p.Add(1, 2)); while (true) { Console.WriteLine(p.HelloAsync("Hello World").Result); Thread.Sleep(100); } }
测试结果:
正常:
一级熔断
二级熔断
结合asp.net core依赖注入
新建WebAPI项目aspnetcorehystrix,
并添加AspectCore.Core、Polly包引用
Install-Package AspectCore.Core
Install-Package Polly
Install-Package Microsoft.Extensions.Caching.Memory
编写HystrixCommandAttribute.cs
编写业务类Person.cs
public class Person//需要public类 { [HystrixCommand(nameof(HelloFallBackAsync))] public virtual async Task<string> HelloAsync(string name)//需要是虚方法 { Console.WriteLine("hello" + name); String s = null; s.ToString(); return "ok"; } public async Task<string> HelloFallBackAsync(string name) { Console.WriteLine("执行失败" + name); return "fail"; } [HystrixCommand(nameof(AddFall))] public virtual int Add(int i, int j) { String s = null; // s.ToArray(); return i + j; } public int AddFall(int i, int j) { return 0; } }
在asp.net core项目中,可以借助于asp.net core的依赖注入,简化代理类对象的注入,不用再自己调用ProxyGeneratorBuilder 进行代理类对象的注入了。
Install-Package AspectCore.Extensions.DependencyInjection
修改Startup.cs的ConfigureServices方法,把返回值从void改为IServiceProvider
public IServiceProvider ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddScoped<Person>(); return services.BuildAspectCoreServiceProvider(); }
其中services.AddSingleton<Person>();表 示 把Person注 入 。
BuildAspectCoreServiceProvider是让aspectcore接管注入。
升级一波
当然要通过反射扫描所有Service类,只要类中有标记了CustomInterceptorAttribute的方法都算作服务实现类。为了避免一下子扫描所有类,所以RegisterServices还是手动指定从哪个程序集中加载。
/// <summary> /// 根据特性批量注入 /// </summary> private static void RegisterServices(Assembly assembly, IServiceCollection services) { //遍历程序集中的所有public类型 foreach (Type type in assembly.GetExportedTypes()) { //判断类中是否有标注了CustomInterceptorAttribute的方法 bool hasHystrixCommandAttr= type.GetMethods().Any(m => m.GetCustomAttribute(typeof(HystrixCommandAttribute)) != null); if (hasHystrixCommandAttr) { services.AddSingleton(type); } } }
public IServiceProvider ConfigureServices(IServiceCollection services) { services.AddMvc(); RegisterServices(this.GetType().Assembly, services); return services.BuildAspectCoreServiceProvider(); }
Ocelot网关
现有微服务的几点不足:
1)对于在微服务体系中、和Consul通讯的微服务来讲,使用服务名即可访问。但是对于手机、web端等外部访问者仍然需要和N多服务器交互,需要记忆他们的服务器地址、端口号等。一旦内部发生修改,很麻烦,而且有时候内部服务器是不希望外界直接访问的。
2)各个业务系统的人无法自由的维护自己负责的服务器;
3)现有的微服务都是“我家大门常打开”,没有做权限校验。如果把权限校验代码写到每个微服务上,那么开发工作量太大。
4)很难做限流、收费等。
ocelot 中文文档:https://blog.csdn.net/sD7O95O/article/details/79623654
资料:http://www.csharpkit.com/apigateway.html
腾讯.Net大队长“张善友”是项目主力开发人员之一。
先搞两个短信、邮件假的服务器(这里用WebAPI代替)
新建 smsservice1 WebAPI项目,并创建SMSController.cs
[Route("api/[Controller]")] public class SMSController : Controller { [Route("Send")] public bool Send(string msg) { Console.WriteLine("发送短信"+msg); return true; } }
新建 emailservice1 WebAPI项目,并创建EmailController.cs
[Route("api/[controller]")] public class EmailController : Controller { [Route("Send")] public bool Send(string msg) { Console.WriteLine("发送邮件" + msg); return true; } }
Ocelot基本配置
Ocelot就是一个提供了请求路由、安全验证等功能的API网关微服务。
建一个 ocelotserver1 WebAPI项目,然后把默认生成的Controller删除,添加 Ocelot Nuget包引用
Install-Package Ocelot
项目根目录下创建configuration.json
{ "ReRoutes": [ { "DownstreamPathTemplate": "/api/sms/{url}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 5001 } ], "UpstreamPathTemplate": "/sms/{url}", "UpstreamHttpMethod": [ "Get", "Post" ], "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10, "TimeoutValue": 5000 } }, { "DownstreamPathTemplate": "/api/email/{url}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 5002 } ], "UpstreamPathTemplate": "/youjian/{url}", "UpstreamHttpMethod": [ "Get", "Post" ], "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10, "TimeoutValue": 5000 } } ] }
修改Program.cs
public static IWebHost BuildWebHost(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .ConfigureAppConfiguration(conf => { conf.AddJsonFile("configuration.json", optional: false, reloadOnChange: true); }) .Build();
修改Startup.cs
public void ConfigureServices(IServiceCollection services) { //services.AddMvc(); services.AddOcelot(Configuration); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } //app.UseMvc(); app.UseOcelot().Wait();//不要忘了写Wait }
然后将smsservice1与emailservice1以环境变量的方式启动(这里用cmd启动)
set ASPNETCORE_URLS=http://127.0.0.1:5001 dotnet smsservice1.dll set ASPNETCORE_URLS=http://127.0.0.1:5002 dotnet emailservice1.dll
注意:powershell和cmd启动方式不同
# Unix:
ASPNETCORE_URLS="https://*:5123" dotnet run
# Windows PowerShell:
$env:ASPNETCORE_URLS="https://*:5123" ; dotnet run
# Windows CMD (note: no quotes):
SET ASPNETCORE_URLS=https://*:5123 && dotnet run
接下来启动ocelotserver1
然后访问http://127.0.0.1:5000/youjian/Send?msg=aaa的时候就会访问http://127.0.0.1:5002/api/email/Send?msg=aaa
Ocelot+Consul
上面的配置还是把服务的ip地址写死了,Ocelot可以和Consul通讯,通过服务名字来配置。
准备Consul
我们首先先启动Consul
consul.exe agent -dev
我们可以新建一个 smsservice2 WebAPI用来测试,然后添加Consul引用
Install-Package Consul
然后新建SMSController控制器
[Route("api/[Controller]")] public class SMSController : Controller { [Route("Send")] public bool Send(string msg) { Console.WriteLine("发送短信" + msg); return true; } }
添加健康检查HealthController控制器
[Route("api/[controller]")] public class HealthController : Controller { [HttpGet] public IActionResult Get() { return Ok("ok"); } }
修改Program.cs来设置启动的IP与端口号
public static IWebHost BuildWebHost(string[] args) { var config = new ConfigurationBuilder() .AddCommandLine(args) .Build(); String ip = config["ip"]; String port = config["port"]; return WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .UseUrls($"http://{ip}:{port}") .Build(); }
然后在Startup.cs进行Consul注册
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); String ip = Configuration["ip"];//部署到不同服务器的时候不能写成127.0.0.1或者0.0.0.0,因为这是让服务消费者调用的地址 Int32 port = Int32.Parse(Configuration["port"]); //向consul注册服务 ConsulClient client = new ConsulClient(config=>config.Address= new Uri("http://127.0.0.1:8500")); Task<WriteResult> result = client.Agent.ServiceRegister(new AgentServiceRegistration() { ID = "daunxin2" + Guid.NewGuid(),//服务编号,不能重复,用Guid最简单 Name = "daunxin2",//服务的名字 Address = ip,//我的ip地址(可以被其他应用访问的地址,本地测试可以用127.0.0.1,机房环境中一定要写自己的内网ip地址) Port = port,//我的端口 Check = new AgentServiceCheck() { DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//服务停止多久后反注册 Interval = TimeSpan.FromSeconds(10),//健康检查时间间隔,或者称为心跳间隔 HTTP = $"http://{ip}:{port}/api/health",//健康检查地址, Timeout = TimeSpan.FromSeconds(5) } }); }
分别启动两个实例5001和5002
dotnet smsservice2.dll --ip 127.0.0.1 --port 5001 dotnet smsservice2.dll --ip 127.0.0.1 --port 5002
准备Ocelot
创建新的 ocelotserver2 WebAPI项目然后把默认生成的Controller删除,添加 Ocelot Nuget包引用
Install-Package Ocelot
项目根目录下创建configuration.json
{ "ReRoutes": [ { "DownstreamPathTemplate": "/api/sms/{url}", "DownstreamScheme": "http", "UpstreamPathTemplate": "/daunxin/{url}", "UpstreamHttpMethod": [ "Get" ], "ServiceName": "duanxin2", "LoadBalancerOptions": "LeastConnection", "UseServiceDiscovery": true } ], "GlobalConfiguration": { "ServiceDiscoveryProvider": { "Host": "localhost", "Port": 8500 } } }
修改Program.cs
public static IWebHost BuildWebHost(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .ConfigureAppConfiguration(conf => { conf.AddJsonFile("configuration.json", optional: false, reloadOnChange: true); }) .Build();
修改Startup.cs
public void ConfigureServices(IServiceCollection services) { //services.AddMvc(); services.AddOcelot(Configuration); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } //app.UseMvc(); app.UseOcelot().Wait();//不要忘了写Wait }
接下来启动ocelotserver2,用postman进行调试
dotnet ocelotserver2.dll
访问http://localhost:5000/daunxin/send?msg=hello即可
表示只要是/daunxin/开头的(http://localhost:5000/daunxin/send?msg=hello等)都会转给后端的服务名为"duanxin2"的一台服务器,转发的路径是"/{url}"。
"LoadBalancer":"LeastConnection"表示负载均衡算法是“最少连接数”,如果改为RoundRobin就是“轮询”。
ServiceDiscoveryProvider是Consul服务器的配置。
"UpstreamHttpMethod":["Get"]表示只转发Get请求,可以添加"Post"等。
(*)也支持Eureka进行服务的注册、查找(http://ocelot.readthedocs.io/en/latest/features/servicediscovery.html),也支持访问Service Fabric中的服务(http://ocelot.readthedocs.io/en/latest/features/servicefabric.html)。
限流
官方文档地址:http://ocelot.readthedocs.io/en/latest/features/ratelimiting.html
要配置到每个路由规则上
参数说明:
"RateLimitOptions": { "ClientWhitelist": [], //不受限制的白名单 "EnableRateLimiting": true, //启用限流 "Period": "30s", //统计时间段:1s、1m、1h、1d "PeriodTimespan": 10, //一旦碰到一次“超限”,多少秒后重新记数可以重新请求。 "Limit": 5 //指定时间段内最多请求次数 }
我们打开上面的 ocelotserver2 对其配置文件configuration.json进行修改(增加限流配置):
{ "ReRoutes": [ { "DownstreamPathTemplate": "/api/sms/{url}", "DownstreamScheme": "http", "UpstreamPathTemplate": "/daunxin/{url}", "UpstreamHttpMethod": [ "Get" ], "ServiceName": "duanxin2", "LoadBalancerOptions": "RoundRobin", "UseServiceDiscovery": true, "RateLimitOptions": { "ClientWhitelist": [], //不受限制的白名单 "EnableRateLimiting": true, //启用限流 "Period": "30s", //统计时间段:1s、1m、1h、1d "PeriodTimespan": 10, //一旦碰到一次“超限”,多少秒后重新记数可以重新请求。 "Limit": 5 //指定时间段内最多请求次数 } } ], "GlobalConfiguration": { "ServiceDiscoveryProvider": { "Host": "localhost", "Port": 8500 } } }
然后重启ocelotserver2服务
dotnet ocelotserver2.dll
访问http://localhost:5000/daunxin/send?msg=hello即可,我们连续访问5+次
如果要实现自定义的限流规则,比如不同级别用户的限速方式不一样,就要自己写MiddleWare。
简单的请求缓存
官方地址:http://ocelot.readthedocs.io/en/latest/features/caching.html
只支持get,Region是用来调用api手动清理缓存用的。只要url不变,就会缓存。可以这样测试:
public string Get(int id) { return "value" + id + DateTime.Now; }
QOS(熔断器)
官方文档:http://ocelot.readthedocs.io/en/latest/features/qualityofservice.html
Ocelot给后端服务器传数据
修改 ocelotserver2 建立中间件,写到Startup.cs的Configure:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } var configuration = new OcelotPipelineConfiguration { PreErrorResponderMiddleware = async (ctx, next) => { //String token = ctx.HttpContext.Request.Headers["token"].FirstOrDefault();//这里可以进行接收的客户端token解析转发 ctx.HttpContext.Request.Headers.Add("X-Hello", "666"); await next.Invoke(); } }; //app.UseMvc(); //app.UseOcelot().Wait();//不要忘了写Wait app.UseOcelot(configuration).Wait(); }
修改 smsservice2 的SMSController进行接收header:
[Route("api/[Controller]")] public class SMSController : Controller { [Route("Send")] public bool Send(string msg) { string value = Request.Headers["X-Hello"]; Console.WriteLine($"x-hello={value}"); Console.WriteLine("发送短信" + msg); return true; } }
重启smsservice2与ocelotserver2。测试结果:
JWT+Ocelot验证
JWT算法简介
内部Restful接口可以“我家大门常打开”,但是如果要给app等使用的接口,则需要做权限校验,不能谁都随便调用。
最基本的检查就是“登录之后才能调用,而且只能调用自己有权限调用的接口”。
Restful接口不是web网站,App中很难直接处理SessionId,而且Cookie有跨域访问的限制,所以一般不能直接用后端Web框架内置的Session机制。但是可以用类似Session的机制,用户登录之后返回一个类似SessionId的东西,服务器端把SessionId和用户的信息对应关系保存到Redis等地方,客户端把SessionId保存起来,以后每次请求的时候都带着这个SessionId。
用类似Session这种机制的坏处:需要集中的Session机制服务器;不可以在nginx、CDN等静态文件处理服务器上校验权限;每次都要根据SessionId去Redis服务器获取用户信息,效率低;JWT(Json Web Token)是现在流行的一种对Restful接口进行验证的机制。
JWT的特点:把用户信息放到一个JWT字符串中,用户信息部分是明文的,再加上一部分签名区域,签名部分是服务器对于“明文部分+秘钥”加密的,这个加密信息只有服务器端才能解析。用户端只是存储、转发这个JWT字符串。如果客户端篡改了明文部分,那么服务器端解密时候会报错。
JWT由三块组成,可以把用户名、用户Id等保存到Payload部分
注意Payload和Header部分都是Base64编码,可以轻松的Base64解码回来。因此Payload部分约等于是明文的,因此不能在Payload中保存不能让别人看到的机密信息。虽然说Payload部分约等于是明文的,但是不用担心Payload被篡改,因为Signature部分是根据header+payload+secretKey进行加密算出来的,如果Payload被篡改,就可以根据Signature解密时候校验。
用JWT做权限验证的好处:无状态,更有利于分布式系统,不需要集中的Session机制服务器;可以在nginx、CDN等静态文件处理服务器上校验权限;获取用户信息直接从JWT中就可以读取,效率高;
.Net中使用JWT算法
新建 jwttest1 控制台项目,添加 jwt 包引用
Install-Package jwt
加密
var payload = new Dictionary<string, object> { { "UserId", 123 }, { "UserName", "admin" } }; var secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";//不要泄露(这是服务器端秘钥) IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); IJsonSerializer serializer = new JsonNetSerializer(); IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder); var token = encoder.Encode(payload, secret); Console.WriteLine(token);
解密
var token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJVc2VySWQiOjEyMywiVXNlck5hbWUiOiJhZG1pbiJ9.Qjw1epD5P6p4Yy2yju3-fkq28PddznqRj3ESfALQy_U"; var secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk"; try { IJsonSerializer serializer = new JsonNetSerializer(); IDateTimeProvider provider = new UtcDateTimeProvider(); IJwtValidator validator = new JwtValidator(serializer, provider); IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder); var json = decoder.Decode(token, secret, verify: true); Console.WriteLine(json); } catch (FormatException) { Console.WriteLine("Token format invalid"); } catch (TokenExpiredException) { Console.WriteLine("Token has expired"); } catch (SignatureVerificationException) { Console.WriteLine("Token has invalid signature"); }
过期时间
在payload中增加一个名字为exp的值,值为过期时间和1970 / 1 / 1 00:00:00 相差的秒数
使用JWT实现Ocelot的验证
搭建token颁发服务器
新建WebAPI项目 JWTTokenServer1 并添加JWT引用
Install-Package jwt
新建通用返回类 APIResult.cs
public class APIResult<T> { public int Code { get; set; } public T Data { get; set; } public String Message { get; set; } }
新建Api控制器 AuthController
[Route("api/[Controller]")] public class AuthController : Controller { [HttpGet] [Route(nameof(RequestToken))] public APIResult<string> RequestToken(string userName, string password) { APIResult<string> result = new APIResult<string>(); if (userName == "wyt" && password == "123")//todo:连数据库 { var payload = new Dictionary<string, object> { { "UserName", userName }, { "UserId", 666 } }; var secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";//不要泄露 IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); IJsonSerializer serializer = new JsonNetSerializer(); IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder); var token = encoder.Encode(payload, secret); result.Code = 0; result.Data = token; } else { result.Code = -1; result.Message = "username or password error"; } return result; } }
以5001端口启动,用postman进行测试
set ASPNETCORE_URLS=http://127.0.0.1:5001
正确回复:
Ocelot配置
新建WebAPI项目 calcservice3 作为业务服务器
将项目以环境变量方式启动
set ASPNETCORE_URLS=http://127.0.0.1:5002
新建WebAPI项目 ocelotserver3 作为Ocelot服务器,添加 Ocelot Nuget包引用
Install-Package Ocelot
项目根目录下创建configuration.json
/* 认证服务器 5001端口 业务服务器 5002端口 Oclot服务器 5000端口 */ { "ReRoutes": [ { "DownstreamPathTemplate": "/{url}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 5001 } ], "UpstreamPathTemplate": "/auth/{url}", "UpstreamHttpMethod": [ "Get", "Post" ], "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10, "TimeoutValue": 5000 } }, { "DownstreamPathTemplate": "/{url}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 5002 } ], "UpstreamPathTemplate": "/calc/{url}", "UpstreamHttpMethod": [ "Get", "Post" ], "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10, "TimeoutValue": 5000 } } ] }
如果认证服务器注册到Consul,这里也可以按照服务名的方式注册
修改Program.cs
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureAppConfiguration(conf =>
{
conf.AddJsonFile("configuration.json", optional: false, reloadOnChange: true);
})
.Build();
修改Startup.cs
public void ConfigureServices(IServiceCollection services) { //services.AddMvc(); services.AddOcelot(Configuration); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } //app.UseMvc(); app.UseOcelot().Wait();//不要忘了写Wait }
然后将ocelotserver3以环境变量的方式启动(这里用cmd启动)
set ASPNETCORE_URLS=http://127.0.0.1:5000
然后分别通过ocelotserver3访问认证服务器和业务服务器
Ocelot中间件验证token合法性
在中 ocelotserver3 中添加jwt引用
Install-Package jwt
修改Startup.cs中的Configure方法,插入中间件。在后端服务器中就可以从请求图中读取"X-UserName"获取登录用户名
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } var configuration = new OcelotPipelineConfiguration { PreErrorResponderMiddleware = async (ctx, next) => { if (!ctx.HttpContext.Request.Path.Value.StartsWith("/auth"))//不以auth开头的一律校验 { String token = ctx.HttpContext.Request.Headers["token"].FirstOrDefault(); if (string.IsNullOrWhiteSpace(token)) { ctx.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; using (StreamWriter writer = new StreamWriter(ctx.HttpContext.Response.Body)) { writer.Write("token required"); } return; } var secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk"; try { IJsonSerializer serializer = new JsonNetSerializer(); IDateTimeProvider provider = new UtcDateTimeProvider(); IJwtValidator validator = new JwtValidator(serializer, provider); IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder); var json = decoder.Decode(token, secret, verify: true); Console.WriteLine(json); dynamic payload = JsonConvert.DeserializeObject<dynamic>(json); string userName = payload.UserName; ctx.HttpContext.Request.Headers.Add("X-UserName", userName);//将解析出来的用户名传输给后端服务器。 } catch (TokenExpiredException) { ctx.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; using (StreamWriter writer = new StreamWriter(ctx.HttpContext.Response.Body)) { writer.Write("Token has expired"); } } catch (SignatureVerificationException) { ctx.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; using (StreamWriter writer = new StreamWriter(ctx.HttpContext.Response.Body)) { writer.Write("Token has invalid signature"); } } } await next.Invoke(); } }; //app.UseMvc(); //app.UseOcelot().Wait();//不要忘了写Wait app.UseOcelot(configuration).Wait();//不要忘了写Wait }
测试
访问 http://127.0.0.1:5000/calc/api/values
访问http://localhost:5000/auth/api/auth/RequestToken?userName=wyt&password=123获取token
使用token访问http://127.0.0.1:5000/calc/api/values
篡改token后进行访问http://127.0.0.1:5000/calc/api/values
Ocelot+Identity Server
实际做项目的时候接口安全没必要自己写,可以推荐用identity server简化开发。
搭建identity server认证服务器
新建一个空的WebAPI项目 ID4.IdServer
Install-Package IdentityServer4
首先编写一个提供应用列表、账号列表的Config类
public class Config { /// <summary> /// 返回应用列表 /// </summary> /// <returns></returns> public static IEnumerable<ApiResource> GetApiResources() { List<ApiResource> resources = new List<ApiResource>(); //ApiResource第一个参数是应用的名字,第二个参数是显示名字 resources.Add(new ApiResource("chatapi", "我的聊天软件")); resources.Add(new ApiResource("rpandroidapp", "安卓app")); resources.Add(new ApiResource("bdxcx", "百度小程序")); return resources; } /// <summary> /// 返回账号列表 /// </summary> /// <returns></returns> public static IEnumerable<Client> GetClients() { List<Client> clients = new List<Client>(); clients.Add(new Client { ClientId = "wyt",//用户名 AllowedGrantTypes = GrantTypes.ClientCredentials, ClientSecrets = { new Secret("123321".Sha256())//秘钥 }, AllowedScopes = { "chatapi", "rpandroidapp" }//这个账号支持访问哪些应用 }); return clients; } }
如果允许在数据库中配置账号等信息,那么可以从数据库中读取然后返回这些内容。
修改 Startup.cs
public void ConfigureServices(IServiceCollection services) { services.AddIdentityServer() .AddDeveloperSigningCredential() .AddInMemoryApiResources(Config.GetApiResources()) .AddInMemoryClients(Config.GetClients()); //services.AddMvc(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { //if (env.IsDevelopment()) //{ // app.UseDeveloperExceptionPage(); //} //app.UseMvc(); app.UseIdentityServer(); }
然后修改Program.cs在9500端口启动
public static IWebHost BuildWebHost(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .UseUrls("http://127.0.0.1:9500") .Build();
在postman里发出请求,获取token
http://localhost:9500/connect/token,发Post请求,表单请求内容(注意不是报文头):client_id=wyt client_secret=123321 grant_type=client_credentials
把返回的access_token留下来后面用(注意有有效期)。
搭建Ocelot服务器项目
新建WebAPI项目 calcservice3 作为业务服务器
将项目以环境变量方式启动
set ASPNETCORE_URLS=http://127.0.0.1:5002
新建WebAPI项目 ocelot_id4server ,并安装Ocelot包
Install-Package Ocelot
编写配置文件Ocelot.json
{ "ReRoutes": [ { "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 5002 } ], "DownstreamPathTemplate": "/{url}", "UpstreamPathTemplate": "/chat1/{url}", "UpstreamHttpMethod": [ "Get","Post" ], "ReRouteIsCaseSensitive": false, "DownstreamScheme": "http", "AuthenticationOptions": { "AuthenticationProviderKey": "ChatKey", "AllowedScopes": [] } } ] }
把/chat1访问的都转给http:// localhost:5002这个后端服务器。
Program.cs中加载Ocelot.json
public static IWebHost BuildWebHost(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .ConfigureAppConfiguration((hostingContext, builder) => { builder.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) .AddJsonFile("Ocelot.json").AddEnvironmentVariables(); }) .Build();
修改Startup.cs
public void ConfigureServices(IServiceCollection services) { //services.AddMvc(); services.AddAuthentication()//对配置文件中使用ChatKey配置了AuthenticationProviderKey=ChatKey的路由规则使用如下的验证方式 .AddIdentityServerAuthentication("ChatKey", o=> {//IdentityService认证服务的地址 o.Authority = "http://127.0.0.1:9500";//!!!!!!!!!!!!!!!!!(切记,这里不可用localhost) o.ApiName = "chatapi";//要连接的应用的名字 o.RequireHttpsMetadata = false; o.SupportedTokens = SupportedTokens.Both; o.ApiSecret = "123321";//秘钥 }); services.AddOcelot(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { //if (env.IsDevelopment()) //{ // app.UseDeveloperExceptionPage(); //} //app.UseMvc(); app.UseOcelot().Wait(); ` }
9500端口启动认证服务器
10000端口启动Ocelot服务器http://localhost:10000/chat1/api/values/1
在请求头(不是报文体)里加上:Authorization="Bearer "+上面identityserver返回的accesstoken
Thrift高效通讯
什么是RPC
Restful采用Http进行通讯,优点是开放、标准、简单、兼容性升级容易;缺点是性能略低。在QPS高(QPS(Query Per Second)每秒查询率)或者对响应时间要求苛刻的服务上,可以用RPC(Remote Procedure Call)—远程过程调用,RPC由于采用二进制传输、TCP通讯,所以通常性能更好。
.Net Core下的RPC(远程方法调用)框架有gRPC、Thrift等,可以类比.Net Framework下的.Net Remoting、WCF(TCP Binding)。gRPC、Thrift等都支持主流的编程语言。性能:Thirft(大约10倍)>gRPC>Http。数据汇总自网上,自己没测,因为性能和业务数据的特点有关,不谈业务场景、业务数据的性能测试都是“仅供参考”。并不是gRPC,并不是Http不好,没有绝对的好与坏。
RPC虽然效率略高,但是耦合性强,如果兼容性处理不好的话,一旦服务器端接口升级,客户端就要更新,即使是增加一个参数,而rest则比较灵活。
最佳实践:对内一些性能要求高的场合用RPC,对内其他场合以及对外用Rest。比如web服务器和视频转码服务器之间通讯可以用restful就够了,转账接口用RPC性能会更高
Thrift基本使用
参考资料:https://www.cnblogs.com/focus-lei/p/8889389.html
1、下载thrift http://thrift.apache.org/
把thrift-***.exe解压到磁盘,改名为thrift.exe(用起来方便一些)
2、编写一个UserService.thrift文件(IDL(中间定义语言))
namespace csharp ThriftTest1.Contract service UserService{ SaveResult Save(1:User user) User Get(1:i32 id) list<User> GetAll() } enum SaveResult { SUCCESS = 0, FAILED = 1, } struct User { 1: required i64 Id; 2: required string Name; 3: required i32 Age; 4: optional bool IsVIP; 5: optional string Remark; }
service定义的是服务类,enum是枚举,struct是传入或者传出的复杂数据类型(支持对象级联)。
语法规范http://thrift.apache.org/docs/idl
根据thrift语法生成C#代码
thrift.exe -gen csharp UserService.thrift
创建一个类库项目 ThriftTest1.Contract,作为客户端和服务器之间的共用协议,把上一步生成的代码放进项目。
项目nuget安装apache-thrift-netcore:
Install-Package apache-thrift-netcore
然后将生成的文件拷贝到项目中,并重新生成项目
创建服务器端项目 ThriftTest1.Server,建一个控制台项目(放到 web 项目中或者在 Linux中用守护进程运行起来(SuperVisor等,类似Windows下的“Windows服务”)也可以)。
ThriftTest1.Server项目引用ThriftTest1.Contract
编写实现类UserServiceImpl.cs:
public class UserServiceImpl : UserService.Iface { public User Get(int id) { User u = new User(); u.Id = id; u.Name = "用户" + id; u.Age = 6; return u; } public List<User> GetAll() { List<User> list = new List<User>(); list.Add(new User { Id = 1, Name = "wyt", Age = 18, Remark = "hello" }); list.Add(new User { Id = 2, Name = "wyt2", Age = 6 }); return list; } public SaveResult Save(User user) { Console.WriteLine($"保存用户,{user.Id}"); return SaveResult.SUCCESS; } }
修改Program.cs
class Program { static void Main(string[] args) { TServerTransport transport = new TServerSocket(8800); var processor = new ThriftTest1.Contract.UserService.Processor(new UserServiceImpl()); TServer server = new TThreadPoolServer(processor, transport); server.Serve(); Console.WriteLine("Hello World!"); } }
创建客户端项目 ThriftTest1.Client,建一个控制台项目(放到 web 项目中或者在 Linux中用守护进程运行起来(SuperVisor等,类似Windows下的“Windows服务”)也可以)。
ThriftTest1.Server项目引用ThriftTest1.Contract
修改Program.cs
class Program { static void Main(string[] args) { using (TTransport transport = new TSocket("localhost", 8800)) using (TProtocol protocol = new TBinaryProtocol(transport)) using (var clientUser = new UserService.Client(protocol)) { transport.Open(); User u = clientUser.Get(1); Console.WriteLine($"{u.Id},{u.Name}"); } Console.ReadKey(); } }
分别启动:
一个服务器中放多个服务
0.9.1之前只支持一个服务器一个服务,这也是建议的做法。之后支持多路服务在thrift中增加一个服务
service CalcService{ i32 Add(1:i32 i1,2:i32 i2) }
服务器:
新增实现类CalcServiceImpl.cs
public class CalcServiceImpl : CalcService.Iface { public int Add(int i1, int i2) { return i1 + i2; } }
修改Program.cs
class Program { static void Main(string[] args) { TServerTransport transport = new TServerSocket(8800); var processorUserService = new ThriftTest1.Contract.UserService.Processor(new UserServiceImpl()); var processorCalcService = new ThriftTest1.Contract.CalcService.Processor(new CalcServiceImpl()); var processorMulti = new TMultiplexedProcessor(); processorMulti.RegisterProcessor("userService", processorUserService); processorMulti.RegisterProcessor("calcService", processorCalcService); TServer server = new TThreadPoolServer(processorMulti, transport); server.Serve(); Console.WriteLine("Hello World!"); } }
客户端:
修改Program.cs
class Program { static void Main(string[] args) { using (TTransport transport = new TSocket("localhost", 8800)) using (TProtocol protocol = new TBinaryProtocol(transport)) using (var protocolUserService = new TMultiplexedProtocol(protocol,"userService")) using (var clientUser = new UserService.Client(protocolUserService)) using (var protocolCalcService = new TMultiplexedProtocol(protocol,"calcService")) using (var clientCalc = new CalcService.Client(protocolCalcService)) { transport.Open(); User u = clientUser.Get(1); Console.WriteLine($"{u.Id},{u.Name}"); Console.WriteLine(clientCalc.Add(1, 2)); } Console.ReadKey(); } }
分别启动:
https://www.cnblogs.com/focus-lei/p/8889389.html
(*)新版:thrift.exe -gen netcore UserService.thrift
貌似支持还不完善(http://www.cnblogs.com/zhaiyf/p/8351361.html )还不能用,编译也有问题,值得期待的是:支持异步。
Java 等其他语言的融入
和使用Restful做服务一样,Java也可以调用、也可以做Thrift服务,演示一下java调用c#写的Thrift服务的例子
Java编译器版本需要>=1.6
Maven(thrift maven版本一定要和生成代码的thrift的版本一致):
<dependency> <groupId>org.apache.thrift</groupId> <artifactId>libthrift</artifactId> <version>0.11.0</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.5</version> </dependency>
在thrift的IDL文件中加入一行(各个语言的namespace等参数可以共存)
namespace java com.rupeng.thriftTest1.contract 就可以控制生成的java类的报名,最好按照java的命名规范来。
thrift.exe -gen java UserService.thrift
产生java代码
Java代码:
import org.apache.thrift.protocol.TBinaryProtocol; import org.apache.thrift.protocol.TProtocol; import org.apache.thrift.transport.TSocket; import org.apache.thrift.transport.TTransport; public class Main { public static void main(String[] args) throws Exception { System.out.println("客户端启动...."); TTransport transport = new TSocket("localhost", 8800, 30000); TProtocol protocol = new TBinaryProtocol(transport); UserService.Client client = new UserService.Client(protocol); transport.open(); User result = client.Get(1); System.out.println(result.getAge()+result.getName()+result.getRemark()); } }
也可以用Java写服务器,C#调用。当然别的语言也可以。
接口设计原则“API design is like sex: Make one mistake and support it for the rest of your life”
Thrift+Consul 服务发现
注册和发现和Rest方式没有什么区别。
consul支持tcp健康监测:https://www.consul.io/docs/agent/checks.html
因为 Thrift 一般不对外,所以一般不涉及和 API 网关结合的问题
不是所有项目都适合微服务架构,互联网项目及结构复杂的企业信息系统才可以考虑微服务架构。
设计微服务架构,模块拆分的原则:可以独立运行,尽量服务间不要依赖,即使依赖层级也不要太深,不要想着还要 join。按业务划分、按模块划分。
扩展知识
1、 分布式跟踪、日志服务、监控等对微服务来说非常重要
2、 gRPC 另外一个 RPC 框架,gRPC 的.Net Core 支持异步。
3、 https://github.com/neuecc/MagicOnion 可以参考下这位日本 mvp 写的 grpc 封装,不需要定义接口文件。
4、 nanofabric https://github.com/geffzhang/NanoFabric 简单分析
5、 Surging https://github.com/dotnetcore/surging
6、 service fabric https://azure.microsoft.com/zh-cn/documentation/learning-paths/service-fabric/
7、 Spring Cloud 入门视频:http://www.rupeng.com/Courses/Chapter/755
8、 steeltoe http://steeltoe.io/
9、 限流算法 https://mp.weixin.qq.com/s/bck0Q2lDj_J9pLhFEhqm9w
10、https://github.com/PolicyServer/PolicyServer.Local 认证 + 授权 是两个服务, identityserver 解决了认证 ,PolicyServer 解决授权
11、CSharpKit 微服务工具包 http://www.csharpkit.com/
12、如鹏网.Net 提高班 http://www.rupeng.com