微服务架构 + 微信小程序
文章目录
1.微服务架构
- 如果国家比较小,可以直接把国王的命令传到每个百姓厕所前边。
- 国家比较大,要一层层往下分
如果程序不是特别的大,可以直接使用一个项目来写。
如果程序稍微大点,可以借助 redis 实现分布式缓存。
如果再大的话,就需要用服架构了。
如果把这个思想一个用于电商系统,则图示如下:
在这个架构里面:下边的
- 服务就是说 webApi ,
- 商品负载就是 consul(领事): 他就负责将下边的功能登记在册,如果皇上需要,他就查查有没有,然后给皇上说声,然后让下边的士兵过去。
- 网管就是 Ocelot (豹猫)
2. consul 的配置
consul是一个软件
1.去官网下载 consul 网址 https://www.consul.io/
2.在 consul 文件夹下cmd 输入: consul.exe agent -dev
3.其默认的端口号是:8500
直接在浏览器输入: http://localhost:8500,就可以调出 Services-Consul 窗口(这是下边第三部的东西配置完以后才有的界面)
配置 swagger 的使用,比较简单,就不写了
注意配置 swagger 的时候的一个坑,使用的时候,每个action方法上都要httpget方法,不然的话,会报下边的错误。
3.在 .net core 中手写 Consul 中间件
1.建立一个 ConsulManager 类,作为静态扩展文件,所有的类,包括方法,都必须要是静态的。
在包中引入Consul 中间件.
具体代码如下:
public static class ConsulManager
{
//扩展方法必须是静态的。
public static void UseConsul(this IApplicationBuilder app, IConfiguration configuration, IConsulClient consul)
{
Regserver(configuration,consul);
}
private static void Regserver(IConfiguration configuration, IConsulClient consul)
{
//微服务架构 : 需要知道的三个点,所属的组,我的姓, 我的名;
//下边命令行都是通过命令行传入的
string consulGroup = configuration["ConsulGroup"];
string ip = configuration["ip"];//这个是启动时传入的;
int port = Convert.ToInt32(configuration["port"]);
//把这个当兵的注册到军籍里面
string serviceId = $"{consulGroup}_{ip}_{port}";
var check = new AgentServiceCheck //检测客户端是否还在运行
{
Interval = TimeSpan.FromSeconds(6),//3s问一次
HTTP = $"http://{ip}:{port}/heart",//去这个地址访问;
Timeout = TimeSpan.FromSeconds(2),
DeregisterCriticalServiceAfter=TimeSpan.FromSeconds(2),//2s 以后就把这个人从军籍里撤销掉.
};
//把注册列表填一下
var regist = new AgentServiceRegistration
{
//把我的姓我的名填一下;
Address = ip,
Port = port,
Name = consulGroup,
ID = serviceId, //这个功能还不够智能,需要重新组装一下;
//把检测功能也搞进来:
// Check = check,
};
consul.Agent.ServiceRegister(regist);//保存注册;
}
}
2.在静态文件里面,把 consul 的组名穿进去
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConsulGroup": "userservice"
}
3.添加检测是否失效的类:在 Controller 里面 直接 return Ok() 就行
[Route("[controller]")]
[ApiController]
public class HeartController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
return Ok();
}
}
4.在 startup 类里面,依赖注入,并使用中间件,代码如下:
services.AddSingleton<IConsulClient>(c=>new ConsulClient(cc=>
{
cc.Address = new Uri("http://localhost:8500");//ConsulClient 的默认端口
}
));
public void Configure(IApplicationBuilder app, IWebHostEnvironment env,IConsulClient consul)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthorization();
app.UseSwagger();
app.UseSwaggerUI(option=>
{
option.SwaggerEndpoint("/swagger/v1/swagger.json","user api"); //第二个名字要和之前的一样。
} );
app.UseConsul(Configuration,consul);
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
5.启动程序
可以使用微服务:
配置好的地址 运行 dotnet UserServer.dll --urls=“http://*:5196” --ip=“127.0.0.1” --port=5196
在运行一个:dotnet UserServer.dll --urls=“http://*:5197” --ip=“127.0.0.1” --port=5197
再运行一个: dotnet UserServer.dll --urls http://*:5001 --ip=“127.0.0.1” --port=5001
此处的 iP 和 port 是必填的,因为命令行启动要用
注意 127.0.0.1是回送地址。在window 系统中还有一个别名 “Localhost”
6.最终效果如下:
- 有两个 consul 小组: consul:领事
- userservice小组有两个人:
userservice小组 里面有了两个人:
6.2配置了检测以后
按 ctrl + C 。模拟关闭关闭了,如下:
4.配置网关(Ocelot)
1.引入nuge包
引入 Ocelot 包,使用 15.0.6 ,最新版本有问题。
在Startup文件里进行配置:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
//注入,引用
services.AddOcelot();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
//中间件管道引用
app.UseOcelot();
}
}
2.添加配置文件:
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ReRoutes":[
{
"DownstreamPathTemplate": "/{url}", //下端访问的地址
"DownstreamScheme": "http", //下段访问的方式
//绕过了consul
"DownstreamHostAndPorts": [
{
"Host": "127.0.0.1",
"Port": 5001 //下端访问的地址。
} //可以多个,自动实现负载均衡。
],
"UpstreamPathTemplate": "/{url}", //网管地址。
"UpstreamHttpMethod": [ "Get", "Post", "Put", "Delete" ]
//上端可以访问的方式。
}
]
3.进行测试
路由输入 6001/test
结果调到了 UserServer 的方法里面
但是此时,OcelotGateway 的中间件管道都已经注销了。
所以此时相当于 Nginx 均衡负载功能。
其实,consul 有点类似于均衡负载的功能,上边只要调用,你下边给我调哪个兵我不管。
4.怎么样调用consul?
引用 nuget 包,观察目录结构:
更改配置文件:
把原来的给注销掉:
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ReRoutes": [
{
//"DownstreamPathTemplate": "/{url}", //下端访问的地址
//"DownstreamScheme": "http", //下段访问的方式
绕过了consul
//"DownstreamHostAndPorts": [
// {
// "Host": "127.0.0.1",
// "Port": 5001 //下端访问的地址。
// } //可以多个,自动实现负载均衡。
//],
//"UpstreamPathTemplate": "/{url}", //网管地址。
//"UpstreamHttpMethod": [ "Get", "Post", "Put", "Delete" ], //上端可以访问的方式。
"DownstreamPathTemplate": "/{url}", //下端访问的地址
"DownstreamScheme": "http", //下段访问的方式
"UpstreamPathTemplate": "/{url}", //网管地址。
"UpstreamHttpMethod": [ "Get", "Post", "Put", "Delete" ], //上端可以访问的方式。
"ServiceName": "userservice", //哪个consul
"LoadBalancerOptions": {
"Type": "RoundRobin" //轮询的方式调用.
},
"UserServiceDiscovery": true //是否启用服务发现?当然。
}
],
//全局配置
"GlobalConfiguration": {
"BaseUrl": "http://127.0.0.1:5001",
"Host": "loaclhost",//Host 是本地
"Port": "8500",
"Type": "Consul"
}
使用 命令行 启动:
dotnet OcelotGatewag.dll --urls http://*:5001 --ip=“127.0.0.1” --port=6001
注意,最终的启动命令是 url 。–port 只是起到传递参数的作用。
此时在 http:localhost:6001/test,上测试,可以通过。
5.让每次访问都显示他的网关
如何把网关搞出来,靠依赖注入的方法引入:
[Route("test")]
[ApiController]
public class testController : ControllerBase
{
private readonly IConfiguration configuration;
public testController(IConfiguration configuration)
{
this.configuration = configuration;
}
public string GetSomething()
{
var portss = configuration["port"];
return $"你好,恭喜你已经进入{portss}了 UserServer 下面的 test 方法";
}
这样再执行
http://localhost:6001/test
每次刷新得到的结果都不一样:
也可以直接输入 swagger界面进行查看
5.添加第二个服务。
1.新建 ProductService 类
里面的内容和 UserServer 类里面的内容一样,
也是需要添加两个包。
不同点是:在 appseting 文件里面,添加第二个组名:
只需要更改配置文件,这里也可以看出来使用配置文件的好处。
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConsulGroup": "productservice"
}
运行后进入 consul ,能看到有三个组,如下:
2.配置 第二个服务的Swagger
和第一个一样,比较简单。注意两个 “给人看的名字" ,到底是页面的什么地方。
代码部分:
1.依赖注入的部分。
option.SwaggerDoc //swaggerDoc 配置文档.
("v1", new OpenApiInfo//第一版
{
Title = "Product api 大标题",
Version = "v1", //做个版本好是给人引入的。
}));
2.中间件管道部分
app.UseSwagger();//使用Swagger
app.UseSwaggerUI //使用 Swagger 界面。
(option =>
{
//第二个名字要和之前的一样。
//为什么,应为swagger 会先转换成 JSOn文件,然后再展现出来
option.SwaggerEndpoint("/swagger/v1/swagger.json", "Product api 中间件管道");//名字
}
);
界面如下:
3.配置 Ocelot
1.再添加一个consul小组
“UpstreamPathTemplate”: “/product/{url}”,
根据定制的规则,可以把下边挂的两个服务找到。
{
"DownstreamPathTemplate": "/{url}", //下端访问的地址
"DownstreamScheme": "http", //下段访问的方式
"UpstreamPathTemplate": "/user/{url}", //网管地址。
"UpstreamHttpMethod": [ "Get", "Post", "Put", "Delete" ], //上端可以访问的方式。
"ServiceName": "userservice", //哪个consul
"LoadBalancerOptions": {
"Type": "RoundRobin" //轮询的方式调用.
},
"UserServiceDiscovery": true //是否启用服务发现?当然。
},
{
"DownstreamPathTemplate": "/{url}", //下端访问的地址
"DownstreamScheme": "http", //下段访问的方式
"UpstreamPathTemplate": "/product/{url}", //网管地址。//两个 consul 的时候,就要添加前缀了。
"UpstreamHttpMethod": [ "Get", "Post", "Put", "Delete" ], //上端可以访问的方式。
"ServiceName": "productservice", //哪个consul
"LoadBalancerOptions": {
"Type": "RoundRobin" //轮询的方式调用.
},
"UserServiceDiscovery": true //是否启用服务发现?当然。
结果:
用 consul 测试 product 路由:
2.测试 user 路由:
*但是这样配置的swagger,依旧是从,6001的端口号进去的,这样说明配的依然不对,继续更改配置文件。
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ReRoutes": [
{
"DownstreamPathTemplate": "/swagger/user/swagger.json", //下端访问的地址
"DownstreamScheme": "http", //下段访问的方式
"UpstreamPathTemplate": "/user/swagger.json", //网管地址。
"UpstreamHttpMethod": [ "Get", "Post", "Put", "Delete" ], //上端可以访问的方式。
"ServiceName": "userservice", //哪个consul
"UserServiceDiscovery": true //是否启用服务发现?当然。
},
{
"DownstreamPathTemplate": "/user/{url}", //下端访问的地址
"DownstreamScheme": "http", //下段访问的方式
"UpstreamPathTemplate": "/user/{url}", //网管地址。
"UpstreamHttpMethod": [ "Get", "Post", "Put", "Delete" ], //上端可以访问的方式。
"ServiceName": "userservice", //哪个consul
"LoadBalancerOptions": {
"Type": "RoundRobin" //轮询的方式调用.
},
"UserServiceDiscovery": true //是否启用服务发现?当然。
},
{
"DownstreamPathTemplate": "/swagger/product/swagger.json", //下端访问的地址
"DownstreamScheme": "http", //下段访问的方式
"UpstreamPathTemplate": "/product/swagger.json", //网管地址。//两个 consul 的时候,就要添加前缀了。
"UpstreamHttpMethod": [ "Get", "Post", "Put", "Delete" ], //上端可以访问的方式。
"ServiceName": "productservice", //哪个consul
"UserServiceDiscovery": true //是否启用服务发现?当然。
}
,
{
"DownstreamPathTemplate": "/product/{url}", //下端访问的地址
"DownstreamScheme": "http", //下段访问的方式
"UpstreamPathTemplate": "/product/{url}", //网管地址。//两个 consul 的时候,就要添加前缀了。
"UpstreamHttpMethod": [ "Get", "Post", "Put", "Delete" ], //上端可以访问的方式。
"ServiceName": "productservice", //哪个consul
"LoadBalancerOptions": {
"Type": "RoundRobin" //轮询的方式调用.
},
"UserServiceDiscovery": true //是否启用服务发现?当然。
}
],
//全局配置
"GlobalConfiguration": {
"BaseUrl": "http://127.0.0.1:5001",
"Host": "loaclhost",//Host 是本地
"Port": "8500",
"Type": "Consul"
}
}
以 user 服务为例,
更改 UserServer 的路由
ProductServices的路由配置和上边那个一样,就不在多写了,具体看 gitee 源代码。
4.最后实现的结果。
6.如何防止恶意调用API接口
思路就是:添加 ActionFilter 属性。在每次有请求的时候,都记录一下 请求的 ip 地址和 port 端口号,超过五次的话,就短路。
做个过程也叫做 “熔断机制”
ActionFilter 的添加的方法,执行的顺序
构造函数 => ActionFilter中的 ing 方法 => 函数本身的方法 => ActionFilter 的 ed 方法
具体的添加特性是:
注意两个用法
- 如何获得 RemoteIpAddress。
- 如何获得 RemotePort。
- addMinutes 用法。
- 短路机制
//只有继承了 attribute,才能放在别的类前边。
//只有继承了 ActionFilter的接口,才能实现 Actionfiler的功能。
public class CtmActionFilterAttribute : Attribute, IActionFilter
{
private static Dictionary<string, List<DateTime>> dicRequestTimes
= new Dictionary<string, List<DateTime>>();
public void OnActionExecuted(ActionExecutedContext context)
{
//throw new NotImplementedException();
}
public void OnActionExecuting(ActionExecutingContext context)
{
string ip = context.HttpContext.Connection.RemoteIpAddress.ToString();
int port = context.HttpContext.Connection.RemotePort;//远程路由的端口号
string requestname = $"{ip}_{port}";
//查找是否包含当前地址。
bool hasHost = dicRequestTimes.ContainsKey(requestname);
if (hasHost)
{
var allTimes = dicRequestTimes[requestname];
//访问次数为空或者小于五次
if (allTimes==null||allTimes.Count<5)
{
allTimes.Add(DateTime.Now);
}
else
{
//获取当前前一分钟的访问次数。
// Now.AddMinutes 想当与是选的时间点。
var timeCount = allTimes.Count(m => m>DateTime.Now.AddMinutes(-1));//m在第一分钟以后的。
if (timeCount>=5)
{
//使用断路器.
context.Result = new JsonResult("访问过于频繁,请稍候再试") { StatusCode=500};//状态码是500
}
else
{
allTimes.Add(DateTime.Now);
//把之前一分钟的内容都删掉;
var lessCount = allTimes.Count - timeCount;
allTimes.RemoveRange(0,lessCount-1);//为节约空间,把一分钟之前的都给删掉。
}
}
}
else
{
List<DateTime> value = new List<DateTime>();
value.Add(DateTime.Now);
dicRequestTimes.Add(requestname,value);
}
}
}
第一次点击:
连续点击五次以后。
但是由于每个程序是单独运行的。所以每个实例都会单独的计数,为了消除这种这种问题,一般采用的方法
- 在网管中进行配置
- 使用redis 分布式缓存