Asp.Net Core微服务再体验

Stella981
• 阅读 705

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

Asp.Net Core微服务再体验

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();
}

Asp.Net Core微服务再体验

然后启动的时候:

dotnet WebApplication5.dll--ip 127.0.0.1-port 8889

.Net Core因为跨平台,所以可以不依赖于IIS运行了。可以用.Net Core内置的kestrel服务器运行网站,当然真正面对终端用户访问的时候一般通过Nginx等做反向代理

WebAPI基础

编写几个有意义的接口,多搞几个不同的服务,包含 get、post,方便后面测试、演示。

调试 WebAPI 项目的时候把项目属性中的【启动浏览器】勾掉,这样就不会启动浏览器了,讲解使用 PostMan 调试 Http 接口。

WebAPI 就是 Restful 风格,请求响应都最好是 json 格式,虽然请求也可以是表单格式,但是最好都用 json 格式请求( contenttype=application/json )的方法体:

{phoneNum:"110",msg:"aaaaaaaaaaaaa"},因此这里只讲 json 格式请求的方法。

和.Net Framework 中的 WebAPI 不一样,如果[HttpPost]、[HttpGet]等标记不加参数,则表示匹配“没有 Action”,比如 http://localhost:5000/api/SMS/。如果指定\[HttpPost("Send\_MI")\],则匹配 Action 的名字,比如 http://localhost:5000/api/SMS/ Send_MI。如果方法名字和 Action 名字一样,建议用 nameof。参数:

1)   正确:public void Send_MI(dynamic model)

2)   正确:public void Send_HW(SendSMSRequest model)

3)   错误:public void Send_LX(string phoneNum,string msg)

新建解决方案  MS2V 

信息服务WebAPI项目  MsgService  新增两个控制器

EmailController.cs:

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

[Route("api/[Controller]")]
public class EmailController : ControllerBase
{
    [HttpPost(nameof(Send_QQ))]
    public void Send_QQ(SendEmailRequest model)
    {
        Console.WriteLine($"通过QQ邮件接口向{model.Email}发送邮件,标题{model.Title},内容:{model.Body}");
    }

    [HttpPost(nameof(Send_163))]
    public void Send_163(SendEmailRequest model)
    {
        Console.WriteLine($"通过网易邮件接口向{model.Email}发送邮件,标题{model.Title},内容:{model.Body}");
    }

    [HttpPost(nameof(Send_Sohu))]
    public void Send_Sohu(SendEmailRequest model)
    {
        Console.WriteLine($"通过Sohu邮件接口向{model.Email}发送邮件,标题{model.Title},内容:{model.Body}");
    }
}

public class SendEmailRequest
{
    public string Email { get; set; }
    public string Title { get; set; }
    public string Body { get; set; }
}

View Code

SMSController.cs:

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

[Route("api/[Controller]")]
public class SMSController : ControllerBase
{
    //发请求,报文体为{phoneNum:"110",msg:"aaaaaaaaaaaaa"},
    [HttpPost(nameof(Send_MI))]
    public void Send_MI(dynamic model)
    {
        Console.WriteLine($"通过小米短信接口向{model.phoneNum}发送短信{model.msg}");
    }

    [HttpPost(nameof(Send_LX))]
    public void Send_LX(SendSMSRequest model)
    {
        Console.WriteLine($"通过联想短信接口向{model.PhoneNum}发送短信{model.Msg}");
    }

    [HttpPost(nameof(Send_HW))]
    public void Send_HW(SendSMSRequest model)
    {
        Console.WriteLine($"通过华为短信接口向{model.PhoneNum}发送短信{model.Msg}");
    }
}

public class SendSMSRequest
{
    public string PhoneNum { get; set; }
    public string Msg { get; set; }
}

View Code

产品信息服务 WebAPI 项目  ProductService 

新增类文件Product.cs:

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

public class Product
{
    public long Id { get; set; }
    public string Name { get; set; }
    public double Price { get; set; }
    public string Description { get; set; }
}

View Code

新增控制器ProductController.cs:

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

[Route("api/[Controller]")]
public class ProductController : ControllerBase
{
    //这显然是为了demo,这样放到内存中不能“集群”
    private static List<Product> products = new List<Product>();
    static ProductController()
    {
        products.Add(new Product { Id = 1, Name = "T430笔记本", Price = 8888, Description = "CPU i7标压版,1T硬盘" });
        products.Add(new Product { Id = 2, Name = "华为Mate10", Price = 3888, Description = "大猩猩屏幕,多点触摸" });
        products.Add(new Product { Id = 3, Name = "天梭手表", Price = 9888, Description = "瑞士经典款,可好了" });
    }

    [HttpGet]
    public IEnumerable<Product> Get()
    {
        //string name = this.User.Identity.Name;//读取的就是"Name"这个特殊的Claims的值
        //string userId = this.User.FindFirst("UserId").Value;
        //string realName = this.User.FindFirst("RealName").Value;
        //string email = this.User.FindFirst("Email").Value;
        //Console.WriteLine($"name={name},userId={userId},realName={realName},email={email}");

        //System.Console.WriteLine("Get请求过来了" + DateTime.Now);
        return products;
    }

    [HttpGet("{id}")]
    public Product Get(int id)
    {
        var product = products.SingleOrDefault(p => p.Id == id);
        if (product == null)
        {
            Response.StatusCode = 404;
        }
        return product;
    }

    [HttpPost]
    public void Add(Product model)
    {
        if (products.Any(p => p.Id == model.Id))
        {
            Response.StatusCode = (int)HttpStatusCode.Conflict;//通过状态码而非响应体报错,是restful风格
            return;
        }
        products.Add(model);
    }

    [HttpDelete("{id}")]
    public void Delete(int id)
    {
        var product = products.SingleOrDefault(p => p.Id == id);
        if (product != null)
        {
            products.Remove(product);
        }
    }
} 

View Code

然后再把两个项目配置通过命令行读取ip、port自定义监听的ip、端口。

修改他们的Program.cs,增加如下配置:

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

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();
}

View Code

Consul服务治理发现

Consul([ˈkɒnsl],康搜)是注册中心,服务提供者、服务消费者等都要注册到Consul中,这样就可以实现服务提供者、服务消费者的隔离。

除了Consul之外,还有Eureka、Zookeeper等类似软件。

用DNS举例来理解Consul。consul是存储服务名称与IP和端口对应关系的服务器。

Asp.Net Core微服务再体验

consul服务器安装

consul下载地址https://www.consul.io/

运行

consul.exe agent -dev

这是开发环境测试,生产环境要建集群,要至少一台Server,多台Agent。

开发环境中consul重启后数据就会丢失。

consul的监控页面http://127.0.0.1:8500/consult主要做三件事:提供服务到ip地址的注册;提供服务到ip地址列表的查询;对提供服务方的健康检查(HealthCheck);

.Net Core连接Consul

打开之前新建的WebAPI项目 MsgService 与 ProductService ,安装Consul nuget包

Install-Package Consul

先使用使用默认生成的ValuesController做测试

在以上两个项目中新建一个控制器用来做Consul健康检查。HealthController.cs

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

[Route("api/[Controller]")]
public class HealthController : Controller
{
    [HttpGet]
    public IActionResult Get()
    {
        return Ok("ok");
    }
}

View Code

服务注册Consul及注销

Startup.cs:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime applicationLifetime)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseMvc();

    string ip = Configuration["ip"];
    int port = Convert.ToInt32(Configuration["port"]);
    string serviceName = "MsgService";
    string serviceId = serviceName + Guid.NewGuid();
    using (var client = new ConsulClient(ConsulConfig))
    {//注册服务到Consul
        client.Agent.ServiceRegister(new AgentServiceRegistration()
        {
            ID = serviceId,//服务编号,不能重复,用Guid最简单
            Name = serviceName,//服务的名字
            Address = ip,//服务提供者的能被消费者访问的ip地址(可以被其他应用访问的地址,本地测试可以用127.0.0.1,机房环境中一定要写自己的内网ip地址)
            Port = port,//服务提供者的能被消费者访问的端口
            Check = new AgentServiceCheck
            {
                DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//服务停止多久后反注册(注销)
                Interval = TimeSpan.FromSeconds(10),//健康检查时间间隔,或者称为心跳间隔
                HTTP = $"http://{ip}:{port}/api/health",//健康检查地址
                Timeout = TimeSpan.FromSeconds(5)
            }
        }).Wait();//Consult客户端的所有方法几乎都是异步方法,但是都没按照规范加上Async后缀,所以容易误导。记得调用后要Wait()或者await
    }
    
    //程序正常退出的时候从Consul注销服务//要通过方法参数注入IApplicationLifetime
    applicationLifetime.ApplicationStopped.Register(() =>
    {
        using (var client = new ConsulClient(ConsulConfig))
        {
            client.Agent.ServiceDeregister(serviceId).Wait();
        }
    });
}

private void ConsulConfig(ConsulClientConfiguration c)
{
    c.Address = new Uri("http://127.0.0.1:8500");
    c.Datacenter = "dc1";
}

也支持tcp探测,很显然也可以把普通TCP服务注册到Consul,因为Consul中注册的只是服务名字、ip地址、端口号,具体服务怎么实现、怎么调用Consul不管。

注意不同实例一定要用不同的Id,即使是相同服务的不同实例也要用不同的Id,上面的代码用Guid做Id,确保不重复。相同的服务用相同的Name。Address、Port是供服务消费者访问的服务器地址(或者IP地址)及端口号。Check则是做服务健康检查的(解释一下)。

在注册服务的时候还可以通过AgentServiceRegistration的Tags属性设置额外的标签。

查看节点状态

consul operator raft list-peers

通过命令行启动两个实例:

dotnet MsgService.dll --ip 127.0.0.1 --port 5001
dotnet ProductService.dll --ip 127.0.0.1 --port 5002

 打开Consul的Web页面服务已经注册进来了,注意刚开始启动的时候,有短暂的Failing是正常的。服务正常结束(Ctrl+C)会触发ApplicationStopped,正常注销。即使非正常结束也没关系,Consul健康检查过一会发现服务器死掉后也会主动注销。

