💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
一个完整的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的内部机制。