ASP.NET Core微服务之服务间的调用方式(REST and RPC)

释放双眼,带上耳机,听听看~!

Tip: 此篇已加入.NET Core微服务基础系列文章索引

一、REST or RPC ?

1.1 REST & RPC

微服务之间的接口调用通常包含两个部分,序列化和通信协议。常见的序列化协议包括json、xml、hession、protobuf、thrift、text、bytes等;通信比较流行的是http、soap、websockect,RPC通常基于TCP实现,常用框架例如dubbo,netty、mina、thrift。

REST:严格意义上说接口很规范,操作对象即为资源,对资源的四种操作(post、get、put、delete),并且参数都放在URL上,但是不严格的说Http+json、Http+xml,常见的http api都可以称为Rest接口。

RPC:即我们常说的远程过程调用,就是像调用本地方法一样调用远程方法,通信协议大多采用二进制方式。

1.2 HTTP vs 高性能二进制协议

HTTP相对更规范,更标准,更通用,无论哪种语言都支持HTTP协议。如果你是对外开放API,例如开放平台,外部的编程语言多种多样,你无法拒绝对每种语言的支持,相应的,如果采用HTTP,无疑在你实现SDK之前,支持了所有语言,所以,现在开源中间件,基本最先支持的几个协议都包含RESTful。

RPC协议性能要高的多,例如Protobuf、Thrift、Kyro等,(如果算上序列化)吞吐量大概能达到http的二倍。响应时间也更为出色。千万不要小看这点性能损耗,公认的,微服务做的比较好的,例如,netflix、阿里,曾经都传出过为了提升性能而合并服务。如果是交付型的项目,性能更为重要,因为你卖给客户往往靠的就是性能上微弱的优势。

所以,最佳实践一般是对外REST,对内RPC,但是追求极致的性能会消耗很多额外的成本,所以一般情况下对内一般也REST,但对于个别性能要求较高的接口使用RPC。

二、案例结构

ASP.NET Core微服务之服务间的调用方式(REST and RPC)

这里假设有两个服务,一个ClinetService和一个PaymentService,其中PaymentService有两部分,一部分是基于REST风格的WebApi部分,它主要是负责一些对性能没有要求的查询服务,另一部分是基于TCP的RPC Server,它主要是负责一些对性能要求高的服务,比如支付和支出等涉及到钱的接口。假设User在消费ClientService时需要调用PaymentService根据客户账户获取Payment History(走REST)以及进行交易事务操作(走RPC)。

三、REST调用

3.1 一个好用的REST Client : WebApiClient

使用过Java Feign Client的人都知道,一个好的声明式REST客户端可以帮我们省不少力。在.NET下,园子里的大大老九就写了一款类似于Feign Client的REST Client:WebApiClient。WebApiClient是开源在github上的一个httpClient客户端库,内部基于HttpClient开发,是一个只需要定义C#接口(interface),并打上相关特性,即可异步调用http-api的框架 ,支持.net framework4.5+、netcoreapp2.0和netstandard2.0。它的GitHub地址是:https://github.com/dotnetcore/WebApiClient

如何安装?

NuGet>Install-Package WebApiClient-JIT  

3.2 使用实例:走API Gateway

Step1.定义HTTP接口


1
2
3
4
5
6
7
8
9
1    [HttpHost("http://yourgateway:5000")]
2    public interface IPaymentWebApi: IHttpApi
3    {
4        // GET api/paymentservice/history/edisonzhou
5        // Return 原始string内容
6        [HttpGet("/api/paymentservice/history/{account}")]
7        ITask<IList<string>> GetPaymentHistoryByAccountAsync(string account);
8    }
9

这里需要注意的是,由于我们要走API网关,所以这里定义的HttpHost地址是一个假的,后面具体调用时会覆盖掉,当然你也可以直接把地址写在这里,不过我更倾向于写到配置文件中,然后把这里的HttpHost设置注释掉。

Step2.在Controller中即可异步调用:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1    [Route("api/[controller]")]
2    public class PaymentController : Controller
3    {
4        private readonly string gatewayUrl;public PaymentController(IConfiguration _configuration)
5        {
6            gatewayUrl = _configuration["Gateway:Uri"];
7        }
8
9        [HttpGet("{account}")]
10        public async Task<IList<string>> Get(string account)
11        {
12            using (var client = HttpApiClient.Create<IPaymentWebApi>(gatewayUrl))
13            {
14                var historyList = await client.GetPaymentHistoryByAccountAsync(account);
15                // other business logic code here
16                // ......
17                return historyList;
18            }
19        }
20  }
21

当然你也可以在Service启动时注入一个单例的IPaymentServiceWebApi实例,然后直接在各个Controller中直接使用,这样更加类似于Feign Client的用法:

(1)StartUp类注入