如果服务器刚刚崩溃,但是还买来得及注销,消费的使用者可能就会拿到已经崩溃的实例,这个问题通过后面讲的重试等策略解决。

服务只会注册ip、端口,consul只会保存服务名、ip、端口这些信息,至于服务提供什么接口、方法、参数,consul不管,需要消费者知道服务的这些细节。

多个服务应用就注册多个就可以。Consul中可能注册多个服务,一个服务有多个服务器实例。上面讲的就是服务治理:服务的注册、注销、健康检查。

编写服务消费者

这里用控制台测试,真实项目中服务消费者同时也可能是另外一个Web应用(比如Web服务器调用短信服务器发短信)。

下面就是打印出所有Consul登记在册的服务实例.

新建控制台项目  服务消费者1 安装Consul nuget包

Install-Package Consul

using (var consulClient = new ConsulClient(c => c.Address = new Uri("http://127.0.0.1:8500")))
{
    var services = consulClient.Agent.Services().Result.Response;
    foreach (var service in services.Values)
    {
        Console.WriteLine($"id={service.ID},name={service.Service},ip={service.Address},port={service.Port}");
    }
}

 Asp.Net Core微服务再体验

下面的代码使用当前 TickCount 进行取模的方式达到随机获取一台服务器实例的效果,这叫做“客户端负载均衡”:

using (var consulClient = new ConsulClient(c => c.Address = new Uri("http://127.0.0.1:8500")))
{
    var services = consulClient.Agent.Services().Result.Response.Values.Where(s => s.Service.Equals("MsgService", StringComparison.OrdinalIgnoreCase));
    if (!services.Any())
    {
        Console.WriteLine("找不到服务的实例");
    }
    else
    {
        var service = services.ElementAt(Environment.TickCount % services.Count());
        Console.WriteLine($"{service.Address}:{service.Port}");
    }
}

Asp.Net Core微服务再体验

当然在一个毫秒之类会所有请求都压给一台服务器,基本就够用了。也可以自己写随机、轮询等客户端负载均衡算法,也可以自己实现按不同权重分配(注册时候 Tags 带上配置、权重等信息)等算法。

首先编写一个  RestTemplateCore  类库项目(模仿 Spring Cloud 中的 RestTemplate)

GitHub 地址:https://github.com/yangzhongke/RuPeng.RestTemplateCore

Nuget 地址:https://www.nuget.org/packages/RestTemplateCore

nuget 安装:Consul、Newtonsoft.Json

Install-Package Consul
Install-Package Newtonsoft.Json

新建Rest响应结果类RestResponse.cs

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

/// <summary> 
/// Rest响应结果 
/// </summary>     
public class RestResponse 
{ 
    /// <summary> 
    /// 响应状态码 
    /// </summary>         
    public HttpStatusCode StatusCode { get; set; } 

    /// <summary> 
    /// 响应的报文头 
    /// </summary>         
    public HttpResponseHeaders Headers { get; set; } 
} 

View Code

新建带响应报文的Rest响应结果类RestResponseWithBody.cs

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

/// <summary> 
/// 带响应报文的Rest响应结果,而且json报文会被自动反序列化 
/// </summary> 
/// <typeparam name="T"></typeparam>     
public class RestResponseWithBody<T>: RestResponse 
{ 
    /// <summary> 
    /// 响应报文体json反序列化的内容 
    /// </summary>         
    public T Body { get; set; } 
} 

View Code

新建解析类RestTemplate.cs

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

/// <summary>
/// 会自动到Consul中解析服务的Rest客户端,能把"http://ProductService/api/Product/"这样的虚拟地址
/// 按照客户端负载均衡算法解析为http://192.168.1.10:8080/api/Product/这样的真实地址
/// </summary>
public class RestTemplate
{
    public String ConsulServerUrl { get; set; } = "http://127.0.0.1:8500";
    private HttpClient httpClient;

    public RestTemplate(HttpClient httpClient)
    {
        this.httpClient = httpClient;
    }

    /// <summary>
    /// 获取服务的第一个实现地址
    /// </summary>
    /// <param name="consulClient"></param>
    /// <param name="serviceName"></param>
    /// <returns></returns>
    private async Task<String> ResolveRootUrlAsync(String serviceName)
    {
        using (var consulClient = new ConsulClient(c => c.Address = new Uri(ConsulServerUrl)))
        {
            var services = (await consulClient.Agent.Services()).Response.Values
                .Where(s => s.Service.Equals(serviceName, StringComparison.OrdinalIgnoreCase));
            if (!services.Any())
            {
                throw new ArgumentException($"找不到服务{serviceName }的任何实例");
            }
            else
            {
                //根据当前时钟毫秒数对可用服务个数取模,取出一台机器使用
                var service = services.ElementAt(Environment.TickCount % services.Count());
                return $"{service.Address}:{service.Port}";
            }
        }
    }

    //把http://apiservice1/api/values转换为http://192.168.1.1:5000/api/values
    private async Task<String> ResolveUrlAsync(String url)
    {
        Uri uri = new Uri(url);
        String serviceName = uri.Host;//apiservice1
        String realRootUrl = await ResolveRootUrlAsync(serviceName);//查询出来apiservice1对应的服务器地址192.168.1.1:5000
                                                                    //uri.Scheme=http,realRootUrl =192.168.1.1:5000,PathAndQuery=/api/values
        return uri.Scheme + "://" + realRootUrl + uri.PathAndQuery;
    }

    /// <summary>
    /// 发出Get请求
    /// </summary>
    /// <typeparam name="T">响应报文反序列类型</typeparam>
    /// <param name="url">请求路径</param>
    /// <param name="requestHeaders">请求额外的报文头信息</param>
    /// <returns></returns>
    public async Task<RestResponseWithBody<T>> GetForEntityAsync<T>(String url, HttpRequestHeaders requestHeaders = null)
    {
        using (HttpRequestMessage requestMsg = new HttpRequestMessage())
        {
            if (requestHeaders != null)
            {
                foreach (var header in requestHeaders)
                {
                    requestMsg.Headers.Add(header.Key, header.Value);
                }
            }
            requestMsg.Method = System.Net.Http.HttpMethod.Get;
            //http://apiservice1/api/values转换为http://192.168.1.1:5000/api/values
            requestMsg.RequestUri = new Uri(await ResolveUrlAsync(url));
            RestResponseWithBody<T> respEntity = await SendForEntityAsync<T>(requestMsg);
            return respEntity;
        }
    }

    /// <summary>
    /// 发出Post请求
    /// </summary>
    /// <typeparam name="T">响应报文反序列类型</typeparam>
    /// <param name="url">请求路径</param>
    /// <param name="body">请求数据,将会被json序列化后放到请求报文体中</param>
    /// <param name="requestHeaders">请求额外的报文头信息</param>
    /// <returns></returns>
    public async Task<RestResponseWithBody<T>> PostForEntityAsync<T>(String url, object body = null, HttpRequestHeaders requestHeaders = null)
    {
        using (HttpRequestMessage requestMsg = new HttpRequestMessage())
        {
            if (requestHeaders != null)
            {
                foreach (var header in requestHeaders)
                {
                    requestMsg.Headers.Add(header.Key, header.Value);
                }
            }
            requestMsg.Method = System.Net.Http.HttpMethod.Post;
            //http://apiservice1/api/values转换为http://192.168.1.1:5000/api/values
            requestMsg.RequestUri = new Uri(await ResolveUrlAsync(url));
            requestMsg.Content = new StringContent(JsonConvert.SerializeObject(body));
            requestMsg.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");

            RestResponseWithBody<T> respEntity = await SendForEntityAsync<T>(requestMsg);
            return respEntity;
        }
    }

    /// <summary>
    /// 发出Post请求
    /// </summary>
    /// <param name="url">请求路径</param>
    /// <param name="requestHeaders">请求额外的报文头信息</param>
    /// <returns></returns>
    public async Task<RestResponse> PostAsync(String url, object body = null, HttpRequestHeaders requestHeaders = null)
    {
        using (HttpRequestMessage requestMsg = new HttpRequestMessage())
        {
            if (requestHeaders != null)
            {
                foreach (var header in requestHeaders)
                {
                    requestMsg.Headers.Add(header.Key, header.Value);
                }
            }
            requestMsg.Method = System.Net.Http.HttpMethod.Post;
            //http://apiservice1/api/values转换为http://192.168.1.1:5000/api/values
            requestMsg.RequestUri = new Uri(await ResolveUrlAsync(url));
            requestMsg.Content = new StringContent(JsonConvert.SerializeObject(body));
            requestMsg.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");

            var resp = await SendAsync(requestMsg);
            return resp;
        }
    }

    /// <summary>
    /// 发出Put请求
    /// </summary>
    /// <typeparam name="T">响应报文反序列类型</typeparam>
    /// <param name="url">请求路径</param>
    /// <param name="body">请求数据,将会被json序列化后放到请求报文体中</param>
    /// <param name="requestHeaders">请求额外的报文头信息</param>
    /// <returns></returns>
    public async Task<RestResponseWithBody<T>> PutForEntityAsync<T>(String url, object body = null, HttpRequestHeaders requestHeaders = null)
    {
        using (HttpRequestMessage requestMsg = new HttpRequestMessage())
        {
            if (requestHeaders != null)
            {
                foreach (var header in requestHeaders)
                {
                    requestMsg.Headers.Add(header.Key, header.Value);
                }
            }
            requestMsg.Method = System.Net.Http.HttpMethod.Put;
            //http://apiservice1/api/values转换为http://192.168.1.1:5000/api/values
            requestMsg.RequestUri = new Uri(await ResolveUrlAsync(url));
            requestMsg.Content = new StringContent(JsonConvert.SerializeObject(body));
            requestMsg.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");

            RestResponseWithBody<T> respEntity = await SendForEntityAsync<T>(requestMsg);
            return respEntity;
        }
    }

