使用ASP.NET Core实现整个应用程序
先看看A服务的实现,它比较简单,在ASP.NET Core的项目上,新建一个控制器(Controller),然后曝露RESTful API向外界提供计算服务即可。控制器代码如下:[Route("api/[controller]")] [ApiController] public class CalcController : ControllerBase { [HttpGet("add/{x}/{y}")] public int Add(int x, int y) => x + y; [HttpGet("sub/{x}/{y}")] public int Sub(int x, int y) => x - y; [HttpPost("stddev")] public double StdDev([FromBody]float[] numbers) => Math.Sqrt(numbers.Select(x => Math.Pow(x - numbers.Average(), 2)).Sum() / (numbers.Length - 1)); }A服务其它部分的代码就不多说明了,都是标准做法,没有什么特别。再看看B服务,其实除了通过外部数据源获取指定城市5年来每天的平均气温数据,以及调用A服务计算标准差之外,也没有什么特别之处,主要代码如下:
[Route("api/[controller]")] [ApiController] public class WeatherController : ControllerBase { const string StdDevCalculationApiURI = "http://localhost:49814/api/calc/stddev"; static readonly Dictionary为了便于演示,我已经预先将气温数据序列化成一个JSON文件,所以在上面的代码中,WeatherController在初次被引用时,会将气温数据从JSON文件中反序列化出来,并读入到weatherData字典中。在StdDevForCity的方法中可以看到,此API通过HttpClient向A服务发起了计算标准差的请求,并将平均气温数据以Post Body的形式代入到发出的请求中。 接下来,我们的前端页面需要使用A服务进行一些纯粹的数学计算,并且使用B服务为用户提供一些天气数据的统计信息。为了简化描述,这个前端应用我也采用ASP.NET Core来实现。使用Visual Studio创建了一个ASP.NET Core MVC的应用程序,然后,添加一个API的页面,在HomeController中,使用如下代码为API页面提供数据模型:>> weatherData; static WeatherController() { weatherData = JsonConvert.DeserializeObject >>>(System.IO.File.ReadAllText("weather_data.json")); } [HttpGet("stddev/{city}")] public async Task StdDevForCity(string city) { if (!weatherData.ContainsKey(city.ToUpper())) { return BadRequest($"城市'{city}'的气象数据不存在。"); } using (var client = new HttpClient()) { var response = await client.PostAsJsonAsync(StdDevCalculationApiURI, weatherData[city.ToUpper()].Select(x => x.Item2).ToArray()); response.EnsureSuccessStatusCode(); return Ok(Convert.ToDouble(await response.Content.ReadAsStringAsync())); } } }
public class HomeController : Controller { const string CalculationServiceUri = "http://localhost:49814"; const string WeatherServiceUri = "http://localhost:50379"; public async Task然后,使用下面的视图(View)将数据模型显示给最终用户:API() { using (var client = new HttpClient()) { // 调用计算服务,计算两个整数的和与差 const int x = 124, y = 134; var sumResponse = await client.GetAsync($"{CalculationServiceUri}/api/calc/add/{x}/{y}"); sumResponse.EnsureSuccessStatusCode(); var sumResult = await sumResponse.Content.ReadAsStringAsync(); ViewData["sum"] = $"x + y = {sumResult}"; var subResponse = await client.GetAsync($"{CalculationServiceUri}/api/calc/sub/{x}/{y}"); subResponse.EnsureSuccessStatusCode(); var subResult = await subResponse.Content.ReadAsStringAsync(); ViewData["sub"] = $"x - y = {subResult}"; // 调用天气服务,计算大连和广州的平均气温标准差 var stddevShenyangResponse = await client.GetAsync($"{WeatherServiceUri}/api/weather/stddev/shenyang"); stddevShenyangResponse.EnsureSuccessStatusCode(); var stddevShenyangResult = await stddevShenyangResponse.Content.ReadAsStringAsync(); ViewData["stddev_sy"] = $"沈阳:{stddevShenyangResult}"; var stddevGuangzhouResponse = await client.GetAsync($"{WeatherServiceUri}/api/weather/stddev/guangzhou"); stddevGuangzhouResponse.EnsureSuccessStatusCode(); var stddevGuangzhouResult = await stddevGuangzhouResponse.Content.ReadAsStringAsync(); ViewData["stddev_gz"] = $"广州:{stddevGuangzhouResult}"; } return View(); } }
@{ ViewData["Title"] = "API"; }API
计算服务调用测试
@ViewData["sum"]
@ViewData["sub"]
天气服务调用测试
2013年1月1日至2018年10月26日各城市平均气温标准差为:
- @ViewData["stddev_sy"]
- @ViewData["stddev_gz"]
- 在一个微服务架构的应用程序中,业务逻辑根据界定上下文分隔,并在不同的微服务中实现。所以,这样的应用程序通常会有几个、十几个甚至几十个微服务在运行、在调度、在伸缩。我们一个小小的演示案例就牵涉到了两个服务之间的互相调用,以及一个前端页面需要同时依赖两个服务的情况,更不用说在真实的应用中。比如,曾经有过报道,亚马逊商品信息页面后端就牵涉到几十个微服务,如果让前端页面对这几十个后端API进行逐个调用,就需要在前端应用中配置几十个API的地址,这样维护起来会非常麻烦而且容易出错
- 前端直接访问后端的API,会增加客户端与服务端之间的网络负载,比如:前端页面如果发出上百个API请求,那么就会有上百个来回于客户端和服务端的网络传输;但如果我们能够在服务端完成这些API的调用,并将结果组合起来一次发给客户端,那么网络传输就会变得更加高效
- 前端直接访问后端的API不利于微服务的治理和重构。比如:微服务容器重启之后,内部IP地址会发生变化,如果强依赖于IP,那么所有前端页面都需要更改。在今后维护后端代码时候,也会有可能对API的URI进行重构,也会导致URI维护出现困难和错误
- 前端直接访问后端的API,使得后端API不得不将访问接口曝露到公共域,而这样就加大了整个系统被攻击的可能性。比如,攻击A服务不成功,我可以尝试攻击B服务,一旦有一个服务受到攻击,就会对整个应用程序造成影响,带来安全隐患
- 与非功能性需求相关的技术实现,比如缓存、身份认证等等,都需要在每个微服务上进行实现,而绝大多数情况下,这些实现都是非常类似的,无需重复实现,如果在前端页面(客户端)与服务端之间有另外一层能够处理这部分逻辑,那么,后端微服务的实现就会得到简化
在ASP.NET Core应用程序中使用Ocelot API网关
API网关是这样一种机制,它能够向服务端外界提供访问内部服务的统一接口,并且提供一定的负载均衡、安全认证、缓存等应用特性。在微服务实践领域,API网关可以有很多种实现,比如可以使用Nginx,这也是我在《 ASP.NET Core应用程序容器化、持续集成与Kubernetes集群部署(一)》一文中介绍的案例tasklist所使用的一种方式。在这里,我还是主要介绍ASP.NET Core下API网关的一种实现: Ocelot,其实,网上介绍Ocelot的文章已经有很多了,在这里,我只想由浅入深地将微服务实践中API网关的应用进行一个系统性的探讨,在接下来的文章中,我还会结合Ocelot对微服务的注册、发现以及容器部署进行更深入的实践。另外值得一提的是,国内.NET开源先锋, 张善友队长也是Ocelot项目的贡献者之一。 在使用Ocelot之前,先了解一下,上述的应用程序架构会产生哪些变化。最直接的结果就是,在API用户与A、B两个服务之间,会多出API网关这一层,整体结构会是下面这个样子: 现在开始改造我们的应用程序,来看看如何在ASP.NET Core的应用程序中使用Ocelot实现上述架构。 首先,新建一个 空的ASP.NET Core应用程序,这里强调“空的”,意味着我们不需要实现任何控制器(Controller),只需要借用ASP.NET Core的运行机制即可。创建空的ASP.NET Core应用程序是指,在下面的对话框中,选择Empty模板: 然后,添加Ocelot NuGet包的引用,可以通过Install-Package命令行,也可以在Visual Studio中右键点击刚刚新建的项目,选择Manage NuGet Packages选项,具体步骤就不说明了。添加完成后,就会在项目依赖项的NuGet节点下,看到Ocelot的引用: 接下来,在项目中添加一个配置Ocelot的JSON文件,我们就用ocelot.configuration.json吧,内容如下:{ "ReRoutes": [ { "DownstreamPathTemplate": "/api/calc/add/{x}/{y}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 49814 } ], "UpstreamPathTemplate": "/add/{x}/{y}", "UpstreamHttpMethod": [ "Get" ] }, { "DownstreamPathTemplate": "/api/calc/sub/{x}/{y}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 49814 } ], "UpstreamPathTemplate": "/sub/{x}/{y}", "UpstreamHttpMethod": [ "Get" ] }, { "DownstreamPathTemplate": "/api/calc/stddev", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 49814 } ], "UpstreamPathTemplate": "/stddev", "UpstreamHttpMethod": [ "Post" ] }, { "DownstreamPathTemplate": "/api/weather/stddev/{city}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 50379 } ], "UpstreamPathTemplate": "/weather-stddev/{city}", "UpstreamHttpMethod": [ "Get" ] } ], "GlobalConfiguration": { "RequestIdKey": "OcRequestId", "AdministrationPath": "/administration" } }在ReRoutes部分定义了URL重定向的规则,比如,当Ocelot服务接收到/sub/{x}/{y}的Get请求时,它就会把请求转发到 http://localhost:49814/api/calc/sub/{x}/{y}上。目前Ocelot的配置还是相对简单的,仅定义了一些重定向的规则,也没有使用Ocelot的一些高级功能。不过,这些设定对于我们实现上面的软件架构已经绰绰有余了。网上有很多文章介绍Ocelot的配置文件,官网也有详细说明,这里就不多解释了。 然后,修改Program.cs文件,修改WebHostBuilder的构建过程:
public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .ConfigureAppConfiguration((configBuilder) => { configBuilder.AddJsonFile("ocelot.configuration.json"); }) .ConfigureServices((buildContext, services) => { services.AddOcelot(); }) .UseStartup注意上面代码的高亮部分,基本上就是将配置文件载入,然后加入Ocelot的Middleware即可。OK,就这么简单,我们的API网关就完成了! 接下来的事情就显而易见了,将前面所介绍的前端MVC的代码,从分别向A、B两个服务发出请求,改为仅向API网关发出请求即可。例如,可以将MVC中HomeController的代码改造如下:() .Configure(app => { app.UseOcelot().Wait(); }); }
public class HomeController : Controller { const string ServiceUri = "http://localhost:59495"; public async Task怎么,现在改为将API网关的URL地址hard code在代码里了?暂时还是先别纠结URL是hard code还是在配置文件中,后面的文章中我会逐渐改善这部分代码的。API() { using (var client = new HttpClient()) { // 调用计算服务,计算两个整数的和与差 const int x = 124, y = 134; var sumResponse = await client.GetAsync($"{ServiceUri}/add/{x}/{y}"); sumResponse.EnsureSuccessStatusCode(); var sumResult = await sumResponse.Content.ReadAsStringAsync(); ViewData["sum"] = $"x + y = {sumResult}"; var subResponse = await client.GetAsync($"{ServiceUri}/sub/{x}/{y}"); subResponse.EnsureSuccessStatusCode(); var subResult = await subResponse.Content.ReadAsStringAsync(); ViewData["sub"] = $"x - y = {subResult}"; // 调用天气服务,计算大连和广州的平均气温标准差 var stddevShenyangResponse = await client.GetAsync($"{ServiceUri}/weather-stddev/shenyang"); stddevShenyangResponse.EnsureSuccessStatusCode(); var stddevShenyangResult = await stddevShenyangResponse.Content.ReadAsStringAsync(); ViewData["stddev_sy"] = $"沈阳:{stddevShenyangResult}"; var stddevGuangzhouResponse = await client.GetAsync($"{ServiceUri}/weather-stddev/guangzhou"); stddevGuangzhouResponse.EnsureSuccessStatusCode(); var stddevGuangzhouResult = await stddevGuangzhouResponse.Content.ReadAsStringAsync(); ViewData["stddev_gz"] = $"广州:{stddevGuangzhouResult}"; } return View(); } }