一个完整的RPC流程,可以用下面这张图来描述:
![](https://img.kancloud.cn/14/90/1490426b8fa1b25cfcb26f8dc7ff2429_308x240.png)
其中左边的Client,对应的就是前面的Service A,而右边的Server,对应的则是Service B。
1.Service A的应用层代码中,调用了Calculator的一个实现类的add方法,希望执行一个加法运算;
2.这个Calculator实现类,内部并不是直接实现计算器的加减乘除逻辑,而是通过远程调用Service B的RPC接口,来获取运算结果,因此称之为**Stub**;
3.Stub怎么和Service B建立远程通讯呢?这时候就要用到**远程通讯工具**了,也就是图中的**Run-time Library**,这个工具将帮你实现远程通讯的功能,比如Java的**Socket**,就是这样一个库,当然,你也可以用基于Http协议的**HttpClient**,或者其他通讯工具类,都可以,**RPC并没有规定说你要用何种协议进行通讯**;
4.Stub通过调用通讯工具提供的方法,和Service B建立起了通讯,然后将请求数据发给Service B。需要注意的是,由于底层的网络通讯是基于**二进制格式**的,因此这里Stub传给通讯工具类的数据也必须是二进制,比如calculator.add(1,2),你必须把参数值1和2放到一个Request对象里头(这个Request对象当然不只这些信息,还包括要调用哪个服务的哪个RPC接口等其他信息),然后**序列化**为二进制,再传给通讯工具类,这一点也将在下面的代码实现中体现;
5.二进制的数据传到Service B这一边了,Service B当然也有自己的通讯工具,通过这个通讯工具接收二进制的请求;
6.既然数据是二进制的,那么自然要进行**反序列化**了,将二进制的数据反序列化为请求对象,然后将这个请求对象交给Service B的Stub处理;
7.和之前的Service A的Stub一样,这里的Stub也同样是个“假玩意”,它所负责的,只是去解析请求对象,知道调用方要调的是哪个RPC接口,传进来的参数又是什么,然后再把这些参数传给对应的RPC接口,也就是Calculator的实际实现类去执行。很明显,如果是Java,那这里肯定用到了**反射**。
8.RPC接口执行完毕,返回执行结果,现在轮到Service B要把数据发给Service A了,怎么发?一样的道理,一样的流程,只是现在Service B变成了Client,Service A变成了Server而已:Service B反序列化执行结果->传输给Service A->Service A反序列化执行结果 -> 将结果返回给Application,完毕。
-----------
RPC简单示例:
首先是Client端的应用层怎么发起RPC,ComsumerApp:
```
public class ComsumerApp {
public static void main(String[] args) {
Calculator calculator = new CalculatorRemoteImpl();
int result = calculator.add(1, 2);
}
}
```
**通过一个CalculatorRemoteImpl,我们把RPC的逻辑封装进去了,客户端调用时感知不到远程调用的麻烦**。
CalculatorRemoteImpl类代码:
```
public class CalculatorRemoteImpl implements Calculator {
public int add(int a, int b) {
List<String> addressList = lookupProviders("Calculator.add");
String address = chooseTarget(addressList);
try {
Socket socket = new Socket(address, PORT);
// 将请求序列化
CalculateRpcRequest calculateRpcRequest = generateRequest(a, b);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
// 将请求发给服务提供方
objectOutputStream.writeObject(calculateRpcRequest);
// 将响应体反序列化
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
Object response = objectInputStream.readObject();
if (response instanceof Integer) {
return (Integer) response;
} else {
throw new InternalError();
}
} catch (Exception e) {
log.error("fail", e);
throw new InternalError();
}
}
}
```
分布式应用下,一个服务可能有多个实例,比如Service B,可能有ip地址为198.168.1.11和198.168.1.13两个实例,lookupProviders,其实就是在寻找要调用的服务的实例列表。在分布式应用下,通常会有一个**服务注册中心**,来提供查询实例列表的功能。
查到实例列表之后要调用哪一个实例呢,只时候就需要chooseTarget了,其实内部就是一个**负载均衡**策略。
由于我们这里只是想实现一个简单的RPC,所以暂时不考虑服务注册中心和负载均衡,因此代码里写死了返回ip地址为127.0.0.1。
代码继续往下走,我们这里用到了Socket来进行远程通讯,同时利用**ObjectOutputStream**的writeObject和**ObjectInputStream**的readObject,来实现序列化和反序列化。
最后再来看看Server端的实现,和Client端非常类似,ProviderApp:
```
public class ProviderApp {
private Calculator calculator = new CalculatorImpl();
public static void main(String[] args) throws IOException {
new ProviderApp().run();
}
private void run() throws IOException {
ServerSocket listener = new ServerSocket(9090);
try {
while (true) {
Socket socket = listener.accept();
try {
// 将请求反序列化
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
Object object = objectInputStream.readObject();
log.info("request is {}", object);
// 调用服务
int result = 0;
if (object instanceof CalculateRpcRequest) {
CalculateRpcRequest calculateRpcRequest = (CalculateRpcRequest) object;
if ("add".equals(calculateRpcRequest.getMethod())) {
result = calculator.add(calculateRpcRequest.getA(), calculateRpcRequest.getB());
} else {
throw new UnsupportedOperationException();
}
}
// 返回结果
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
objectOutputStream.writeObject(new Integer(result));
} catch (Exception e) {
log.error("fail", e);
} finally {
socket.close();
}
}
} finally {
listener.close();
}
}
}
```
Server端主要是通过ServerSocket的accept方法,来接收Client端的请求,接着就是反序列化请求->执行->序列化执行结果,最后将二进制格式的执行结果返回给Client。
**就这样我们实现了一个简陋而又详细的RPC。**
说它简陋,是因为这个实现确实比较挫。
说它详细,是因为它一步一步的演示了一个RPC的执行流程,方便了解RPC的内部机制。