    /// <summary>
    /// 发出Put请求
    /// </summary>
    /// <param name="url">请求路径</param>
    /// <param name="body">请求数据,将会被json序列化后放到请求报文体中</param>
    /// <param name="requestHeaders">请求额外的报文头信息</param>
    /// <returns></returns>
    public async Task<RestResponse> PutAsync(String url, object body = null, HttpRequestHeaders requestHeaders = null)
    {
        using (HttpRequestMessage requestMsg = new HttpRequestMessage())
        {
            if (requestHeaders != null)
            {
                foreach (var header in requestHeaders)
                {
                    requestMsg.Headers.Add(header.Key, header.Value);
                }
            }
            requestMsg.Method = System.Net.Http.HttpMethod.Put;
            //http://apiservice1/api/values转换为http://192.168.1.1:5000/api/values
            requestMsg.RequestUri = new Uri(await ResolveUrlAsync(url));
            requestMsg.Content = new StringContent(JsonConvert.SerializeObject(body));
            requestMsg.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");

            var resp = await SendAsync(requestMsg);
            return resp;
        }
    }

    /// <summary>
    /// 发出Delete请求
    /// </summary>
    /// <typeparam name="T">响应报文反序列类型</typeparam>
    /// <param name="url">请求路径</param>
    /// <param name="requestHeaders">请求额外的报文头信息</param>
    /// <returns></returns>
    public async Task<RestResponseWithBody<T>> DeleteForEntityAsync<T>(String url, HttpRequestHeaders requestHeaders = null)
    {
        using (HttpRequestMessage requestMsg = new HttpRequestMessage())
        {
            if (requestHeaders != null)
            {
                foreach (var header in requestHeaders)
                {
                    requestMsg.Headers.Add(header.Key, header.Value);
                }
            }
            requestMsg.Method = System.Net.Http.HttpMethod.Delete;
            //http://apiservice1/api/values转换为http://192.168.1.1:5000/api/values
            requestMsg.RequestUri = new Uri(await ResolveUrlAsync(url));
            RestResponseWithBody<T> respEntity = await SendForEntityAsync<T>(requestMsg);
            return respEntity;
        }
    }

    /// <summary>
    /// 发出Delete请求
    /// </summary>
    /// <param name="url">请求路径</param>
    /// <param name="requestHeaders">请求额外的报文头信息</param>
    /// <returns></returns>
    public async Task<RestResponse> DeleteAsync(String url, HttpRequestHeaders requestHeaders = null)
    {
        using (HttpRequestMessage requestMsg = new HttpRequestMessage())
        {
            if (requestHeaders != null)
            {
                foreach (var header in requestHeaders)
                {
                    requestMsg.Headers.Add(header.Key, header.Value);
                }
            }
            requestMsg.Method = System.Net.Http.HttpMethod.Delete;
            //http://apiservice1/api/values转换为http://192.168.1.1:5000/api/values
            requestMsg.RequestUri = new Uri(await ResolveUrlAsync(url));
            var resp = await SendAsync(requestMsg);
            return resp;
        }
    }

    /// <summary>
    /// 发出Http请求
    /// </summary>
    /// <typeparam name="T">响应报文反序列类型</typeparam>
    /// <param name="requestMsg">请求数据</param>
    /// <returns></returns>
    public async Task<RestResponseWithBody<T>> SendForEntityAsync<T>(HttpRequestMessage requestMsg)
    {
        var result = await httpClient.SendAsync(requestMsg);
        RestResponseWithBody<T> respEntity = new RestResponseWithBody<T>();
        respEntity.StatusCode = result.StatusCode;
        respEntity.Headers = respEntity.Headers;
        String bodyStr = await result.Content.ReadAsStringAsync();
        if (!string.IsNullOrWhiteSpace(bodyStr))
        {
            respEntity.Body = JsonConvert.DeserializeObject<T>(bodyStr);
        }

        return respEntity;
    }

    /// <summary>
    /// 发出Http请求
    /// </summary>
    /// <param name="requestMsg">请求数据</param>
    /// <returns></returns>
    public async Task<RestResponse> SendAsync(HttpRequestMessage requestMsg)
    {
        var result = await httpClient.SendAsync(requestMsg);
        RestResponse response = new RestResponse();
        response.StatusCode = result.StatusCode;
        response.Headers = result.Headers;
        return response;
    }
}

View Code

编写控制台项目 consultest1 做测试

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

class Program
{
    static void Main(string[] args)
    {
        using (HttpClient httpClient = new HttpClient())
        {
            RestTemplate rest = new RestTemplate(httpClient);

            Console.WriteLine("---查询数据---------");
            var ret1 =rest.GetForEntityAsync<Product[]>("http://ProductService/api/Product/").Result;
            Console.WriteLine(ret1.StatusCode);
            if (ret1.StatusCode == System.Net.HttpStatusCode.OK)
            {
                foreach (var p in ret1.Body)
                {
                    Console.WriteLine($"id={p.Id},name={p.Name}");
                }
            }

            Console.WriteLine("---新增数据---------");
            Product newP = new Product();
            newP.Id = 888;
            newP.Name = "辛增";
            newP.Price = 88.8;
            var ret = rest.PostAsync("http://ProductService/api/Product/", newP).Result;
            Console.WriteLine(ret.StatusCode);
        }
        Console.ReadKey();

    }
}

class Product
{
    public long Id { get; set; }
    public string Name { get; set; }
    public double Price { get; set; }
    public string Description { get; set; }
}

View Code

Asp.Net Core微服务再体验

熔断、降级

什么是熔断降级

  熔断就是“保险丝”。当出现某些状况时,切断服务,从而防止应用程序不断地尝试执行可能会失败的操作给系统造成“雪崩”,或者大量的超时等待导致系统卡死。

  降级的目的是当某个服务提供者发生故障的时候,向调用方返回一个错误响应或者替代响应。举例子:调用联通接口服务器发送短信失败之后,改用移动短信服务器发送,如果移动短信服务器也失败,则改用电信短信服务器,如果还失败,则返回“失败”响应;在从推荐商品服务器加载数据的时候,如果失败,则改用从缓存中加载,如果缓存中也加载失败,则返回一些本地替代数据。

Polly 简介

  .Net Core 中有一个被.Net 基金会认可的库 Polly,可以用来简化熔断降级的处理。主要功能:重试(Retry);断路器(Circuit-breaker);超时检测(Timeout);缓存(Cache);降级(FallBack);

  官网:https://github.com/App-vNext/Polly

  介绍文章:https://www.cnblogs.com/CreateMyself/p/7589397.html

Install-Package Polly -Version 6.0.1

  Polly 的策略由“故障”和“动作”两部分组成,“故障”包括异常、超时、返回值错误等情况,“动作”包括 FallBack(降级)、重试(Retry)、熔断(Circuit-breaker)等。

  策略用来执行可能会有有故障的业务代码,当业务代码出现“故障”中情况的时候就执行“动作”。

  由于实际业务代码中故障情况很难重现出来,所以 Polly 这一些都是用一些无意义的代码模拟出来。

  Polly 也支持请求缓存“数据不变化则不重复自行代码”,但是和新版本兼容不好,而且功能局限性很大,因此这里不讲。

  由于调试器存在,看不清楚 Polly 的执行过程,因此本节都用【开始执行(不调试)】

Polly 简单使用

  使用Policy的静态方法创建ISyncPolicy实现类对象,创建方法既有同步方法也有异步方法,根据自己的需要选择。下面先演示同步的,异步的用法类似。

  新建控制台项目  pollytest1 

  举例:当发生ArgumentException异常的时候,执行Fallback代码。

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

Policy policy = Policy.Handle<ArgumentException>()//故障
    .Fallback(() =>//动作 
    {
        Console.WriteLine("出错了");
    });
policy.Execute(() =>//在策略中执行业务代码 
{
    //这里是可能会产生问题的业务系统代码 
    Console.WriteLine("开始执行");
    throw new ArgumentException();
    Console.WriteLine("执行结束");
});

View Code

Asp.Net Core微服务再体验

  如果没有被Handle处理的异常,则会导致未处理异常被抛出。还可以用Fallback的其他重载获取异常信息:

Policy policy = Policy.Handle<ArgumentException>()//故障
    .Fallback(() =>//动作 
    {
        Console.WriteLine("出错了");
    },ex=> {
        Console.WriteLine("详细异常对象" + ex);
    });
policy.Execute(() =>//在策略中执行业务代码 
{
    //这里是可能会产生问题的业务系统代码 
    Console.WriteLine("开始执行");
    throw new ArgumentException();
    Console.WriteLine("执行结束");
});

Asp.Net Core微服务再体验

如果Execute中的代码是带返回值的,那么只要使用带泛型的Policy类即可:

Policy<string> policy = Policy<string>.Handle<Exception>() //故障 
    .Fallback(() =>//动作 
    {
        Console.WriteLine("执行出错");
        return "降级的值";
    });
string value = policy.Execute(() => {
    Console.WriteLine("开始任务");
    throw new Exception("Hello world!");
    Console.WriteLine("完成任务");
    return "正常的值";
});
Console.WriteLine("返回值:" + value);

Asp.Net Core微服务再体验

  FallBack的重载方法也非常多,有的异常可以直接提供降级后的值。

  (*)异常中还可以通过lambda表达式对异常判断“满足***条件的异常我才处理”,简单看看试试重载即可。还可以多个Or处理各种不同的异常。

  (*)还可以用HandleResult等判断返回值进行故障判断等,我感觉没太大必要。

重试处理

Policy policy = Policy
.Handle<Exception>().RetryForever();

policy.Execute(() => {
    Console.WriteLine("开始任务");
    if (DateTime.Now.Second % 10 != 0)
    {
        throw new Exception("出错");
    }
    Console.WriteLine("完成任务");
});

  RetryForever()是一直重试直到成功

  Retry()是重试最多一次;

  Retry(n) 是重试最多n次;

  WaitAndRetry()可以实现“如果出错等待100ms再试还不行再等150ms秒。。。。”,重载方法很多,不再一一介绍。

  还有WaitAndRetryForever。

