RPC(Remote procedure call)远程调用技术,核心就是如何实现远程通信,在架构上类似 C/S(注:Tomcat 是 B/S 架构,底层基于 NIO/Netty。),即 Provider(Server) 持续提供服务, Consumer(Client) 远程调用服务。目前 Java 提供的网络编程方式有 BIO、NIO、AIO,本篇我们就基于 BIO 来实现一个简易的 RPC 框架。
PS:RPC 与 HTTP 的关系?RPC 是一种技术的概念名词,HTTP 是一种协议,RPC可以通过 HTTP 来实现,也可以通过Socket自己实现一套协议来实现。
先来看需求:
- Producer
- 提供一个要暴露的服务(接口)
- 通过框架的 publish() 方法就可以发布一个服务实例
- Consumer:通过框架的 clientProxy() 方法就可以远程调用 Producer 发布的服务,并且拿到返回结果
所以框架需要关注的几个问题:
- Producer 说是发布服务,到底什么叫发布服务?答:类似 C/S 架构,创建一个 SockerServer,持续接收请求
- Consumer 如何发起远程调用?答:可以通过 Socket 发送请求信息,然后将这部分发送逻辑封装到代理对象中
- Producer 与 Consumer 如何通信,即 Producer 如何知道 Consumer 调用什么服务的什么方法?答:自定义协议(RpcRequest)
- Producer 如何处理请求,并返回结果?答:解码 RpcRequest -> 反射执行方法 -> 编码结果
- Consumer 收到响应后如何解析?答:解码,返回给用户
框架整体结构如下:
- provider包:根据 Consumer 请求提供服务,即处理请求并返回结果
- consumer包:构建服务代理对象,根据调用的服务的方法构建 RpcRequest,然后进行远程调用
我们先来看看 protocol包中的 RpcRequest 是什么。
1.RpcRequest
RpcRequest 封装了消费者要调用方法的具体信息,是我们的自定义协议,或者说是消息格式。
涉及两个过程:
- Consumer 编码:RpcRequest -> 数据流(二进制) => 告诉 Provider 要执行哪个方法
- Provider 解码:数据流(二进制)-> RpcRequest => 获取 Consumer 要调用哪个方法
// 注:只有实现了序列化接口,才能实现远程传输
public class RpcRequest implements Serializable {
private String className; // 类(接口/服务)
private String methodName; // 方法
private Object[] parameters; // 参数
public String getClassName() {
return className;
}
public void setClassName(String className) {
this.className = className;
}
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
public Object[] getParameters() {
return parameters;
}
public void setParameters(Object[] parameters) {
this.parameters = parameters;
}
private String version;
public void setVersion(String version) {
this.version = version;
}
public String getVersion() {
return version;
}
}
注意,下面是 provider包 的内容,目的是根据 consumer 包请求提供服务,即处理请求并返回结果…
2.RpcServer
负责将请求派发给不同线程
public class RpcServer {
// 用线程池实现给所有连接都分配一个线程
ExecutorService executorServic = Executors.newCachedThreadPool();
// 发布服务
// 入参是:服务的实现实例(单例,负责执行consumer要调用的方法),发布的端口
// 注:如果要发布n个服务,要指定n个端口;线程池的线程为这n个服务的所有连接提供服务
public void publisher(Object service, int port) {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(port);
while (true) {
Socket socket = serverSocket.accept();
// 每一个socket,交给一个processorHandler去处理
executorServic.execute(new ProcessorHandler(socket, service));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (serverSocket != null){
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
3.ProcessorHandler
Provider 线程的具体调用逻辑:
- 接收(解码):接收 Client 请求,将二进制流转换成 RpcRequest
- 执行:获取要执行的方法和参数,调用服务实现对象去执行方法
- 发送(编码):将方法执行结果返回,将基本类型/Java对象转换成 二进制流
PS:由于这里采用的是BIO,并且传输时涉及到对象的编解码,所以
- 可以统一调用 ObjectOutputStream#writeObject() 编码
- 可以统一调用 ObjectInputStream#readObject() 解码,然后再将 Object 转换成包装类/普通Java对象
public class ProcessorHandler implements Runnable {
private Socket socket;
private Object service;
public ProcessorHandler(Socket socket,Object service) {
this.socket = socket;
this.service = service;
}
@Override
public void run() {
try {
// Object~Stream也是包装类,其作用在于将字节流解析成java对象
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
// 解析出具体的请求信息(RPCRequest)
RpcRequest rpcRequest = (RpcRequest)objectInputStream.readObject();
// 反射调用本地服务
Object res = invoke(rpcRequest);
// 将执行结果返回
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
objectOutputStream.writeObject(res);
objectOutputStream.flush(); // 切记要flush手动刷新
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
// 通过反射具体执行provider提供的方法(即消费者要调用的方法)
public Object invoke(RpcRequest request) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException {
// 根据实参获取形参列表
// 注:获取形参列表后才能确定一个方法
Object[] args = request.getParameters();
Class<?>[] types = new Class[args.length];
for (int i = 0; i < args.length; i++) {
types[i] = args[i].getClass();
}
// 通过全类名拿到具体具体Class对象
Class<?> clazz = Class.forName(request.getClassName());
// 获取 Method
Method method = clazz.getMethod(request.getMethodName(), types);
// 执行方法
// 注:service 这里是单例模式,但是也可以new一个对象后再执行具体方法
Object res = method.invoke(service, args);
return res;
}
}
注意,下面是 consumer包 内容,目的是构建服务代理对象,根据调用的服务的方法构建 RpcRequest,然后进行远程调用…
4.RpcProxyClient
代理对象,通过 JDK 动态代理生成一个代理对象
PS:这里创建一个代理对象是因为,服务的实现实例在 Provider,但 Consumer 调用服务的具体方法时也需要一个实例,而 Consumer 并没有这个实例。
public class RpcProxyClient {
// 创建代理对象,代理的就是指定服务
// 注:因为 Provider 发布时就是一个端口一个服务,所以这里代理的唯一标识就是 host:port
public <T>T clientProxy(final Class<T> interfaceCls, final String host, final int port) {
return (T)Proxy.newProxyInstance(interfaceCls.getClassLoader(), new Class[]{interfaceCls},
new RemoteInvocationHandler(host, port));
}
}
5.RemoteInvocationHandler
代理对象的具体逻辑,核心是 invoke 方法,当 Consumer 调用了服务的方法时,就会走到 invoke():
- 构建请求信息 RpcRequest
- 发送(编码):将 RpcRequest 转换成二进制流,发送
- 线程阻塞等待 Provider 处理结果
- 接收(解码):接收 Provider 的处理结果,将二进制流转换成基本类型/Java对象,并返回给上层函数
注:234步的逻辑都是网络 IO 相关,所以后面单独封装了一个 RpcNetTransport 类去实现
public class RemoteInvocationHandler implements InvocationHandler {
private String host;
private int port;
public RemoteInvocationHandler(String host, int port) {
this.host = host;
this.port = port;
}
@Override
// 所有请求都会进入这里
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 构建调用Provider的请求参数
RpcRequest rpcRequest = new RpcRequest();
rpcRequest.setClassName(method.getDeclaringClass().getName());
rpcRequest.setMethodName(method.getName());
rpcRequest.setParameters(args);
// 进行远程调用,并返回执行结果
RpcNetTransport netTransport = new RpcNetTransport(host, port);
Object res = netTransport.send(rpcRequest);
return res;
}
}
6.RpcNetTransport
/**
* 实现网络调用
*/
public class RpcNetTransport {
private String host;
private int port;
public RpcNetTransport(String host, int port) {
this.host = host;
this.port = port;
}
public Object send(RpcRequest request) {
Socket socket = null;
Object result = null;
ObjectInputStream inputStream = null;
ObjectOutputStream outputStream = null;
try {
// 将调用的服务的具体信息通过网络写出
socket = new Socket(host, port);
outputStream = new ObjectOutputStream(socket.getOutputStream());
// writeObject实际上是一种序列化
outputStream.writeObject(request);
outputStream.flush();
// 阻塞等待Provider的返回结果
inputStream = new ObjectInputStream(socket.getInputStream());
result = inputStream.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return result;
}
}
好了,到此框架部分的内容就完成了,可以看到 RPC 也无非就是在 IO 基础上,多了个调用指定服务的逻辑而已(核心是:自定义协议+代理对象)。
结果测试
这里再说一下关于 api 包的内容,包括了服务(接口)和公共模块,是面向接口编程的基础,
- Provider:提供接口的实现
- Consumer:根据接口进行调用
public interface TestService {
String test(String name);
}
Provider
服务实现类:
public class TestServiceImpl implements TestService {
@Override
public String test(String name) {
System.out.println("new requst coming..." + name);
Random random = new Random();
String json = "{\"name\":" + "\"" + name + "\"" + ", \"age\":" + random.nextInt(40) + "}";
return json;
}
}
发布服务:
public class Provider {
public static void main(String[] args) {
RpcServer proxyServer = new RpcServer();
// 创建一个服务实例
proxyServer.publisher(new TestServiceImpl(), 8080);
}
}
Consumer
远程调用服务:
public class Consumer {
public static void main(String[] args) {
// 创建代理对象
RpcProxyClient rpcProxyClient = new RpcProxyClient();
// 创建一个代理对象
TestService service = rpcProxyClient.clientProxy(TestService.class, "localhost", 8080);
// test() 会进行远程调用,阻塞式...
String json = service.test("老五");
System.out.println(json);
}
}
结果如下:
完整代码我放到 GitHub 上了,有需要参考的同学点击这里跳转…