各位吴彦祖&刘亦菲你好,俺是一名偷偷卷RPC的程序员龙哥😉
开始之前看下大纲
本文努力做到通俗一点,也画图辅助你理解,提到俺接下来的flag俺不会忘记,主要是尝试理解并慢慢深入RPC,最终能自己实现一个简单的RPC。这是系列的开篇。当然只有自己亲身理解做过,俺才会讲给同志们,我也把基础的知识点全面在系列梳理一遍,2022年做点有意义的,希望多多支持。
=正片开始=
1 RPC定义的理解
RPC(Remote Procedure Call)翻译过来 远程过程调用,简单解释下字面的意思哈,远程肯定是指要跨机器而不是本机啦,所以要用网络才能实现,但是不是说只能通过网络通信访问到另外的一台主机的应用程序,就可以称为RPC调用了,那就大错特错了,这显然还远远不够。
还有,它是种通信协议。该协议允许运行于一台计算机的程序调用另一个地址空间(通常是一个开放网络的一台计算机)的子程序,而程序员就像调用本地程序一样,无需额外地为这个交互动作编程了,就是不用关注细节,同时 RPC 也是遵循 CS 这种模式。
再想下 [过程] 这个词感觉那里听说过,我学C的时候听说过,也不知道最开始谁翻译的,俺觉得程序,服务,方法,这几个都比[过程]要好,可能大神不一样吧。
好吧,为了后面咱们能够好理解,我们统一一下叫法,干脆叫,远程服务调用,服务就包括程序接口,方法等一类的资源。
最后我们看 调用, RPC 是一种进程间通信的模式,程序是分布在不同的地址空间里。如果在同一主机里,RPC 可以通过不同的虚拟地址空间(即便使用相同的物理地址)进行通讯,而在不同的主机间,则通过不同的物理地址进行交互。许多技术都是基于这种概念而实现的。
听懂了些,好像又没完全懂,那说人话吧。比如说两台服务器:A 和 B,一个应用部署在 A 服务器上,想要调用 B 服务器上某个应用提供的函数/方法,下面画的这样。
总结下,俺的理解 RPC 是帮助我们屏蔽网络编程的细节,实现调用远程方法就跟调用本地(同一个项目中的方法)是一样的,不需要因为这个方法是远程调用就需要编写很多业务无关的代码,这是不需要。
这就好比在修在小河的桥一样,它连着河的两岸,如果没有桥了,你只能通过划船,绕道等方式才能到达河对面。
但是现在是有桥走的,这跟路上走到河对面有啥区别呢?没得啥区别啊。
确实有点抽象,俺再举个例子再加深一点对 rpc 印象
现在呢假设你有一个计算器接口,叫作Calculator,以及它的实现类CalculatorImpl,那么在系统还是单体的时喉,你要调用 Calculator接口 的add方法来执行一个加法运算,直接 new 一个 CalculatorImpl,然后调用 add方法 就行了,这其实就是非常普通的本地方法调用,这个我会在下面描述。本地嘛,因为在同一个地址空间,或者说在同一块内存,所以通过方法栈和参数栈就可以实现啦。
但是大事不妙,我们要考虑高性能和高可靠的因素啦,你决定把它改成分布式应用,将很多可以共享的功能都单独做出来,比如上面说到的计算器,你就单独把它放到一个服务里头,让别的服务去调用它。
这下问题来了啊,服务A 里面并没得 CalculatorImpl 这个类,那我要咋调 服务B 的 CalculatorImpl 的add方法呢?
有兄弟会说,你可以模仿 B/S 的那调用方式呀,在 B服务 暴露一个Restful接口,然后 A服务 通过调用这个 Restful接口 来间接调用 CalculatorImpl 的 add方法不就行了。
很好,这已经有点接近RPC了,但是如果是这样,那我每次调用时,那是不是都需要写一串发起 http请求 的代码呢?比如 httpClient.sendRequest...之类的,也太麻烦了吧,能不能跟本地调用一样,去发起远程调用,让使用者感知不到远程调用的过程呢,像这样:
@Reference
private Calculator calculator;
...
calculator.add(1,2);
...
这时候,有兄弟就会说,你可以用代理模式嘛!而且最好是结合Spring IoC一起用,通过 Spring 注入 calculator对象,注入的时候,如果扫描到对象加了 @Reference 注解,那就给它生成一个代理对象,将这个代理对象放进容器里面去。而这个代理对象的内部,就是通过 httpClient 来实现RPC远程过程调用的。
可能上面这段描述比较抽象,不过这就是很多RPC框架要解决的问题和解决的思路,比如阿里著名的Dubbo。
所以总结俺认为,RPC 的作用就是两个方面:
- 屏蔽远程调用和本地调用的区别,让我们感觉调项目内的方法一样的
- 隐藏底层网络的复杂性,让我们更专注于业务逻辑
好我们从中提炼 三个关键词
简单 高效 通用
- 简单:RPC 概念的语义十分清晰和简单,这样建立出来分布式计算就更容易。
- 高效:过程调用看起来很简单并且高效。
- 通用:在单机中过程往往是不同算法部分间最重要的通信机制。
但是想想互联网应用的量级越来越大,单台计算机的肯定能力有限,那么需要借助可扩展的集群来完成,具体看下如何 RPC 在不同机器之间的完成调用。
首先既然有远程的
本地过程调用是啥
所以咱们在研究 RPC 不同机器间调用,先看看本地调用是怎么调的。假设我们要调方法 Multiply() 来计算 lvalue * rvalue 乘积的结果是多少,就是下面这个方法:
int Multiply(int l, int r) {
int y = l * r;
return y;
}
int lvalue = 10;
int rvalue = 20;
int l_times_r = Multiply(lvalue, rvalue);
执行第8行的时:把 lvalue 和 rvalue 的值压栈并放到 Multiply方法的栈帧里面去,然后取出栈中的值10 和 20,把它赋给 l 和 r 执行第2行代码,然后计算 l * r,并把结果存到 y 里面去,接着把 y 的值压栈,然后从 Multiply 方法返回第8行,从栈中取出返回值 200,并赋值给 l_times_r。 我说的这5步就是执行本地调用的过程。当然这些步骤只是为了说明原理。对于参数和返回值少的情况会直接把它存放到寄存器里面,而不需要压栈弹栈的这个过程啦。
2 看下调用流程
正儿八经看下调用流程图
别人游戏开局一只狗,咱们这里就开局简单的画一下 rpc 整个调用流程,目的让你在心里对这张流程大致有个思路,让你很快熟悉它的调用。
咱们就开始了
典型的 RPC 架构可划成三块:
1)服务提供者(RPC Server):运行在服务端,提供服务接口定义和服务实现类。
2)服务消费者(RPC Client):运行在客户端的,通过远程代理对象调用远程服务。
3)注册中心(Registry):也是运行在服务端,负责把本地服务发布成远程的服务,它也要去管理,提供给服务消费者来使用的。
通过上面的图可以看出,一次简单的 RPC 调用可以分为以下几个步骤:
(1)服务提供者启动后主动向注册中心注册机器ip、端口以及提供的服务列表;
(2)服务消费者在启动时到注册中心获取服务提供方地址列表,在本地放一份;
(3)服务消费者通过本地调用的方式调用服务,调用模块收到请求后通过负载均衡算法选取合适的远程服务地址;
(4)协议模块负责把方法、入参等信息进行序列化(编码)成能够进行网络传输的消息体,并把消息通过网络发送给服务端;
(5)服务端收到消息后进行解码(反序列化操作)。
(6)根据解码结果调用本地的服务进行相关处理;
(7)服务端将处理返回的结果进行序列化(编码),并将结果通过网络发送至服务消费者;
(8)服务消费者收到消息后进行解码最终得到结果;
敲黑板:在不同的 RPC 框架实现中步骤 1、2、3的顺序可能有些不同。
RPC 调用分成下面两种:
同步调用:所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不会返回,比如有三个程序要执行,必须第一个程序被触发,执行结束了吗,才轮到其他程序执行。
异步调用:所有程序的执行不需要同步,可以多个触发,互相独立的执行相应的指令。客户方调用后不用等待执行结果返回,但依然可以通过回调通知等方式获取返回结果。若客户方不关心调用返回结果,则变成单向异步调用,单向调用不用返回结果。异步和同步的区分点在于是否等待服务端执行完成并返回结果。
俺用代码来定义一个委托的 Invoke(),并显示两者的不同之处
public delegate int AddHandler(int a,int b);
public class 加法类 {
public static int Add(int a, int b) {
Console.WriteLine("开始计算:" + a + "+" + b);
Thread.Sleep(3000); //模拟该方法运行三秒
Console.WriteLine("计算完成!");
return a + b;
}
}
同步调用
委托的 Invoke方法 用来进行同步调用。同步调用也可以叫阻塞调用,它将阻塞当前线程,然后执行调用,调用完毕后再继续向下执行。
public class 同步调用 {
static void main() {
Console.WriteLine("===== 同步调用 SyncInvokeTest =====");
AddHandler handler = new AddHandler(加法类.Add);
int result = handler.Invoke(1, 2);
Console.WriteLine("继续做别的事情。。。");
Console.WriteLine(result);
Console.ReadKey();
}
}
同步调用会阻塞线程,如果是要调用一项繁重的工作(如大量IO操作),可能让程序停顿过长时间,造成糟糕的用户体验,这时候异步调用就很有必要了。
异步调用
异步调用不阻塞线程,而是把调用塞到线程池中,程序主线程或UI线程可以继续执行。
委托的异步调用通过 BeginInvoke 和 EndInvoke 来实现。
public class 异步调用 {
static void main() {
Console.WriteLine("===== 异步调用 AsyncInvokeTest =====");
AddHandler handler = new AddHandler(加法类.Add);
//IAsyncResult: 异步操作接口
//BeginInvoke: 委托的一个异步方法的开始
IAsyncResult result = handler.BeginInvoke(1, 2, null, null);
Console.WriteLine("继续做别的事情。。。");
//异步操作返回
Console.WriteLine(handler.EndInvoke(result));
Console.ReadKey();
}
}
可以看到,主线程并没有等待,而是直接向下运行了。
但是问题依然存在,当主线程运行到 EndInvoke 时,如果这时侯调用没有结束,这种情况很可能出现,这时为了等待调用结果,线程依旧会被阻塞的。
好了,同步与异步在代码层面就说到这里
下面我们进一步拆解了 RPC 调用流程整个过程,下面我们详细说明下每个组件的职责划分。
RpcServer
负责导出(export)远程接口
RpcClient
负责导入(import)远程接口的代理实现
RpcProxy
远程接口的代理实现
RpcInvoker
客户方实现:负责编码调用信息和发送调用请求到服务方并等待调用结果返回
服务方实现:负责调用服务端接口的具体实现并返回调用结果
RpcProtocol
负责协议编/解码
RpcConnector
负责维持客户方和服务方的连接通道和发送数据到服务方
RpcAcceptor
负责接收客户方请求并返回请求结果
RpcProcessor
负责在服务方控制调用过程,包括管理调用线程池、超时时间等
RpcChannel
数据传输通道
你看上面图,这里俺根据每个组件给你描述下过程,RPC 服务端通过 RpcServer 去导出远程接口方法,而客户端通过 RpcClient 去引入远程接口方法,这样呢客户端就能像调用本地方法一样去调用远程接口方法,RPC 框架提供接口的代理实现,实际的调用将委托给代理 RpcProxy。 代理封装调用信息并将调用转交给 RpcInvoker 去实际执行。 在客户端的 RpcInvoker 通过连接器 RpcConnector 去维持与服务端的通道 RpcChannel, 并使用 RpcProtocol 执行协议编码,并把编码后的请求消息通过通道发给服务端。
RPC 服务端接收器 RpcAcceptor 接收客户端的调用请求,同样使用 RpcProtocol 执行协议解码(decode)。 解码后的调用信息传递给 RpcProcessor 去控制处理调用过程,最后再委托调用给 RpcInvoker 去实际执行并返回调用结果。
说到这里了还得,跟我一起弄清楚几个概念东西,不要搞混淆就行。
跟RMI有点像
RMI 叫远程方法调用,和上面那个只差两个字,它是允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。
它属于 Java 编程语言里面,一种用于实现远程过程调用的应用程序编程接口而已。它使客户机上运行的程序可以调远程服务器上的对象。远程方法调用特性使得 Java编程人员 能够在网络环境中分布操作。RMI全部的宗旨就是尽可能简化远程接口对象的使用。
我们知道远程过程调用,就是 RPC 可以用于一个进程调用另一个进程,很可能在另一个远程主机上的过程,从而提供了过程的分布这种能力。
RPC与Restful的比较
其实这两者并不是一个维度的概念,往大的说 RPC 涉及的维度更广。
如果硬要比较,那么可以从RPC风格的url和Restful风格的url上进行比较。
比如你提供一个查询订单的接口,用RPC风格,你可能会这样写:
//RPC风格
/queryOrder?orderId=123
用Restful风格呢?
/order?orderId=123
RPC是面向过程,Restful是面向资源,并且它用了Http这个协议。这个角度上来看,Restful风格的url在表述的精简性、可读性上都要更好。
(1)传输协议与性能上:RPC 的传输协议灵活,可基于 TCP 实现,由于 TCP 协议处于协议栈的下层,能够更灵活地对协议字段进行定制,让请求报文体积更小,减少网络开销,提高传输性能并缩短传输耗时,实现更大的吞吐量和并发数。REST 的 HTTP 协议是上层协议,发送包含同等内容的信息,请求中会包含很多无用的内容,所占用的字节数比使用 TCP 协议传输更高,因此在同等网络下,HTTP 会比基于 TCP 协议的数据传输效率要低,传输耗时更长,不仅如此,REST 的 HTTP 大部分是通过 JSON 来实现的,序列化也更消耗性能,但如果是基于 HTTP2.0,那么经过封装也是可以作为一个 RPC 来使用的。
(2)灵活性、开放与通用性:REST 通过 HTTP 实现,相对更加规范与通用,无论哪种语言都支持 HTTP 协议,所以 REST 的调用和测试都很方便,但使用 RPC 则会有很多约束,而如果 RPC 需要对外开放的话,需要进一步处理,灵活性不如 REST
(3)应用的场景:REST 主要用于对外开放的异构环境,比如浏览器接口调用,Api 接口调用,第三方接口调用等。RPC 主要用于公司内部的服务调用,性能消耗低,传输效率高,特别是大型的网站,内部子系统较多、接口非常多的情况下适合使用 RPC
3 再看下 RPC 核心功能
一个完整商用的 RPC 框架有很多功能,具体再说下最最核心的基本功能就是三个:服务寻址、数据编解码、网络传输。
服务寻址
如果像上面的本地调用那样,被调用的方法在同一个进程内,操作系统或者是虚拟机可以去地址空间去找;但是在远程调用中,这是行不通的,因为啥子呢,两个进程的地址空间是完全不一样的啊,肯定也不会晓得远端的进程在那。
所以要想实现远程调用,我们需要对服务消费者和服务提供者两者进行约束:
在远程过程调用中所有的函数都必须有一个 ID,这个 ID 在整套系统中是唯一存在确定的。
服务消费者在做远程过程调用时,发送的消息体中必须要携带这个 ID。
服务消费者和服务提供者分别维护一个函数和 ID 的对应表。
当服务消费者需要进行远程调用时,它就查一下这个表,找出对应的 ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码就行。
上面说的可能比较抽象,通俗一点就是服务消费者如何寻找服务提供者,这就是服务寻址。
服务寻址的实现方式有很多种,常用的是:服务注册中心。要调用服务,首先你需要一个服务注册中心去查询对方服务都有哪些实例,然后根据负载均衡策略择优选一。
像 Dubbo 框架的服务注册中心是可以配置的,官方推荐使用 Zookeeper。
但是我私下用的nacos也行。
数据编解码(序列化和反序列化)
对计网稍微有一点熟悉的同学都知道,数据在网络中传输都是二进制比特流:01010101010101010,类似这种,只有二进制数据才能在网络间传。
那一个客户端调用远程服务的一个方法,像方法入参这些必然需要转换成二进制才能进行传输,这种将对象转换成二进制流的过程就叫做序列化(编码),学过 JavaIO流 那部分你应该熟悉吧。
服务端接收到二进制流不能识别,势必要将二进制流转换成对象,这个逆过程就叫做反序列化,也可以叫解码。
下面用一张图总结
一般情景下是可以将序列化编码简称为序列化。
敲黑板:
如果你非要较真,严格来说序列化和编码是完全不同的两个概念,俺用老家的小黄猫画一张图类比下。
总结下序列化+编码的逆过程就是:解码+反序列化。
整体上RPC通信流程是上图那样,自己看就看的明白我就不复述了哈。
所以你现在清楚了吗?图里面网络传的必须是二进制数据,所以在 RPC 调用中,对入参对象与返回值对象进行序列化与反序列化是必然的过程了。
我们这里再具体说下网络传输,估计大家脑海里肯定马上就能想到 TCP/IP 四层模型,OSI 那七层,那咱们 RPC 会选择那一层作为传输协议呢?
在回答这个问题前我们先看下
RPC 需要网络传输实现了啥
网络传输
再复习下啊,客户端的数据经过序列化+编码以后,就需要通过网线,不是需要通过网络传到服务端。网络传输层需要把前面说的函数 ID 和序列化的参数字节流传给服务端,服务端处理完然后再把序列化后的调用结果传回客户端那边。
我们看下 TCP 协议,TCP 协议连接可以按需连接,需要调用的时候就先建立连接,调用完了立马就断开了。也可以是长连接这种,客户端和服务器建立连接之后保持长期持有,不管此事有无数据包的发送,可以配合心跳检测机制定期检测建立的连接是否存活有效。
由此可见 TCP 的性能确实很好,所以市面上大部分 RPC 框架都使用 TCP 协议这种,但也有极少部分框架使用其他协议,比如 gRPC 用的是 HTTP2 来实现。
特别注意的是
数据编解码和网络传输可以有多种组合方式,比如常见的有:HTTP+JSON, Dubbo 协议+TCP 等。
那你就想了为啥咋不用HTTP?
首先需要指正,这两个并不是并行概念。RPC 是一种设计思路,就是为了解决不同服务之间的调用问题,完整的 RPC 实现一般会包含有 传输协议 和 序列化协议 这两个。
而 HTTP 是一种传输协议,RPC 框架完全可以使用 HTTP 作为传输协议,也可以直接使用 TCP,所以使用不同的协议一般也是为了适应不同的场景。
TCP 和使用 HTTP 各有优势:
传输效率:
- TCP,通常自定义上层协议,可以让请求报文体积更加小
- HTTP:如果是基于HTTP 1.1 的协议,请求中会包含很多无用的内容
性能消耗上,主要在于序列化和反序列化的耗时
- TCP,可以基于各种序列化框架进行实现,效率比较高
- HTTP,大部分是通过 json 来实现的,字节大小和序列化耗时都要更消耗性能
跨平台:
TCP:通常要求客户端和服务器为统一平台HTTP:可以在各种异构系统上运行。
总结下:
RPC 的 TCP 方式主要用于公司内部的服务调用,性能消耗低,传输效率高。
HTTP主要用于对外的异构环境,浏览器接口调用,APP接口调用,第三方接口调用应用较多。
常见的 RPC 框架
说了这么多 RPC 相关的技术,咱们盘点一下市面上常用的 RPC 框架。
- RMI(Sun/Oracle)
- Thrift(Facebook/Apache)
- gRPC(Google)
- Finagle(Twitter)
- Dubbo(估计大家了解一些)
- Motan(新浪微博)
- brpc(百度/Apache)
- ……欢迎大家补充其他的。
3 解决三个小疑问
1 通过轮子你能学到什么?
敲黑板:首先强调一下轮子的目的不是为了放在生产上面去用,肯定会有很多缺陷是不得行,而是造轮子以实战经验来促进你高效来学习,让你掌握零散的知识点连成一条线。
这次我会深入研究带大家从零开始撸一个 RPC 框架,如果在编写过程中如果呈现问题,欢迎小伙伴们到 github(id: Datalong)提交你的issue,会及时修正。
2 为什么你需要学习造轮子
从零开始,手写一个RPC, 跟随这篇前提知识做铺垫以及数个迭代版本的代码,由简陋到逐渐完备,目的让所有人都能看懂并且写出一个RPC框架。
本篇与代码都是本人第一次手写RPC的心路历程,会有理解的偏差与代码上的不完善,但更是由于这样,有着与新手对同样问题的疑惑,也许会使新手更容易理解这样做的缘故是啥。
学习建议:
- 一定要实际上手敲代码
- 后面每一版本有着对应独立的代码与文档,结合来看
每一版本前有一个背景知识,建议先掌握其相关概念再上手码
- 每个版本都有着要解决的问题与此版本的最大痛点,带着问题去写代码,并且与上个版本的代码进行比较差异
如果你认真去学下去,可以掌握下面的技术:
basic
- 1 JavaSE就不说了
- 2 使用自定义注解,学完可以了解注解的基本运行机制;
- 3 会用到反射机制
- 4 学会如何改配置项,并绑定到 bean;
- 5 知道监听 spring 容器的事件;
hard
- 1 底层网络基于 netty, 学完 netty 入门没有问题;
- 2 会用到动态代理技术;
- 3 服务注册基于 zookeeper, 学完 zk 入门没有问题;
- 4 教你如何定义一个xxx-spring-boot-starter, 了解spring boot自动配置机制;
还有就是底层的网络是基于 netty, 学完后 netty 入门没有问题;
有没有一点心动呢?!
这篇文章就当做一个引入和前言吧,主要是为了增强大家的信心,肯定是能学到东西的。
3 为啥需要RPC
上面简单解答了为啥咱们要学 rpc 轮子,那铁子们就问为什么需要 RPC?一项新技术肯定是为了改善技术工具或者重新设计解决具体的业务及架构方面的问题。
不得不说单体架构,这种就是把应用程序的所有功能都打成一个部署包。
从上面的架构图总结一下单体架构的特点:
- 所有的功能集成在一个项目工程里面;
- 通过分层架构,上层调用下层接口,所有的调用都在应用内完成;
- 所有的功能打成一个 war 包放在服务器上run;
- 应用和数据库是分开部署;
如果网站流量很小时,只需一个应用,把所有功能部署在一起,来减少部署结点和运维成本。如果流量稍微大一点可以通过部署应用集群和数据库集群来提高系统的性能。
这种肯定逐渐淘汰,业务逐渐复杂,应用的外部流量压力骤增,团队成员会越来越多,这时的抗压能力会逐渐暴露。
具体聊下单体不足的地方
缺点一:高耦合
某个模块出现死循环,导致内存溢出,上下应用都会挂掉。
缺点二:扩展性很差
系统的扩容只能针对应用进行扩容,不能做到对某个功能模块进行扩容,扩容后必然带来资源浪费的问题
缺点三:持续交付周期拉长
单体应用变大后构建和部署时间也跟着来延长,对于频繁部署不利,阻碍项目持续交付。即使在仅仅更改了一行
网站应用的规模不断扩大,传统的垂直应用架构早已经没法满足了,分布式服务架构以及流动计算架构也在行业应用很广泛,但是在这背景就需要个治理系统确保架构有条不紊的继续演进。
简单介绍下演进的四种结构
单一应用结构(也叫ORM)
当网站流量很小,它只需一个应用,将所有功能如下单,支付等都部署在一起,减少了部署节点和成本。
- 缺点就是:单一的系统,使得在开发途中,占用的资源会越来越多,并且是随着流量的增加越来越难以维护
垂直应用结构(MVC)
垂直应用架构解决了单一应用架构所面临的扩容问题,流量能够分散到各个子系统当中,且系统的体积可控,一定程度上降低了开发人员之间协同以及维护的成本,提升了开发效率。
- 缺点:但是在垂直架构中相同逻辑代码需要不断的复制,不能复用。
分布式应用结构(RPC)
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心
流计算结构(SOA)
它也叫服务治理结构,随着服务化的进一步发展,服务越来越多,服务之间的调用和依赖关系也越来越复杂,诞生了面向服务的架构体系(SOA),也因此衍生出了一系列相应的技术,如对服务提供、服务调用、连接处理、通信协议、序列化方式、服务发现、服务路由、日志输出等行为进行封装的服务框架。
还需解决问题
最后要实现一个RPC不算难,难的是实现一个高性能高可靠的RPC框架。
比如,既然是分布式了,那么一个服务可能有多个实例,你在调用时,要如何获取这些实例的地址呢?
这时候就需要一个服务注册中心,比如在Dubbo里头,前面已经说过了可以使用Zookeeper作为注册中心,在调用时,从Zookeeper获取服务的实例列表,再从中选择一个进行调用。
那么选哪个调用好呢?这时候就需要负载均衡了,于是你又得考虑如何实现复杂均衡,比如Dubbo就提供了好几种负载均衡策略。
这还没完,总不能每次调用时都去注册中心查询实例列表吧,这样效率多低呀,于是又有了缓存,有了缓存,就要考虑缓存的更新问题,blablabla......
你以为就这样结束了,没呢,还有这些:
客户端总不能每次调用完都干等着服务端返回数据吧,于是就要支持异步调用;
服务端的接口修改了,老的接口还有人在用,怎么办?总不能让他们都改了吧?这就需要版本控制了;
服务端总不能每次接到请求都马上启动一个线程去处理吧?于是就需要线程池;
服务端关闭时,还没处理完的请求怎么办?是直接结束呢,还是等全部请求处理完再关闭呢?
......
如此种种,都是一个优秀的RPC框架需要考虑的问题。
下篇带你敲代码,整个简单架子先
下期再见