短路保护 Circuit Breaker

  出现N次连续错误,则把“熔断器”(保险丝)熔断,等待一段时间,等待这段时间内如果再Execute 则直接抛出BrokenCircuitException异常,根本不会再去尝试调用业务代码。等待时间过去之后,再执行Execute的时候如果又错了(一次就够了),那么继续熔断一段时间,否则就恢复正常。这样就避免一个服务已经不可用了,还是使劲的请求给系统造成更大压力。

Policy policy = Policy.Handle<Exception>()
    .CircuitBreaker(6, TimeSpan.FromSeconds(5));//连续出错6次之后熔断5秒(不会再去尝试执行业务代码)。 
while (true)
{
    Console.WriteLine("开始Execute");
    try
    {
        policy.Execute(() => {
            Console.WriteLine("开始任务");
            throw new Exception("出错");
            Console.WriteLine("完成任务");
        });
    }
    catch (Exception ex)
    {
        Console.WriteLine("execute出错" + ex);
    }
    Thread.Sleep(500);
}

其计数的范围是policy对象,所以如果想整个服务器全局对于一段代码做短路保护,则需要共用一个policy对象。

https://andrewlock.net/when-you-use-the-polly-circuit-breaker-make-sure-you-share-yourpolicy-instances-2/

策略封装

  可以把多个ISyncPolicy合并到一起执行:

policy3= policy1.Wrap(policy2);

  执行policy3就会把policy1、policy2封装到一起执行

policy9=Policy.Wrap(policy1, policy2, policy3, policy4, policy5);

把更多一起封装。

超时处理

这些处理不能简单的链式调用,要用到Wrap。例如下面实现“出现异常则重试三次,如果还出错就FallBack”这样是不行的

Asp.Net Core微服务再体验

  注意Wrap是有包裹顺序的,内层的故障如果没有被处理则会抛出到外层。

  下面代码实现了“出现异常则重试三次,如果还出错就FallBack”

Policy policyRetry = Policy.Handle<Exception>()
    .Retry(3);
Policy policyFallback = Policy.Handle<Exception>()
     .Fallback(() => {
         Console.WriteLine("降级");
     });
//Wrap:包裹。policyRetry在里面,policyFallback裹在外面。 
//如果里面出现了故障,则把故障抛出来给外面 
Policy policy = policyFallback.Wrap(policyRetry);
policy.Execute(() => {
    Console.WriteLine("开始任务");
    if (DateTime.Now.Second % 10 != 0)
    {
        throw new Exception("出错");
    }
    Console.WriteLine("完成任务");
});

Asp.Net Core微服务再体验

  Timeout是定义超时故障。

Policy policy = Policy.Timeout(3, TimeoutStrategy.Pessimistic);// 创建一个3秒钟(注意单位)的超时策略。 

  Timeout生成的Policy要和其他Policy一起Wrap使用。超时策略一般不能直接用,而是和其他封装到一起用:

Policy policy = Policy
    .Handle<Exception>()    //定义所处理的故障 
    .Fallback(() =>
    {
        Console.WriteLine("执行出错");
    });
policy = policy.Wrap(Policy.Timeout(2, TimeoutStrategy.Pessimistic));
policy.Execute(() => 
{
    Console.WriteLine("开始任务");
    Thread.Sleep(5000);
    Console.WriteLine("完成任务");
});

Asp.Net Core微服务再体验

上面的代码就是如果执行超过2秒钟,则直接Fallback。  这个的用途:请求网络接口,避免接口长期没有响应造成系统卡死。

Polly 的异步用法

  所有方法都用Async方法即可,Handle由于只是定义异常,所以不需要异常方法:

  带返回值的例子:

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

Policy<byte[]> policy = Policy<byte[]>.Handle<Exception>()
    .FallbackAsync(async c =>
    {
        Console.WriteLine("执行出错");
        return new byte[0];
    }, async r =>
    {
        Console.WriteLine(r.Exception);
    });
policy = policy.WrapAsync(Policy.TimeoutAsync(20, TimeoutStrategy.Pessimistic, async (context, timespan, task) =>
{
    Console.WriteLine("timeout");
}));
var bytes = await policy.ExecuteAsync(async () =>
{
    Console.WriteLine("开始任务");
    HttpClient httpClient = new HttpClient();
    var result = await httpClient.GetByteArrayAsync("http://static.rupeng.com/upload/chatimage/20183/07EB793A4C247A654B31B4D14EC64BCA.png");
    Console.WriteLine("完成任务");
    return result;
});
Console.WriteLine("bytes长度" + bytes.Length);

View Code

Asp.Net Core微服务再体验

  没返回值的例子

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

Policy policy = Policy
    .Handle<Exception>()
    .FallbackAsync(async c => {
        Console.WriteLine("执行出错");
    }, async ex => {//对于没有返回值的,这个参数直接是异常 
        Console.WriteLine(ex);
    });
policy = policy.WrapAsync(Policy.TimeoutAsync(3, TimeoutStrategy.Pessimistic, async (context, timespan, task) =>
    {
        Console.WriteLine("timeout");
    }));
await policy.ExecuteAsync(async () => {
    Console.WriteLine("开始任务");
    await Task.Delay(5000);//注意不能用Thread.Sleep(5000); 
    Console.WriteLine("完成任务");
});

View Code

Asp.Net Core微服务再体验

AOP 框架基础

  要求懂的知识:AOP、Filter、反射(Attribute)。

  如果直接使用 Polly,那么就会造成业务代码中混杂大量的业务无关代码。我们使用 AOP (如果不了解 AOP,请自行参考网上资料)的方式封装一个简单的框架,模仿 Spring cloud 中的 Hystrix。

  需要先引入一个支持.Net Core 的 AOP,目前我发现的最好的.Net Core 下的 AOP 框架是AspectCore(国产,动态织入),其他要不就是不支持.Net Core,要不就是不支持对异步方法进行拦截。MVC Filter

  GitHub:https://github.com/dotnetcore/AspectCore-Framework

  这里只介绍和我们相关的用法:

  新建控制台项目  aoptest1 ,并添加nuget引用

Install-Package AspectCore.Core

编写拦截器类

  编写拦截器类CustomInterceptorAttribute.cs 一般继承自AbstractInterceptorAttribute

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

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");
        }
    }

}

View Code

AspectContext的属性的含义:

  Implementation  实际动态创建的Person子类的对象。

  ImplementationMethod就是Person子类的Say方法

Parameters 方法的参数值。

  Proxy==Implementation:当前场景下

  ProxyMethod==ImplementationMethod:当前场景下

ReturnValue返回值

  ServiceMethod是Person的Say方法

编写需要被代理拦截的类

  在要被拦截的方法上标注CustomInterceptorAttribute 。类需要是public类,方法需要是虚方法,支持异步方法,因为动态代理是动态生成被代理的类的动态子类实现的。

  建立测试类 Person.cs

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

public class Person
{
    [CustomInterceptor]
    public virtual void Say(string msg)
    {
        Console.WriteLine("service calling..." + msg);
    }
}

View Code

通过AspectCore创建代理对象

ProxyGeneratorBuilder proxyGeneratorBuilder = new ProxyGeneratorBuilder();
using (IProxyGenerator proxyGenerator = proxyGeneratorBuilder.Build())
{
    Person p = proxyGenerator.CreateClassProxy<Person>();
    p.Say("rupeng.com");
}

  注意p指向的对象是AspectCore生成的Person的动态子类的对象,直接new Person是无法被拦截的。

Asp.Net Core微服务再体验

创建简单的熔断降级框架

  新建控制台项目   MyHystrix1  添加nuget引用

Install-Package AspectCore.Core

  编写熔断降级框架 HystrixCommandAttribute.cs

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

[AttributeUsage( AttributeTargets.Method)]
public class HystrixCommandAttribute: AbstractInterceptorAttribute
{
    public string FallBackMethod { get; set; }

    public HystrixCommandAttribute(string fallBackMethod)
    {
        this.FallBackMethod = fallBackMethod;
    }
    public override async Task Invoke(AspectContext context, AspectDelegate next)
    {
        try
        {
            await next(context);//执行被拦截的方法 
        }
        catch (Exception ex)
        {
            //context.ServiceMethod被拦截的方法。
            //context.ServiceMethod.DeclaringType被拦截方法所在的类
            //context.Implementation实际执行的对象p 
            //context.Parameters方法参数值 
            //如果执行失败,则执行FallBackMethod 
            var fallBackMethod = context.ServiceMethod.DeclaringType.GetMethod(this.FallBackMethod);
            Object fallBackResult = fallBackMethod.Invoke(context.Implementation, context.Parameters);
            context.ReturnValue = fallBackResult;
        }
    }

}

View Code

  新建测试类 Person.cs

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

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;
    }
}

View Code

  要达到的目标是:参与降级的方法参数要一样。当HelloAsync执行出错的时候执行HelloFallBackAsync方法。

创建代理对象

ProxyGeneratorBuilder proxyGeneratorBuilder = new ProxyGeneratorBuilder();
using (IProxyGenerator proxyGenerator = proxyGeneratorBuilder.Build())
{
    Person p = proxyGenerator.CreateClassProxy<Person>();
    Console.WriteLine(p.HelloAsync("yzk").Result);
    Console.WriteLine(p.Add(1, 2));
}

Asp.Net Core微服务再体验

  上面的Person代码还支持多次降级,方法上标注[HystrixCommand]并且virtual即可:  

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

public class Person//需要public类 
{
    [HystrixCommand(nameof(Hello1FallBackAsync))]
    public virtual async Task<string> HelloAsync(string name)//需要是虚方法 
    {
        Console.WriteLine("hello" + name); String s = null; s.ToString(); return "ok";
    }

    [HystrixCommand(nameof(Hello2FallBackAsync))]
    public virtual async Task<string> Hello1FallBackAsync(string name)
    {
        Console.WriteLine("Hello降级1" + name); String s = null; s.ToString(); return "fail_1";
    }
    public virtual async Task<string> Hello2FallBackAsync(string name)
    {
        Console.WriteLine("Hello降级2" + name);

        return "fail_2";
    }