1
2
3
4
5
6
7
8
1    public void ConfigureServices(IServiceCollection services)
2    {
3
4        // IoC - WebApiClient
5        services.AddSingleton(HttpApiClient.Create<IPaymentServiceWebApi>(Configuration["PaymentService:Url"]));
6
7    }
8

(2)Controller中直接使用


1
2
3
4
5
6
7
8
1    [HttpPost]
2    public async Task<string> Post([FromBody]ModelType model, [FromServices]IPaymentServiceWebApi restClient)
3    {
4        ......
5        var result = await restClient.Save(model);
6        ......
7    }
8

这里PaymentService的实现很简单,就是返回了一个String集合:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1    // GET api/history/{account}
2    [HttpGet("{account}")]
3    public IList<string> Get(string account)
4    {
5        // some database logic
6        // ......
7        IList<string> historyList = new List<string>
8        {
9            "2018-06-10,10000RMB,Chengdu",
10            "2018-06-11,11000RMB,Chengdu",
11            "2018-06-12,12000RMB,Beijing",
12            "2018-06-13,10030RMB,Chengdu",
13            "2018-06-20,10400RMB,HongKong"
14        };
15
16        return historyList;
17    }
18

最终调用结果如下:

ASP.NET Core微服务之服务间的调用方式(REST and RPC)

3.3 使用实例:直接访问具体服务

在服务众多,且单个服务就部署了多个实例的情况下,我们可以通过API网关进行中转,但是当部分场景我们不需要通过API网关进行中转的时候,比如:性能要求较高,负载压力较小单个实例足够等,我们可以直接与要通信的服务进行联接,也就不用从API网关绕一圈。

Step1.改一下HTTP接口:


1
2
3
4
5
6
7
8
9
1    [HttpHost("http://paymentservice:8880")]
2    public interface IPaymentDirectWebApi: IHttpApi
3    {
4        // GET api/paymentservice/history/edisonzhou
5        // Return 原始string内容
6        [HttpGet("/api/history/{account}")]
7        ITask<IList<string>> GetPaymentHistoryByAccountAsync(string account);
8    }
9

同理,这里的HttpHost也是后面需要被覆盖的,原因是我们将其配置到了配置文件中。

