一、通讯协议选型

1、HTTP REST Api

根据微服务的定义,服务间需要使用轻量级的通讯协议,可以选择的有http rest api,是基于http上的一种与语言无关的通讯协议,主要使用json数据格式进行传输。

2、GRPC

由于http协议本身的特性,在连接使用完成后,都会断开,下次需要的时候再重新创建连接,如果服务间的调用比较频繁的话,那么频繁的创建和断开连接就会导致一些不必要的性能损失。后来google公司推出了grpc,也是一种与语言无关的通讯协议,直接使用tcp连接,也可以在http2上进行使用,默认使用protobuf序列化协议,也支持使用json,但protobuf是直接序列化为二进制流,所以更高效一些。连接的话可以一直保留,避免了连接的频繁创建和断开。建议在网关与服务,服务与服务之间的调用使用grpc协议。

二、调用时相关策略

1、服务发现/负载均衡(策略)

是指当从服务治理那里获取到要调用的服务有多个运行实例时,需要根据一定的负载均衡策略来选择一个进行调用,最简单的就是随机。

2、熔断策略

是一种对下游服务的保护措施。跟保险丝保护家里的电器原理一样。当连续调用下游服务都失败或者超时时,可能此时下游服务确实太繁忙了,如果此时调用,失败的可能性仍然会非常大,并且可能由于更多的调用直接导致下游服务崩溃。所以需要一定的策略,在哪些情况下,就不再真实调用下游服务,而是直接返回一个失败给上游。

3、降级策略

是一种对核心业务的保护。
当流量太大时,可以关停一些不必要的非核心业务,让更多的资源来保证核心业务的运行。
由于核心业务不再发起对非核心业务的调用,一定程度上减少了核心业务请求的处理时间,减少了调用环节,增加了可用性。
需要在配置中心有地方可以设置哪些是非核心业务,并且是否停用,也可以考虑直接通过服务治理那里,将所有非信心业务服务停掉,取不到这些服务地址时就认为是停掉。

4、容错策略

这是一种网络容错的策略,比如失败重试或者失败后自动尝试另外一个服务实例的调用,但此时要注意与熔断策略的冲突,容错需要限制一定的数量,否则一个请求会被放大为好多请求,并且要求下游服务要能够做到幂等效应才可以。

三、GRPC的使用步骤

微软官方的在asp.net core中使用grpc的文档 https://docs.microsoft.com/zh-cn/aspnet/core/grpc/?view=aspnetcore-3.1

1、创建proto接口文件

既然是用于服务之间的调用的,那么首先肯定需要定义api接口,在grpc中,是使用proto文件来定义接口的,其中定义了接口名称,接收参数和返回内容等。

  1. syntax = "proto3";
  2. package Com.Gshis.Services;
  3. service LoginService{
  4. rpc Login(LoginRequest) returns(LoginResponse);
  5. }
  6. message LoginRequest{
  7. string Hid = 1;
  8. string UserName = 2;
  9. string Password = 3;
  10. }
  11. message LoginResponse{
  12. bool Success = 1;
  13. string ErrMsg = 2;
  14. string Token = 3;
  15. }

详细protobuf文件语法可以参考:https://www.yuque.com/kevin_study/micro_service/qnahcn

2、根据proto接口文件生成相应的类

在.net项目中,只需要将proto文件包含在项目里面,并且项目添加nuget包
image.png
并且设置ptoto文件的编译动作为Protobuf compilier,设置后再次查看属性时,就会弹出下面的窗口,
image.png
做好以上两步后,更改proto文件后,只需要重新编译一次项目,就会自动生成ServerBase和Client类,ServerBase是需要我们重新实现的一个抽象类,用于实现服务,Client类是一个stub类,用于客户端调用服务时使用的。

3、实现服务

using Com.Gshis.Services;
using Grpc.Core;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using JWT.Builder;
using JWT.Algorithms;

namespace LoginDemo
{
    public class LoginServiceImp:LoginService.LoginServiceBase
    {
        public override Task<LoginResponse> Login(LoginRequest request, ServerCallContext context)
        {
            if(request != null && "000001".Equals(request.Hid) && "admin".Equals(request.UserName) && "Jxd598".Equals(request.Password))
            {
                // calc jwt token
                var secretKey = "jxd598@gshis.com";
                var token = new JwtBuilder()
                    .WithAlgorithm(new HMACSHA512Algorithm())
                    .WithSecret(secretKey)
                    .ExpirationTime(DateTime.Now.AddHours(8))
                    .IssuedAt(DateTime.Now)
                    .Issuer("Gemstar")
                    .AddClaim("Hid", request.Hid)
                    .AddClaim("UserCode", request.UserName)
                    .Build();

                return Task.FromResult(new LoginResponse
                {
                    Success = true,
                    ErrMsg = "",
                    Token = token
                });
            }
            return Task.FromResult(new LoginResponse
            {
                Success = false,
                ErrMsg = "请指定有效的登录参数"
            });
        }
    }
}

注:其中的LoginService.LoginServiceBase是自动从proto文件中生成的,在项目中找不到此文件。

4、在服务端启动grpc服务

 var firstIp = GetFirstIp4();
var server = new Server
{
    Ports = { new ServerPort(firstIp, 0, ServerCredentials.Insecure) },
    Services = { LoginService.BindService(new LoginServiceImp()) }
};
server.Start();

启动服务很简单,只需要指定ip和端口,指定0表示由系统分配一个未使用的端口,然后将我们实现的服务实例放到server实例里面,调用server的start方法即可运行服务,监听grpc连接请求和调用。

5、在客户端连接并调用服务

var channel = new Channel($"{service.Service.Address}:{service.Service.Port}", ChannelCredentials.Insecure);
var loginClient = new LoginService.LoginServiceClient(channel);
var loginResult = await loginClient.LoginAsync(new LoginRequest
{
    Hid = model.Hid,
    Password = model.Password,
    UserName = model.UserCode
});

客户端使用时,需要先创建一个channel连接(连接应尽量重用,而不是每次调用都重新创建一个),创建连接时需要指定服务运行时的ip和端口,然后生成一个客户端实例,即可在客户端实例上调用相应的方法。