    [HystrixCommand(nameof(AddFall))]
    public virtual int Add(int i, int j)
    {
        String s = null; s.ToString(); return i + j;
    }
    public int AddFall(int i, int j)
    {
        return 0;
    }
}

View Code

 Asp.Net Core微服务再体验

细化框架

  github最新地址 https://github.com/yangzhongke/RuPeng.HystrixCore

  Nuget地址:https://www.nuget.org/packages/RuPeng.HystrixCore

  重试:MaxRetryTimes表示最多重试几次,如果为0则不重试,RetryIntervalMilliseconds 表示重试间隔的毫秒数;

  熔断:EnableCircuitBreaker是否启用熔断,ExceptionsAllowedBeforeBreaking表示熔断前出现允许错误几次,MillisecondsOfBreak表示熔断多长时间(毫秒);超时:TimeOutMilliseconds执行超过多少毫秒则认为超时(0表示不检测超时)缓存CacheTTLMilliseconds 缓存多少毫秒(0 表示不缓存),用“类名+方法名+所有参数值ToString拼接”做缓存Key(唯一的要求就是参数的类型ToString对于不同对象一定要不一样)。

  由于CircuitBreaker要求同一段代码必须共享同一个Policy对象。而方法上标注的Attribute 对于这个方法来讲就是唯一的对象,一个方法对应一个方法上标注的Attribute对象。一般我们熔断控制是针对一个方法,一个方法无论是通过几个 Person 对象调用,无论是谁调用,只要全局出现ExceptionsAllowedBeforeBreaking次错误,就会熔断,这是我框架的实现,你如果认为不合理,你自己改去。我们在Attribute上声明一个Policy的成员变量,这样一个方法就对应一个Policy对象。

Install-Package Microsoft.Extensions.Caching.Memory

  新建类库  RuPeng.HystrixCore 添加nuget引用

Install-Package AspectCore.Core

Install-Package Polly

  编写熔断降级框架 HystrixCommandAttribute.cs

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

[AttributeUsage(AttributeTargets.Method)]
public class HystrixCommandAttribute : AbstractInterceptorAttribute
{
    /// <summary>
    /// 最多重试几次,如果为0则不重试
    /// </summary>
    public int MaxRetryTimes { get; set; } = 0;

    /// <summary>
    /// 重试间隔的毫秒数
    /// </summary>
    public int RetryIntervalMilliseconds { get; set; } = 100;

    /// <summary>
    /// 是否启用熔断
    /// </summary>
    public bool EnableCircuitBreaker { get; set; } = false;

    /// <summary>
    /// 熔断前出现允许错误几次
    /// </summary>
    public int ExceptionsAllowedBeforeBreaking { get; set; } = 3;

    /// <summary>
    /// 熔断多长时间(毫秒)
    /// </summary>
    public int MillisecondsOfBreak { get; set; } = 1000;

    /// <summary>
    /// 执行超过多少毫秒则认为超时(0表示不检测超时)
    /// </summary>
    public int TimeOutMilliseconds { get; set; } = 0;

    /// <summary>
    /// 缓存多少毫秒(0表示不缓存),用“类名+方法名+所有参数ToString拼接”做缓存Key
    /// </summary>

    public int CacheTTLMilliseconds { get; set; } = 0;

    private static ConcurrentDictionary<MethodInfo, Policy> policies = new ConcurrentDictionary<MethodInfo, Policy>();

    private static readonly Microsoft.Extensions.Caching.Memory.IMemoryCache memoryCache = new Microsoft.Extensions.Caching.Memory.MemoryCache(new Microsoft.Extensions.Caching.Memory.MemoryCacheOptions());

    /// <summary>
    /// 
    /// </summary>
    /// <param name="fallBackMethod">降级的方法名</param>
    public HystrixCommandAttribute(string fallBackMethod)
    {
        this.FallBackMethod = fallBackMethod;
    }

    public string FallBackMethod { get; set; }

    public override async Task Invoke(AspectContext context, AspectDelegate next)
    {
        //一个HystrixCommand中保持一个policy对象即可
        //其实主要是CircuitBreaker要求对于同一段代码要共享一个policy对象
        //根据反射原理,同一个方法的MethodInfo是同一个对象,但是对象上取出来的HystrixCommandAttribute
        //每次获取的都是不同的对象,因此以MethodInfo为Key保存到policies中,确保一个方法对应一个policy实例
        policies.TryGetValue(context.ServiceMethod, out Policy policy);
        lock (policies)//因为Invoke可能是并发调用,因此要确保policies赋值的线程安全
        {
            if (policy == null)
            {
                policy = Policy.NoOpAsync();//创建一个空的Policy
                if (EnableCircuitBreaker)
                {
                    policy = policy.WrapAsync(Policy.Handle<Exception>().CircuitBreakerAsync(ExceptionsAllowedBeforeBreaking, TimeSpan.FromMilliseconds(MillisecondsOfBreak)));
                }
                if (TimeOutMilliseconds > 0)
                {
                    policy = policy.WrapAsync(Policy.TimeoutAsync(() => TimeSpan.FromMilliseconds(TimeOutMilliseconds), Polly.Timeout.TimeoutStrategy.Pessimistic));
                }
                if (MaxRetryTimes > 0)
                {
                    policy = policy.WrapAsync(Policy.Handle<Exception>().WaitAndRetryAsync(MaxRetryTimes, i => TimeSpan.FromMilliseconds(RetryIntervalMilliseconds)));
                }
                Policy policyFallBack = Policy
                    .Handle<Exception>()
                    .FallbackAsync(async (ctx, t) =>
                    {
                        AspectContext aspectContext = (AspectContext)ctx["aspectContext"];
                        var fallBackMethod = context.ServiceMethod.DeclaringType.GetMethod(this.FallBackMethod);
                        Object fallBackResult = fallBackMethod.Invoke(context.Implementation, context.Parameters);
                        //不能如下这样,因为这是闭包相关,如果这样写第二次调用Invoke的时候context指向的
                        //还是第一次的对象,所以要通过Polly的上下文来传递AspectContext
                        //context.ReturnValue = fallBackResult;
                        aspectContext.ReturnValue = fallBackResult;
                    }, async (ex, t) => { });

                policy = policyFallBack.WrapAsync(policy);
                //放入
                policies.TryAdd(context.ServiceMethod, policy);
            }
        }

        //把本地调用的AspectContext传递给Polly,主要给FallbackAsync中使用,避免闭包的坑
        Context pollyCtx = new Context();
        pollyCtx["aspectContext"] = context;

        //Install-Package Microsoft.Extensions.Caching.Memory
        if (CacheTTLMilliseconds > 0)
        {
            //用类名+方法名+参数的下划线连接起来作为缓存key
            string cacheKey = "HystrixMethodCacheManager_Key_" + context.ServiceMethod.DeclaringType
                                                               + "." + context.ServiceMethod + string.Join("_", context.Parameters);
            //尝试去缓存中获取。如果找到了,则直接用缓存中的值做返回值
            if (memoryCache.TryGetValue(cacheKey, out var cacheValue))
            {
                context.ReturnValue = cacheValue;
            }
            else
            {
                //如果缓存中没有,则执行实际被拦截的方法
                await policy.ExecuteAsync(ctx => next(context), pollyCtx);
                //存入缓存中
                using (var cacheEntry = memoryCache.CreateEntry(cacheKey))
                {
                    cacheEntry.Value = context.ReturnValue;
                    cacheEntry.AbsoluteExpiration = DateTime.Now + TimeSpan.FromMilliseconds(CacheTTLMilliseconds);
                }
            }
        }
        else//如果没有启用缓存,就直接执行业务方法
        {
            await policy.ExecuteAsync(ctx => next(context), pollyCtx);
        }
    }
}

View Code

  新建WebAPI项目  hystrixTest1  用于测试添加RuPeng.HystrixCore引用护着nuget引用,但是安装AspectCore.Extensions.DependencyInjection nuget包

Install-Package AspectCore.Extensions.DependencyInjection

  新建测试类 Person.cs

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

public class Person//需要public类
{
    [HystrixCommand(nameof(Hello1FallBackAsync))]
    public virtual async Task<string> HelloAsync(string name)//需要是虚方法
    {
        Console.WriteLine("hello" + name);
        String s = null;
        s.ToString();
        return "ok";
    }

    [HystrixCommand(nameof(Hello2FallBackAsync))]
    public virtual async Task<string> Hello1FallBackAsync(string name)
    {
        Console.WriteLine("Hello降级1" + name);
        String s = null;
        s.ToString();
        return "fail_1";
    }

    public virtual async Task<string> Hello2FallBackAsync(string name)
    {
        Console.WriteLine("Hello降级2" + name);

        return "fail_2";
    }

    [HystrixCommand(nameof(AddFall))]
    public virtual int Add(int i, int j)
    {
        String s = null;
        s.ToString();
        return i + j;
    }
    public int AddFall(int i, int j)
    {
        return 0;
    }
}

View Code

  修改Startup.cs,这里不再动态创建Person类,而是通过依赖注入和程序集注入

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    // services.AddSingleton<Person>();
    RegisterServices(this.GetType().Assembly, services);
    return services.BuildAspectCoreServiceProvider();

}

/// <summary>
/// 扫描asm程序集中所有的public类,对于类看看是否含有标注了HystrixCommand的方法
/// 如果有,则AddSingleton到services
/// </summary>
/// <param name="asm"></param>
/// <param name="services"></param>
private void RegisterServices(Assembly asm, IServiceCollection services)
{
    foreach (Type type in asm.GetExportedTypes())
    {
        //判断type类型中是否有至少一个方法含有HystrixCommandAttribute
        bool hasHystrixCmd = type.GetMethods().Any(m => m.GetCustomAttribute(typeof(HystrixCommandAttribute)) != null);
        //type.GetMethods().Where(m => m.GetCustomAttribute(typeof(HystrixCommandAttribute)) != null).Count();
        if (hasHystrixCmd)
        {
            services.AddSingleton(type);
        }
    }
}

  修改ValuesController.cs

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