Step2.改一下调用代码:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
1    [Route("api/[controller]")]
2    public class PaymentController : Controller
3    {
4        private readonly string gatewayUrl;
5        private readonly string paymentServiceUrl;
6
7        public PaymentController(IConfiguration _configuration)
8        {
9            gatewayUrl = _configuration["Gateway:Uri"];
10            paymentServiceUrl = _configuration["PaymentService:Uri"];
11        }
12
13        [HttpGet("{account}")]
14        public async Task<IList<string>> Get(string account)
15        {
16            #region v2 directly call PaymentService
17            using (var client = HttpApiClient.Create<IPaymentDirectWebApi>(paymentServiceUrl))
18            {
19                var historyList = await client.GetPaymentHistoryByAccountAsync(account);
20                // other business logic code here
21                // ......
22                return historyList;
23            }
24            #endregion
25        }
26

最终调用结果如下:

ASP.NET Core微服务之服务间的调用方式(REST and RPC)

四、RPC调用

4.1 Thrift简介

ASP.NET Core微服务之服务间的调用方式(REST and RPC)

Thrift是一个软件框架,用来进行可扩展且跨语言的服务的开发。它结合了功能强大的软件堆栈和代码生成引擎,以构建在 C++, Java, Go,Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 这些编程语言间无缝结合的、高效的服务。

当然,还有gRPC也可以选择,不过从网上的性能测试来看,Thrift性能应该优于gRPC 2倍以上,但是gRPC的文档方面要比Thrift友好很多。

4.2 Thrift的使用

(1)下载Thrift (这里选择Windows版)

ASP.NET Core微服务之服务间的调用方式(REST and RPC)

下载完成后解压,这里我将其改名为thrift.exe(去掉了版本号),一会在命令行敲起来更方便一点。

(2)编写一个PaymentService.thrift,这是一个IDL中间语言


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1namespace csharp Manulife.DNC.MSAD.Contracts
2
3service PaymentService {
4    TrxnResult Save(1:TrxnRecord trxn)
5}
6
7enum TrxnResult {
8    SUCCESS = 0,
9    FAILED = 1,
10}
11
12struct TrxnRecord {
13    1: required i64 TrxnId;
14    2: required string TrxnName;
15    3: required i32 TrxnAmount;
16    4: required string TrxnType;
17    5: optional string Remark;
18}
19

(3)根据thrift语法规则生成C#代码

cmd>thrift.exe -gen csharp PaymentService.thrift

ASP.NET Core微服务之服务间的调用方式(REST and RPC)

(4)创建一个Contracts类库项目,将生成的C#代码放进去

ASP.NET Core微服务之服务间的调用方式(REST and RPC)

4.3 增加RPC Server

(1)新增一个控制台项目,作为我们的Payment Service RPC Server,并引用Contracts类库项目

ASP.NET Core微服务之服务间的调用方式(REST and RPC)

(2)引入thrift-netcore包:

NuGet>Install-Package apache-thrift-netcore

(3)加入一个新增的PaymentService实现类


1
2
3
4
5
6
7
8
9
10
11
1    public class PaymentServiceImpl : Manulife.DNC.MSAD.Contracts.PaymentService.Iface
2    {
3        public TrxnResult Save(TrxnRecord trxn)
4        {
5            // some business logic here
6            //Thread.Sleep(1000 * 1);
7            Console.WriteLine("Log : TrxnName:{0}, TrxnAmount:{1}, Remark:{2}", trxn.TrxnName, trxn.TrxnAmount, trxn.Remark);
8            return TrxnResult.SUCCESS;
9        }
10    }
11

这里输出日志仅仅是为了测试。

(4)编写启动RPC Server的主程序


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1    public class Program
2    {
3        private const int port = 8885;
4
5        public static void Main(string[] args)
6        {
7            Console.WriteLine("[Welcome] PaymentService RPC Server is lanuched...");
8            TServerTransport transport = new TServerSocket(port);
9            var processor = new Manulife.DNC.MSAD.Contracts.PaymentService.Processor(new PaymentServiceImpl());
10            TServer server = new TThreadedServer(processor, transport);
11            // lanuch
12            server.Serve();
13        }
14    }
15

(5)如果是多个服务实现的话,也可以如下这样启动:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
1    public static void Main(string[] args)
2    {
3        Console.WriteLine("[Welcome] PaymentService RPC Server is lanuched...");
4        TServerTransport transport = new TServerSocket(port);
5        var processor1 = new Manulife.DNC.MSAD.Contracts.PaymentService.Processor(new PaymentServiceImpl());
6        var processor2 = new Manulife.DNC.MSAD.Contracts.PayoutService.Processor(new PayoutServiceImpl());
7        var processorMulti = new Thrift.Protocol.TMultiplexedProcessor();
8        processorMulti.RegisterProcessor("Service1", processor1);
9        processorMulti.RegisterProcessor("Service2", processor2);
10        TServer server = new TThreadedServer(processorMulti, transport);
11        // lanuch
12        server.Serve();
13    }
14

4.4 调用RPC

在ClientService中也引入apache-thrift-netcore包,然后在调用的地方修改如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
1    [HttpPost]
2    public string Post([FromBody]TrxnRecordDTO trxnRecordDto)
3    {
4        // RPC - use Thrift
5        using (TTransport transport = new TSocket(
6            configuration["PaymentService:RpcIP"],
7            Convert.ToInt32(configuration["PaymentService:RpcPort"])))
8        {
9            using (TProtocol protocol = new TBinaryProtocol(transport))
10            {
11                using (var serviceClient = new PaymentService.Client(protocol))
12                {
13                    transport.Open();
14                    TrxnRecord record = new TrxnRecord
15                    {
16                        TrxnId = GenerateTrxnId(),
17                        TrxnName = trxnRecordDto.TrxnName,
18                        TrxnAmount = trxnRecordDto.TrxnAmount,
19                        TrxnType = trxnRecordDto.TrxnType,
20                        Remark = trxnRecordDto.Remark
21                    };
22                    var result = serviceClient.Save(record);
23
24                    return Convert.ToInt32(result) == 0 ? "Trxn Success" : "Trxn Failed";
25                }
26            }
27        }
28    }
29
30    private long GenerateTrxnId()
31    {
32        return 10000001;
33    }
34

最终测试结果如下:

ASP.NET Core微服务之服务间的调用方式(REST and RPC)

五、小结

本篇简单的介绍了下微服务架构下服务之间调用的两种常用方式:REST与RPC,另外前面介绍的基于消息队列的发布/订阅模式也是服务通信的方式之一。本篇基于WebApiClient这个开源库介绍了如何进行声明式的REST调用,以及Thrift这个RPC框架介绍了如何进行RPC的通信,最后通过一个小例子来结尾。最后,服务调用的最佳实践一般是对外REST,对内RPC,但是追求极致的性能会消耗很多额外的成本,所以一般情况下对内一般也REST,但对于个别性能要求较高的接口使用RPC。

参考资料

远方的行者,《微服务 RPC和REST》

杨中科,《.NET Core微服务课程:Thrift高效通讯》

醉眼识朦胧,《Thrift入门初探–thrift安装及java入门实例》

focus-lei,《.net core下使用Thrift》

宝哥在路上,《Thrift性能测试与分析》

给TA打赏
共{{data.count}}人
人已打赏
安全网络

CDN安全市场到2022年价值76.3亿美元

2018-2-1 18:02:50

安全经验

GitLab 发布重要安全修复版本 11.8.3 和 11.7.7

2019-3-22 11:12:22

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索