任何分布式系统的开发都涉及到跨进程之间的远程过程调用,也就是所谓的RPC。前面两讲我们讨论的网络通信和序列化实际上也都属于是RPC架构的范畴,只是关注于不同的技术切入点。而RPC本身也构成一种架构体系,包含一系列相互协作的核心组件。在Dubbo、Spring Cloud等主流的分布式服务框架中,这些技术组件使用起来都非常简单,因为框架为开发人员屏蔽了底层的实现细节。那么,现在假如没有这些开源框架,而是需要你自己来设计并实现一套远程过程调用机制,你应该怎么做的?
在分布式系统中,服务与服务之间需要通过远程调用来完成一个个具体的业务操作。这是一个全流程的执行过程,涉及到前面介绍到的网络通信和序列化技术,也涉及到本文内容要进一步阐述的远程调用中的不同技术组件。
在使用Dubbo、Spring Cloud等框架时,你可能并没有感受到远程调用过程是如何执行的,因为这些框架都提供了“远程调用本地化”机制,开发人员调用远程方法感觉和调用本地方法并没有什么差别。这是框架所具备的能力,我们直接使用即可。RPC这个概念我们一直都在说,但面对这一概念,我们应该如何进行系统化的学习呢?我们接下来对这个问题进行分析。
远程过程调用,英文即为Remote Procedure Call,也就是我们通常所说的RPC,这个名词第一次出现是在1974年。从诞生到今天,RPC架构存在了40多年,已经演变成一切分布式系统的基础性架构。而围绕RPC架构的基本概念,这么多年以来实际上并没有发生大的变化。所以,关于RPC架构本身的学习内容和范围是相对固定的,这是我们首先需要明确的一点。
作为一种架构模式,业界已经对RPC架构的各个组成部分进行了抽象和提炼,从而形成一套完整的组件体系。正是基于这套RPC的组件体系,业界诞生了各种具体实现框架,Dubbo就是其中的代表。而无论Dubbo等框架如何实现,其底层的组成结构是完全遵循RPC架构的。
事实上,基于RPC的组成结构,任何人都可以自己实现一套RPC框架。我们可以采用最简单、最常见的技术体系实现一个最基础的PRC架构。通过这一过程,可以确保具体的实现技术和纯粹的理论体系能够对应起来。
接下来我们来讨论具体的技术体系,让我们从RPC架构的组成结构开始讲起。
RPC架构原理
如果我们站在最高的层次来看RPC的执行流程,就是一个服务的消费者向服务的提供者发起远程调用并获取返回结果的过程,如下图所示。
接下来,如果我们把上图做一些展开。通过第3讲内容的学习,我们知道服务提供者需要暴露服务访问的入口,而消费者则会向提供者所暴露的访问入口发起请求。如下图所示。
可以看到,这里我们对服务消费者和提供者的组成结构做了细化,并提取了RpcChannel、RpcProtocol、RpcConnector和RpcAcceptor这四个技术组件。在这四个技术组件中,前两个属于公共组件,而后两个则面向服务的提供者和服务的消费者,分别用于发起请求和接收响应。
有了底层的用于完成网络通信的技术组件之后,我们再来看如何从业务接口定义和使用的角度出发进一步对RPC架构的组成结构进行扩充,如下图所示。
上图中出现了用于代表业务接口的API组件,同时,我们也看到了分别代表客户端和服务器的RpcClient和RpcServer组件。我们不难想象这些组件的定位和作用。而这里需要重点介绍的是RpcCaller组件和RpcInvoker组件。RpcCaller组件位于服务消费者端,会根据API的定义信息发起请求。而RpcInvoker组件位于服务提供者端,负责对RpcServer执行具体的调用过程并返回结果。
最后,为了对远程调用过程进行更好的控制,我们还会引入两个技术组件,分别是RpcProxy和RpcProcessor。完整的RPC架构组成结构如下图所示。
从命名上看,位于服务消费者端的RpcProxy组件充当了一种代理机制,确保服务消费者能够像调用本地方法一样调用远程服务。而位于服务提供者端的RpcProcessor的作用则是为远程调用执行过程添加各种辅助性支持,包括线程管理、超时控制等。
这样,我们对整个RPC架构的演进过程做了详细的描述。通过对上图中的技术组件做进一步梳理,我们会发现这些组件可以归为三大类,即客户端组件、服务端组件和公共组件。其中,客户端组件与职责包括:
- RpcClient,负责导入远程接口代理实现
- RpcProxy,远程接口的代理实现
- RpcCaller,负责执行远程调用
- RpcConnector,负责连接服务器
服务端组件与职责包括:
- RpcServer,负责导出(Export)远程接口
- RpcInvoker,负责调用服务端接口
- RpcAcceptor,负责接收网络请求
- RpcProcessor,负责处理调用过程
而客户端和服务器端所共有的组件包括:
- RpcProtocol,负责执行网络传输
- RpcChannel,数据传输的通道
关于RPC架构的组成结构介绍到这里就结束了。在这一组成结构的基础上,如果采用合适的编程语言和实现技术,原则上我们就可以自己动手实现一个RPC架构。
RPC实现过程
接下来我们通过一个简单的示例来实现前面所介绍的的各个技术组件。该示例的主要目的是为了演示如何从零开始构建一个最基本的RPC架构。
首先我们定义一个业务服务,称为UserService,包含一个用于根据用户编码获取用户姓名的业务方法,如下所示。
public interface UserService {
//根据用户编码获取用户姓名
public String getUserNameByCode(String userCode);
}
UserService接口的实现类也非常简单,我们通过一个内存Map来模拟对数据的存储和查询操作,如下所示。
public class UserServiceImpl implements UserService {
private Map<String, String> users = new HashMap<String, String>();
public UserServiceImpl() {
users.put("user1", "tianyalan1");
users.put("user2", "tianyalan2");
}
@Override
public String getUserNameByCode(String userCode) {
return users.get(userCode);
}
}
对于RPC架构而言,有了服务定义之后,我们就需要分别构建一个服务端组件RpcServer和一个客户端组件RpcClient。但在这之前,我们首先需要定义一种在客户端和服务器端之间进行通信的消息格式,这里命名为Protocol,如下所示。
public class Protocol implements Serializable {
//包名+接口名称
private String interfaceName;
//调用方法名
private String methodName;
//参数类型:按照接口参数顺序
private Class[] paramsTypes;
//参数:按照接口参数顺序
private Object[] parameters;
public Protocol (String interfaceName, String methodName,
Class[] paramsTypes, Object[] parameters) {
super();
this.interfaceName = interfaceName;
this.methodName = methodName;
this.paramsTypes = paramsTypes;
this.parameters = parameters;
}
//省略getter/setter方法
}
可以看到Protocol中定义了一次服务请求所需要的接口名、方法名以及方法调用所需要的参数。注意到该类同时实现了Serializable接口,这是Java中的序列化接口,实现该接口的类能够通过网络进行远程传输。在RPC基础架构图中,Protocol类相当于是通过RpcProtocol进行传递的请求数据。
接下来我们考虑构建RpcServer类,该类需要实现RPC基础架构图中的各个服务端组件。RpcServer类完整代码如下所示。
public class RpcServer {
//线程池
private int threadSize = 10;
private ExecutorService threadPool;
//自定义缓存
private Map<String, Object> servicePool;
//服务端口
private int port = 9000;
public RpcServer() {
super();
synchronized (this) {
threadPool = Executors.newFixedThreadPool(this.threadSize);
}
}
public RpcServer(int threadSize, int port) {
this.threadSize = threadSize;
this.port = port;
synchronized (this) {
threadPool = Executors.newFixedThreadPool(this.threadSize);
}
}
public RpcServer(Map<String, Object> servicePool, int threadSize, int port) {
this.threadSize = threadSize;
this.port = port;
this.servicePool = servicePool;
synchronized (this) {
threadPool = Executors.newFixedThreadPool(this.threadSize);
}
}
//1. 实现Socket监听:RpcAcceptor
public void service() throws IOException {
ServerSocket serverSocket = new ServerSocket(port);
while (true) {
Socket receiveSocket = serverSocket.accept();
final Socket socket = receiveSocket;
//执行请求
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
//2. 处理请求
process(socket);
socket.close();
} catch(IOException e) {
//篇幅关系,省略对各种异常信息的处理
}
}
});
}
}
//2.处理请求:RpcProcessor
private void process(Socket receiveSocket) throws IOException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
ObjectInputStream objectInputStream = new ObjectInputStream(receiveSocket.getInputStream());
Protocol transportMessage = (Protocol)objectInputStream.readObject();
//调用服务
Object result = call(transportMessage);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(receiveSocket.getOutputStream());
objectOutputStream.writeObject(result);
objectOutputStream.close();;
}
//3.执行方法调用:RpcInvoker
private Object call(Protocol protocol) throws ClassNotFoundException, NoSuchMethodException,
IllegalAccessException, InstantiationException, InvocationTargetException {
if(servicePool == null) {
synchronized (this) {
servicePool = new HashMap<String, Object>();
}
}
//通过接口名称构建实现类
String interfaceName = protocol.getInterfaceName();
Object service = servicePool.get(interfaceName);
Class<?> serviceClass = Class.forName(interfaceName);
//判断servicePool对象是否存在,如果不存在,就创建新对象并放入池中
if(service == null) {
synchronized (this) {
service = serviceClass.newInstance();
servicePool.put(interfaceName, service);
}
}
//通过实现类来构建方法
Method method = serviceClass.getMethod(protocol.getMethodName(), protocol.getParamsTypes());
//通过反射来实现方法的执行
Object result = method.invoke(service, protocol.getParameters());
return result;
}
}
RpcServer类代码相对比较长,我们结合RPC基本架构对其进行分段解析。
- service方法:
service方法接收请求并基于Socket启动端口监听,通过线程池为进入的每个请求启动一个线程进行处理。就RPC基础架构而言,该service方法相当于扮演RpcAcceptor的角色。
- process方法
service方法启动了线程池,而每个线程负责执行process方法。这里的process方法从Socket中获取输入流,然后把输入流中的数据转化为Protocol,从而获取远程方法调用的各项元数据。就RPC基础架构而言,该process方法充当了RpcProcessor的角色。
- call方法
一旦获取Protocol,process方法就调用内部的call方法来执行真正的方法调用。这里通过反射机制获取位于服务器端的方法并进行执行。显然,该call方法对应于RpcInvoker角色。
介绍完RpcServer中的各个技术组件,我们再来看一下RpcClient的代码,如下所示。
public class RpcClient {
private String serverAddress;
private int serverPort;
public RpcClient(String serverAddress, int serverPort) {
this.serverAddress = serverAddress;
this.serverPort = serverPort;
}
//RpcConnector + RpcCaller
public Object sendAndReceive(Protocol protocol) {
Object result = null;
try {
Socket socket = new Socket(serverAddress, serverPort);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
objectOutputStream.writeObject(protocol);
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
result = objectInputStream.readObject();
} catch (Exception e) {
//篇幅关系,省略对各种异常信息的处理
}
return result;
}
//省略getter/setter方法
}
RpcClient类的代码相对比较简单,主要就是根据远程服务的地址通过Socket发起通信,传入一个Protocol对象并返回远程调用的结果。
完成了RpcServer和RpcClient类之后,我们就可以编写一些测试用例来进行验证。验证方法就是启动RpcServer,然后通过RpcClient执行远程调用。这里我们编写一个ServerTest来启动RpcServer,如下所示。
public class ServerTest {
public static void main(String[] args) {
Map<String, Object> servicePool = new HashMap<String, Object>();
servicePool.put("com.juejin.rpc.service.UserService", new UserServiceImpl());
RpcServer server = new RpcServer(servicePool, 4, 9000);
try{
server.service();
} catch (IOException e) {
e.printStackTrace();
}
}
}
然后我们再编写一个ClientTest对远程服务发起请求,整个过程如下所示。
public class ClientTest {
public static void main(String[] args) {
String serverAddress = "127.0.0.1";
int serverPort = 9000;
RpcClient client = new RpcClient(serverAddress, serverPort);
Protocol protocol = buildProtocol("user1");
Object result = client.sendAndReceive(protocol);
System.out.println(result);
protocol = buildProtocol("user2");
result = client.sendAndReceive(protocol);
System.out.println(result);
}
private static Protocol buildProtocol(String userCode) {
String interfaceName = "com.juejin.rpc.service.UserService";
Class[] paramsTypes = {String.class};
Object[] parameters = {userCode};
String methodName = "getUserNameByCode";
Protocol protocol = new Protocol(interfaceName, methodName, paramsTypes, parameters);
return protocol;
}
}
至此,我们构建了一个简洁但又完整的RPC架构。通过这个示例,我们可以对RPC的整体结构有一个清晰的认识。事实上,无论是多么复杂的RPC框架,都是从这样的基础架构开始逐步演进而来。