private Person p;
public ValuesController(Person p)
{
    this.p = p;
}

// GET api/values
[HttpGet]
public async Task<IEnumerable<string>> Get()
{
    /*
    ProxyGeneratorBuilder proxyGeneratorBuilder = new ProxyGeneratorBuilder();
    using (IProxyGenerator proxyGenerator = proxyGeneratorBuilder.Build())
    {
        //Person p = new Person();
        Person p = proxyGenerator.CreateClassProxy<Person>();
        await p.HelloAsync("rupeng.com");
    }*/
    await p.HelloAsync("rupeng.com");
    return new string[] { "value1", "value2" };
}

View Code

Asp.Net Core微服务再体验

没必要、也不可能把所有Polly都封装到Hystrix中。框架不是万能的,不用过度框架,过度框架带来的复杂度陡增,从人人喜欢变成人人恐惧。

Ocelot API网关(API GateWay)

  现有微服务的几点不足:

    1)  对于在微服务体系中、和 Consul 通讯的微服务来讲,使用服务名即可访问。但是对于手机、web 端等外部访问者仍然需要和 N 多服务器交互,需要记忆他们的服务器地址、端口号等。一旦内部发生修改,很麻烦,而且有时候内部服务器是不希望外界直接访问的。

    2)  各个业务系统的人无法自由的维护自己负责的服务器;

    3)  现有的微服务都是“我家大门常打开”,没有做权限校验。如果把权限校验代码写到每个微服务上,那么开发工作量太大。

    4)  很难做限流、收费等。

  ocelot 中文文档:https://blog.csdn.net/sD7O95O/article/details/79623654

  资料:http://www.csharpkit.com/apigateway.html

  官网:https://github.com/ThreeMammals/Ocelot

  腾讯.Net 大队长“张善友”是项目主力开发人员之一。

Ocelot 基本配置

  Ocelot 就是一个提供了请求路由、安全验证等功能的 API 网关微服务。建一个空的aspnet core空项目 ocelottest1 。

Install-Package Ocelot

  项目根目录下创建 configuration.json

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

{
  "ReRoutes": [
    {
      "DownstreamPathTemplate": "/api/{url}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5001
        }
      ],
      "UpstreamPathTemplate": "/MsgService/{url}",
      "UpstreamHttpMethod": [ "Get", "Post" ]
    },
    {
      "DownstreamPathTemplate": "/api/{url}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5003
        }
      ],
      "UpstreamPathTemplate": "/ProductService/{url}",
      "UpstreamHttpMethod": [ "Get", "Post" ]
    }
  ]
}

View Code

Asp.Net Core微服务再体验

  Program.cs的CreateWebHostBuilder中

public static IWebHost BuildWebHost(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .UseUrls("http://127.0.0.1:8888")
        .ConfigureAppConfiguration((hostingContext, builder) => {
            builder.AddJsonFile("configuration.json", false, true);
        })
        .Build();

  Startup.cs中通过构造函数注入一个IConfiguration;

public class Startup
{
    private IConfiguration Configuration;

public Startup(IConfiguration Configuration)
{
this.Configuration = Configuration;
}

// This method gets called by the runtime. Use this method to add services to the container.
    // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddOcelot(Configuration);
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        //app.Run(async (context) =>
        //{
        //    await context.Response.WriteAsync("Hello World!");
        //});
        app.UseOcelot().Wait();//不要忘了写Wait 
    }
}

这样当访问http://127.0.0.1:8888/MsgService/sms/Send\_LX的时候就会访问 http://127.0.0.1:5001/api/sms/Send\_LX

UpstreamHttpMethod表示对什么样的请求类型做转发。

Asp.Net Core微服务再体验

Ocelot+Consul

新启动一个服务方便后面做负载均衡

dotnet MsgService.dll --ip 127.0.0.1 --port 5002

上面的配置还是把服务的ip地址写死了,Ocelot可以和Consul通讯,通过服务名字来配置。 只要改配置文件即可configuration.json

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

{
  "ReRoutes": [
    {
      "DownstreamPathTemplate": "/api/{url}",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/MsgService/{url}",
      "UpstreamHttpMethod": [ "Get", "Post" ],
      "ServiceName": "MsgService",
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      },
      "UseServiceDiscovery": true
    }
  ],

  "GlobalConfiguration": {
    "ServiceDiscoveryProvider": {
      "Host": "localhost",
      "Port": 8500
    }
  }
}

View Code

  访问 http://127.0.0.1:8888/MsgService/sms/Send\_LX 即可,请求报文体

  {"phoneNum":"1234567890","msg":"您有新短信"}

Asp.Net Core微服务再体验

  表示只要是/MsgService/开头的都会转给后端的服务名为" MsgService "的一台服务器,转发的路径是"/api/{url}"。

  LoadBalancerOptions 中"LeastConnection"表示负载均衡算法是“选择当前最少连接数的服务器”,如果改为 RoundRobin 就是“轮询”。  

  ServiceDiscoveryProvider 是 Consul 服务器的配置。

  Ocelot 因为是流量中枢,也是可以做集群的。

   (*) 也 支 持 Eureka 进行服务的注册 、 查 找(http://ocelot.readthedocs.io/en/latest/features/servicediscovery.html),也支持访问 Service Fabric 中的服务(http://ocelot.readthedocs.io/en/latest/features/servicefabric.html)。

Ocelot 其他功能简单介绍

限流

  文档:http://ocelot.readthedocs.io/en/latest/features/ratelimiting.html 需要和 Identity Server 一起使用,其他的限速是针对 clientId 限速,而不是针对 ip 限速。比如我调用微博的api开发了一个如鹏版新浪微博,我的 clientid 是 rpwb,然后限制了 1 秒钟只能调用 1000 次,那么所有用如鹏版微博这个 app 的所有用户加在一起,在一秒钟之内,不能累计超过 1000 次。目前开放式 api 的限流都是这个套路。

  如果要做针对 ip 的限速等,要自己在 Ocelot 前面架设 Nginx 来实现。

请求缓存

  http://ocelot.readthedocs.io/en/latest/features/caching.html 只支持 get,只要 url 不变,就会缓存。

QOS(熔断器)

  http://ocelot.readthedocs.io/en/latest/features/qualityofservice.html

JWT算法

JWT 简介

  内部 Restful 接口可以“我家大门常打开”,但是如果要给 app 等使用的接口,则需要做权限校验,不能谁都随便调用。

  Restful 接口不是 web 网站,App 中很难直接处理 SessionId,而且 Cookie 有跨域访问的限制,所以一般不能直接用后端 Web 框架内置的 Session 机制。但是可以用类似 Session 的机制,用户登录之后返回一个类似 SessionId 的东西,服务器端把 SessionId 和用户的信息对应关系保存到 Redis 等地方,客户端把 SessionId 保存起来,以后每次请求的时候都带着这个SessionId。

  用类似 Session 这种机制的坏处:需要集中的 Session 机制服务器;不可以在 nginx、CDN 等静态文件处理服务器上校验权限;每次都要根据 SessionId 去 Redis 服务器获取用户信息,效率低;

  JWT(Json Web Token)是现在流行的一种对 Restful 接口进行验证的机制的基础。JWT 的特点:把用户信息放到一个 JWT 字符串中,用户信息部分是明文的,再加上一部分签名区域,签名部分是服务器对于“明文部分+秘钥”加密的,这个加密信息只有服务器端才能解析。用户端只是存储、转发这个 JWT 字符串。如果客户端篡改了明文部分,那么服务器端解密时候会报错。

  JWT 由三块组成,可以把用户名、用户 Id 等保存到 Payload 部分

 Asp.Net Core微服务再体验

  注意 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

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

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);

View Code

解密

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

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");
}

View Code

过期时间

  在 payload 中增加一个名字为 exp 的值,值为过期时间和 1970/1/1 00:00:00 相差的秒数

不用秘钥解析数据

  payload 因为 payload 部分是明文的,所以在不知道秘钥的时候也可以用 Decode、DecodeToObject 等不需要秘钥的方法把payload部分解析出来。

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

var token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJVc2VySWQiOjEyMywiVXNlck5hbWUiOiJhZG1pbiJ9.Qjw1epD5P6p4Yy2yju3-fkq28PddznqRj3ESfALQy_U";

try
{
    IJsonSerializer serializer = new JsonNetSerializer();
    IDateTimeProvider provider = new UtcDateTimeProvider();
    IJwtValidator validator = new JwtValidator(serializer, provider);
    IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
    IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder);

    var json = decoder.Decode(token);
    Console.WriteLine(json);
}
catch (FormatException)
{
    Console.WriteLine("Token format invalid");
}
catch (TokenExpiredException)
{
    Console.WriteLine("Token has expired");
}

View Code

Ocelot+Identity Server

用JWT机制实现验证的原理如下图:认证服务器负责颁发Token(相当于JWT值)和校验Token的合法性。

Asp.Net Core微服务再体验

Asp.Net Core微服务再体验

相关概念

  API 资源(API Resource):微博服务器接口、斗鱼弹幕服务器接口、斗鱼直播接口就是API 资源。

  客户端(Client):Client 就是官方微博 android 客户端、官方微博 ios 客户端、第三方微博客户端、微博助手等。

  身份资源(Identity Resource):就是用户。

Asp.Net Core微服务再体验

  一个用户可能使用多个客户端访问服务器;一个客户端也可能服务多个用户。

  封禁了一个客户端,所有用户都不能使用这个这个客户端访问服务器,但是可以使用其他客户端访问;封禁了一个用户,这个用户在所有设备上都不能访问,但是不影响其他用户。

搭建 identity server 认证服务器

  新建一个空的 web 项目  ID4.IdServer 

Install-Package IdentityServer4

  首先编写一个提供应用列表、账号列表的 Config.cs 类

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

public class Config
{
    /// <summary> 
    /// 返回应用列表 
    /// </summary> 
    /// <returns></returns> 
    public static IEnumerable<ApiResource> GetApiResources()
    {
        List<ApiResource> resources = new List<ApiResource>();
        //ApiResource第一个参数是应用的名字,第二个参数是描述 
        resources.Add(new ApiResource("MsgAPI", "消息服务API"));
        resources.Add(new ApiResource("ProductAPI", "产品API"));
        return resources;
    }

    /// <summary> 
    /// 返回账号列表 
    /// </summary>         
    /// <returns></returns>         
    public static IEnumerable<Client> GetClients()
    {
        List<Client> clients = new List<Client>();
        clients.Add(new Client
        {
            ClientId = "clientPC1",//API账号、客户端Id 
            AllowedGrantTypes = GrantTypes.ClientCredentials,//认证方式
            ClientSecrets =
            {
                new Secret("123321".Sha256())//秘钥 
            },
            AllowedScopes = { "MsgAPI", "ProductAPI" }//这个账号支持访问哪些应用 
        });
        return clients;
    }

}

View Code

  如果允许在数据库中配置账号等信息,那么可以从数据库中读取然后返回这些内容。疑问待解。

  修改Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddIdentityServer()
        .AddDeveloperSigningCredential()
        .AddInMemoryApiResources(Config.GetApiResources())
        .AddInMemoryClients(Config.GetClients());

}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    //if (env.IsDevelopment())
    //{
    //    app.UseDeveloperExceptionPage();
    //}

    //app.Run(async (context) =>
    //{
    //    await context.Response.WriteAsync("Hello World!");
    //});
    app.UseIdentityServer();
}

  然后在 9500 端口启动

  在 postman 里发出请求,获取 token http://localhost:9500/connect/token,发 Post 请求,表单请求内容(注意不是报文头):

  client_id=clientPC1   client_secret=123321   grant_type=client_credentials

 Asp.Net Core微服务再体验

  把返回的 access_token 留下来后面用(注意有有效期)。  注意,其实不应该让客户端直接去申请 token,这只是咱演示,后面讲解正确做法。

搭建 Ocelot 服务器项目

  新建空 Web 项目,项目名  ID4.Ocelot1 

  nuget 安装 IdentityServer4、Ocelot

Install-Package IdentityServer4
Install-Package Ocelot

  编写配置文件 Ocelot.json(注意设置【如果较新则】)

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

{
  "ReRoutes": [
    {
      "DownstreamPathTemplate": "/api/{url}",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/MsgService/{url}",
      "UpstreamHttpMethod": [ "Get", "Post" ],
      "ServiceName": "MsgService",
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      },
      "UseServiceDiscovery": true,
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "MsgKey",
        "AllowedScopes": []
      }
    }, 
    { 
      "DownstreamPathTemplate": "/api/{url}", 
      "DownstreamScheme": "http", 
      "UpstreamPathTemplate": "/ProductService/{url}", 
      "UpstreamHttpMethod": [ "Get", "Post" ], 
      "ServiceName": "ProductService", 
      "LoadBalancerOptions": { 
        "Type": "RoundRobin" 
      }, 
      "UseServiceDiscovery": true, 
      "AuthenticationOptions": { 
        "AuthenticationProviderKey": "ProductKey", 
        "AllowedScopes": [] 
      } 
    } 
  ], 
 
  "GlobalConfiguration": { 
    "ServiceDiscoveryProvider": { 
      "Host": "localhost", 
      "Port": 8500 
    } 
  } 
}

View Code

  把/MsgService 访问的都转给消息后端服务器(使用Consul进行服务发现)。也可以把Identity Server配置到Ocelot,但是我们不做,后边会讲为什么不放。

在Program.cs 的 CreateWebHostBuilder 中加载 Ocelot.json

public static IWebHost BuildWebHost(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .ConfigureAppConfiguration((hostingContext, builder)=> {
            builder.AddJsonFile("Ocelot.json", false, true);
        })
        .Build();

  修改 Startup.cs 让 Ocelot 能够访问 Identity Server 进行 Token 的验证

public void ConfigureServices(IServiceCollection services)
{
    //指定Identity Server的信息 
    Action<IdentityServerAuthenticationOptions> isaOptMsg = o =>
    {
        o.Authority = "http://localhost:9500";
        o.ApiName = "MsgAPI";//要连接的应用的名字 
        o.RequireHttpsMetadata = false;
        o.SupportedTokens = SupportedTokens.Both;
        o.ApiSecret = "123321";//秘钥 
    };
    Action<IdentityServerAuthenticationOptions> isaOptProduct = o =>
    {
        o.Authority = "http://localhost:9500";
        o.ApiName = "ProductAPI";//要连接的应用的名字 
        o.RequireHttpsMetadata = false;
        o.SupportedTokens = SupportedTokens.Both;
        o.ApiSecret = "123321";//秘钥             
    };
    //对配置文件中使用ChatKey配置了AuthenticationProviderKey=MsgKey 
    //的路由规则使用如下的验证方式 
    services.AddAuthentication()
        .AddIdentityServerAuthentication("MsgKey", isaOptMsg)
        .AddIdentityServerAuthentication("ProductKey", isaOptProduct);
    services.AddOcelot();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    //app.Run(async (context) =>
    //{
    //    await context.Response.WriteAsync("Hello World!");
    //});
    app.UseOcelot().Wait();
}

  很显然我们可以让不同的服务采用不同的Identity Server。

  启动 Ocelot 服务器,然后向 ocelot 请求/MsgService/sms/Send_LX(报文体还是要传 json 数据),在请求头(不是报文体)里加上:Authorization="Bearer "+上面 identityserver 返回的 accesstoken

Asp.Net Core微服务再体验

  如果返回 401,那就是认证错误。

  Ocelot 会把 Authorization 值传递给后端服务器,这样在后端服务器可以用 IJwtDecoder 的这个不传递 key 的重载方法 IDictionary<string, object> DecodeToObject(string token),就可以在不验证的情况下获取 client_id 等信息。

  也可以把 Identity Server 通过 Consul 进行服务治理。

        Ocelot+Identity Server 实现了接口的权限验证,各个业务系统不需要再去做验证。

不能让客户端请求 token

  上面是让客户端去请求 token,如果项目中这么搞的话,就把 client_id 特别是 secret 泄露给普通用户的。正确的做法应该是,开发一个 token 服务,由这个服务来向 identity Server 请求 token,客户端向 token 服务发请求,把 client_id、secret 藏到这个 token 服务器上。当然这个服务器也要经过 Ocelot 转发。这个做起来很简单,就不演示了。放到下面一起演示。

用户名密码登录

  如果 Api 和用户名、密码无关(比如系统内部之间 API 的调用),那么上面那样做就可以了,但是有时候需要用户身份验证的(比如 Android 客户端)。也就是在请求 token 的时候还要验证用户名密码,在服务中还可以获取登录用户信息。

修改认证系统

  修改的地方:

   ID4.IdServer  项目中增加类 ProfileService.cs 

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

public class ProfileService : IProfileService
{
    public async Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
        var claims = context.Subject.Claims.ToList();
        context.IssuedClaims = claims.ToList();
    }

    public async Task IsActiveAsync(IsActiveContext context)
    {
        context.IsActive = true;
    }
}

View Code

  增加类 ResourceOwnerPasswordValidator.cs 

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
    public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
    {
        //根据context.UserName和context.Password与数据库的数据做校验,判断是否合法 
        if (context.UserName == "yzk" && context.Password == "123")
        {
            context.Result = new GrantValidationResult(
                subject: context.UserName, 
                authenticationMethod: "custom", 
                claims: new Claim[] 
                {
                    new Claim("Name", context.UserName),
                    new Claim("UserId", "111"),
                    new Claim("RealName", "杨中科"),
                    new Claim("Email", "yzk365@qq.com")
                });
        }
        else
        {
            //验证失败 
            context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "invalid custom credential");
        }
    }
}

View Code

  当然这里的用户名密码是写死的,可以在项目中连接自己的用户数据库进行验证。claims 中可以放入多组用户的信息,这些信息都可以在业务系统中获取到。

  修改一下Config.cs,主要是把GetClients中的AllowedGrantTypes属性值改为GrantTypes.ResourceOwnerPassword,并且在AllowedScopes中加入IdentityServerConstants.StandardScopes.OpenId, //必须要添加,否则报forbidden错误IdentityServerConstants.StandardScopes.Profile

  修改后的 Config.cs

public class Config
{
    /// <summary> 
    /// 返回应用列表 
    /// </summary> 
    /// <returns></returns> 
    public static IEnumerable<ApiResource> GetApiResources()
    {
        List<ApiResource> resources = new List<ApiResource>();
        //ApiResource第一个参数是应用的名字,第二个参数是描述 
        resources.Add(new ApiResource("MsgAPI", "消息服务API"));
        resources.Add(new ApiResource("ProductAPI", "产品API"));
        return resources;
    }

    /// <summary> 
    /// 返回账号列表 
    /// </summary>         
    /// <returns></returns>         
    public static IEnumerable<Client> GetClients()
    {
        List<Client> clients = new List<Client>();
        clients.Add(new Client
        {
            ClientId = "clientPC1",//API账号、客户端Id 
            AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,//认证方式
            ClientSecrets =
            {
                new Secret("123321".Sha256())//秘钥 
            },
            AllowedScopes = {
                "MsgAPI",
                "ProductAPI",
                IdentityServerConstants.StandardScopes.OpenId, //必须要添加,否则报forbidden错误 
                IdentityServerConstants.StandardScopes.Profile
            }//这个账号支持访问哪些应用 
        });
        return clients;
    }

}

  Startup.cs 的 ConfigureServices 修改为

public void ConfigureServices(IServiceCollection services)
{
    var idResources = new List<IdentityResource>
    {
        new IdentityResources.OpenId(), //必须要添加,否则报无效的 scope 错误           
        new IdentityResources.Profile() 
    };


    services.AddIdentityServer()
        .AddDeveloperSigningCredential()
        .AddInMemoryIdentityResources(idResources)
        .AddInMemoryApiResources(Config.GetApiResources())
        .AddInMemoryClients(Config.GetClients())
        .AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()
        .AddProfileService<ProfileService>();
}

  主要是增加了 AddInMemoryIdentityResources 、 AddResourceOwnerValidator 、AddProfileService

修改业务系统

  以 MsgService 为例,安装nuget包

Install-Package IdentityServer4.AccessTokenValidation

  然后 Startup.cs 的 ConfigureServices 中增加

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddAuthentication("Bearer")
       .AddIdentityServerAuthentication(options =>
       {
           options.Authority = "http://localhost:9500";//identity server 地址         
           options.RequireHttpsMetadata = false; 
       });
}

  Startup.cs 的 Configure 中增加

app.UseAuthentication(); //一定要放在app.UseMvc();上部

app.UseMvc();

请求测试

  请求 ID4.IdServer认证服务的token 把报文头中的 grant_type 值改为 password,报文头增加 username、password 为用户名、密码。

 Asp.Net Core微服务再体验

 像之前一样用返回的 access_token传递给请求的Authorization 中,在业务系统的 User中就可以获取到 ResourceOwnerPasswordValidator 中为用户设置的 claims 等信息了。

[HttpPost(nameof(Send_LX))]
public void Send_LX(SendSMSRequest model)
{
    string name = this.User.Identity.Name;//读取的就是"Name"这个特殊的 Claims 的值 
    string userId = this.User.FindFirst("UserId").Value;
    string realName = this.User.FindFirst("RealName").Value;
    string email = this.User.FindFirst("Email").Value;
    Console.WriteLine($"name={name},userId={userId},realName={realName},email={email}");

    Console.WriteLine($"通过联想短信接口向{model.PhoneNum}发送短信{model.Msg}");
}

独立登录服务器

  解决上面提到的“不能让客户端接触到 client_id、secret 的问题”

  开发一个WebAPI应用  LoginService 

  新建请求体类RequestTokenParam.cs

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

public class RequestTokenParam
{
    public string username { get; set; }
    public string password { get; set; }
}

View Code

  新建登录控制器 LoginController

Asp.Net Core微服务再体验 Asp.Net Core微服务再体验

[Produces("application/json")]
[Route("api/Login")]
public class LoginController : Controller
{
    [HttpPost]
    public async Task<ActionResult> RequestToken(RequestTokenParam model)
    {
        Dictionary<string, string> dict = new Dictionary<string, string>();
        dict["client_id"] = "clientPC1";
        dict["client_secret"] = "123321";
        dict["grant_type"] = "password";
        dict["username"] = model.username;
        dict["password"] = model.password;

        //由登录服务器向IdentityServer发请求获取Token 
        using (HttpClient http = new HttpClient())
        using (var content = new FormUrlEncodedContent(dict))
        {
            var msg = await http.PostAsync("http://localhost:9500/connect/token", content);
            string result = await msg.Content.ReadAsStringAsync();
            return Content(result, "application/json");
        }
    }
}

View Code

  修改Program.cs使用6000端口  

  修改项目  ID4.Ocelot1 配置文件Ocelot.json,新增配置进行登录网关处理

{
  "ReRoutes": [
    {
      "DownstreamPathTemplate": "/api/{url}",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/MsgService/{url}",
      "UpstreamHttpMethod": [ "Get", "Post" ],
      "ServiceName": "MsgService",
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      },
      "UseServiceDiscovery": true,
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "MsgKey",
        "AllowedScopes": []
      }
    },
    {
      "DownstreamPathTemplate": "/api/{url}",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/ProductService/{url}",
      "UpstreamHttpMethod": [ "Get", "Post" ],
      "ServiceName": "ProductService",
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      },
      "UseServiceDiscovery": true,
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "ProductKey",
        "AllowedScopes": []
      }
    },
    {
      "DownstreamPathTemplate": "/api/{url}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 6008
        }
      ],
      "UpstreamPathTemplate": "/LoginService/{url}",
      "UpstreamHttpMethod": [ "Get", "Post" ]
    }
  ], 
 
  "GlobalConfiguration": { 
    "ServiceDiscoveryProvider": { 
      "Host": "localhost", 
      "Port": 8500 
    } 
  } 
}

  这样客户端只要向 LoginService 的 /api/Login/ 发请求带上 json 报文体{username:"yzk",password:"123"}即可。客户端就不知道 client_secret 这些机密信息了。

Asp.Net Core微服务再体验

  把 LoginService 配置到 Ocelot 中。

  参考文章:https://www.cnblogs.com/jaycewu/p/7791102.html

Thrift高效通讯

什么是 RPC

  Restful 采用 Http 进行通讯,优点是开放、标准、简单、兼容性升级容易;缺点是性能略低。在 QPS 高或者对响应时间要求苛刻的服务上,可以用 RPC(Remote Procedure Call),RPC 由于采用二进制传输、TCP 通讯,所以通常性能更好。

  .Net Core 下的 RPC(远程方法调用)框架有 gRPC、Thrift 等,都支持主流的编程语言。

  RPC 虽然效率略高,但是耦合性强,如果兼容性处理不好的话,一旦服务器端接口升级,客户端就要更新,即使是增加一个参数,而 rest 则比较灵活。

  最佳实践:对内一些性能要求高的场合用 RPC,对内其他场合以及对外用 Rest。比如 web 服务器和视频转码服务器之间通讯可以用 restful 就够了,转账接口用 RPC 性能会更高一些。

Thrift基本使用

参考资料:https://www.cnblogs.com/focus-lei/p/8889389.html

1、下载thrift http://thrift.apache.org/

  把thrift-***.exe解压到磁盘,改名为thrift.exe(用起来方便一些)

2、编写一个UserService.thrift文件(IDL(中间定义语言))

Asp.Net Core微服务再体验

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;
}

Asp.Net Core微服务再体验

service定义的是服务类,enum是枚举,struct是传入或者传出的复杂数据类型(支持对象级联)。

语法规范http://thrift.apache.org/docs/idl

根据thrift语法生成C#代码

thrift.exe -gen csharp UserService.thrift

Asp.Net Core微服务再体验

创建一个类库项目 ThriftTest1.Contract,作为客户端和服务器之间的共用协议,把上一步生成的代码放进项目。

项目nuget安装apache-thrift-netcore:

Install-Package apache-thrift-netcore

然后将生成的文件拷贝到项目中,并重新生成项目

创建服务器端项目 ThriftTest1.Server,建一个控制台项目(放到 web 项目中或者在 Linux中用守护进程运行起来(SuperVisor等,类似Windows下的“Windows服务”)也可以)。

ThriftTest1.Server项目引用ThriftTest1.Contract

编写实现类UserServiceImpl.cs:

Asp.Net Core微服务再体验   View Code

修改Program.cs

Asp.Net Core微服务再体验   View Code

监听8800端口

创建客户端项目 ThriftTest1.Client,建一个控制台项目(放到 web 项目中或者在 Linux中用守护进程运行起来(SuperVisor等,类似Windows下的“Windows服务”)也可以)。

ThriftTest1.Server项目引用ThriftTest1.Contract 

修改Program.cs

Asp.Net Core微服务再体验   View Code

分别启动:

Asp.Net Core微服务再体验

一个服务器中放多个服务

0.9.1之前只支持一个服务器一个服务,这也是建议的做法。之后支持多路服务在thrift中增加一个服务

service CalcService{  
  i32 Add(1:i32 i1,2:i32 i2) 
}

服务器

新增实现类CalcServiceImpl.cs

Asp.Net Core微服务再体验   View Code

修改Program.cs

Asp.Net Core微服务再体验   View Code

客户端

修改Program.cs

Asp.Net Core微服务再体验   View Code

分别启动:

Asp.Net Core微服务再体验

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的版本一致):

Asp.Net Core微服务再体验   View Code

在thrift的IDL文件中加入一行(各个语言的namespace等参数可以共存) 

namespace java com.rupeng.thriftTest1.contract 就可以控制生成的java类的报名,最好按照java的命名规范来。 

thrift.exe -gen java UserService.thrift

产生java代码 

Java代码:

Asp.Net Core微服务再体验   View Code

也可以用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

 Asp.Net Core微服务再体验

因为 Thrift 一般不对外,所以一般不涉及和 API 网关结合的问题 

不是所有项目都适合微服务架构,互联网项目及结构复杂的企业信息系统才可以考虑微服务架构。

设计微服务架构,模块拆分的原则:可以独立运行,尽量服务间不要依赖,即使依赖层级也不要太深,不要想着还要 join。按业务划分、按模块划分。

扩展知识

1、 分布式跟踪、日志服务、监控等对微服务来说非常重要

2、 gRPC 另外一个 RPC 框架,gRPC 的.Net Core 支持异步。

3、 https://github.com/neuecc/MagicOnion 可以参考下这位日本 mvp 写的 grpc 封装,不需要定义接口文件。

4、 nanofabric https://github.com/geffzhang/NanoFabric 简单分析

5、 Surging https://github.com/dotnetcore/surging

6、 service fabric https://azure.microsoft.com/zh-cn/documentation/learning-paths/service-fabric/

7、 Spring Cloud 入门视频:http://www.rupeng.com/Courses/Chapter/755

8、 steeltoe http://steeltoe.io/

9、 限流算法 https://mp.weixin.qq.com/s/bck0Q2lDj\_J9pLhFEhqm9w

10、https://github.com/PolicyServer/PolicyServer.Local 认证 + 授权 是两个服务, identityserver 解决了认证 ,PolicyServer 解决授权

11、CSharpKit 微服务工具包 http://www.csharpkit.com/

12、如鹏网.Net 提高班 http://www.rupeng.com

源码位置:https://download.csdn.net/download/qq_25153485/10538936

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Easter79 Easter79
3年前
springcloud eureka.instance
1.在springcloud中服务的 InstanceID默认值是:${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance\_id:${server.port}},也就是:主机名:应用名:应用端口。如图1
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这