HTTP经常接触,大家也不陌生,这是一个超文本传输协议,能够在网络直接传输数据。目前微服务项目很火,微服务之间基本都是使用HTTP传输,例如Feign,OkHttp,RestTemlpate等等。但是分布式这个话题目前还不过时,今天在这里说下分布式的基石:RPC(Remote Procedure Call:远程过程调用)技术,在Java中称为RMI( Remote Method Invocation ,远程方法调用)。
白话解释下:该技术就是让使用者在调用一个服务时的时候无感知地调用一个远程服务。这种无感知让大部分开发者不需要了解技术本身就可以很容易的使用 ,但是框架封装了底层(各种抽象、分层),使得无法知道内部是如何运行, 下面来介绍下什么是RPC。
存在这么一个方法
public class UserService { public String findNameById(Long id) { return "my name " + id; } }
正常调用
public class Test { public static void main(String[] args) { UserService userService = new UserService(); String name = userService.findNameById(1L); System.out.println(name); } }
控制台
my name 1 Process finished with exit code 0
这里不涉及框架,只是基本功能。到这里方法也调用了,功能也实现了。
方法执行核心
但是,某一天领导告诉你,你写的UserService没有用,具体的处理流程是另一个人管的,而且另一个人不和你一个项目,他(提供者)写好了一个实现
public class OtherUserService { public String findNameById(Long id) { return "other name " + id ; } }
此时你一想,他在大西洋,我怎么去调用他写的方法?于是RPC便可以出手了,RPC能干嘛?RPC能顺着网线去找到他。且看RPC如何做
先说方法调用: 正常情况下是通过实例对象+点操作符+方法名称调用
userService.findNameById(1L);
但是一个远程服务,是拿不到实例对象的,也就不能去写一个点操作符去调用,这时候方法的调用还可以通过反射
public Object invoke(Object obj, Object... args){...} // 第一个参数为 实例对象,第二个对象为 参数列表
但是Method这个也只能通过反射来获取,
public Method getMethod(String name, Class>... parameterTypes) // 第一个参数为 方法名称,第二个参数为参数类型列表
这样以来,方法的调用便可以分解为
获取类的Method实例(需要类标识、方法名称(标识)、方法参数类型列表)
通过Method实例调用方法(需要类实例对象、参数列表)
其中通过实例是可以获取其类型的,instance->class,这里的参数列表便可以获取对应参数类型列表,类实例对象可以获取类Class,但是这是在同一个JVM中才能完成的,一旦分离开来,这些都不能获取,而且分离时类实例对象只有提供者具有,使用者是不知道的(如果知道就没必要去远程调用了)。
所以:远程执行一个方法最少需要4个条件:类标识、方法标识、方法参数类型列表、方法参数列表。拿到这些条件,提供方就可以找到实例对象、调用对应的方法了,而且为了不随意构造实例对象,提供者会主动控制实例对象的构建,之后需要放到一个地方以方便交给Method的来调用
所以基本功能示意图如下,描述为:顺着网线,带着四大金刚,直接找到提供方,围殴一顿后,把结果带回来给使用者
RPC基本示意图
RPC就是对以上功能的完善与封装,好吧,上面描述太粗鲁了,代码写的要优雅!!!,要符合设计原则
接口分离原则
使用方和提供方是两个不同的模块,模块与模块之间应该是通过接口解耦的。所以一般在使用RPC服务时都会把接口层给解耦出来,作为第三方jar给使用方和提供方使用,也便于锁定实例对象。
单一职责原则
一个功能就应该职责单一,不仅容易看懂,也便于后续重构。RPC其实是多个功能模块组合起来的。例如:使用方参数封装模块、数据传输模块职责、数据序列化模块、提供方管理类实例模块等。一个完善的RPC功能组件,其职责也会分的越细,每个模块功能越单一。
开放封闭原则
作为一个RPC,其设计应该是满足开放封闭原则的,每个模块都应该是可扩展的,但是RPC流程应该是对内封闭的。
里氏替换原则
构建实例对象的时候,其类型定义应该是接口类型,而不是具体的实现类型。
依赖倒置原则
高层模块不应该依赖低层模块,二者都应该依赖其抽象对象-->抽象不应该依赖细节,细节应该依赖抽象-->应该针对接口编程,不应该针对实现编程。
RPC简单设计
先定义接口
接口应该单独一个包,作为jar对外提供。使用方和提供方都依赖该jar
public interface IUserService { /** * 通过ID获取用户名 * * @param id 用户ID * @return java.lang.String * @author Tinyice */ String findNameById(Long id); }
提供方实现
简单实现
public class OtherUserService implements IUserService { @Override public String findNameById(Long id) { return "other name " + id ; } }
使用方请求参数封装
主要封装四大调用条件对象
@Getter @Setter public class RpcRequest implements java.io.Serializable { private static final long serialVersionUID = -7223166969378743326L; /** * 类名称 */ private String className; /** * 方法名称 */ private String methodName; /** * 参数类型列表 */ private Class>[] parameterTypes; /** * 参数列表 */ private Object[] parameters; }
网络传输工具封装
网络传输方式有很多种,这里使用socket
@Slf4j public class TcpTransport { private String host; private int port; public TcpTransport(String host, int port) { this.host = host; this.port = port; } public Socket newSocketInstance() { Socket socket; try { socket = new Socket(host, port); log.info("[{}] 客户端新建连接:服务端地址=【{}】",LocalDateTime.now(), socket.getRemoteSocketAddress()); return socket; } catch (IOException e) { e.printStackTrace(); throw new RuntimeException("客户端连接失败"); } } public Object send(RpcRequest rpcRequest) { Socket socket = null; try { socket = newSocketInstance(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream()); objectOutputStream.writeObject(rpcRequest); objectOutputStream.flush(); log.info("[{}] 请求参数序列化完毕",LocalDateTime.now()); ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream()); Object obj = objectInputStream.readObject(); objectOutputStream.close(); objectInputStream.close(); log.info("[{}] 请求结果接收完毕",LocalDateTime.now()); return obj; } catch (Exception e) { throw new RuntimeException("RPC 调用异常"); } finally { try { if (socket != null) { socket.close(); } } catch (IOException e) { e.printStackTrace(); } } } }
其中send方法就是数据传输方法,包含请求参数的传输和请求结果的接收。
网络请求发起
为了让使用方感知不到具体实现的位置(本地还是原程),一般都会使用代理技术给接口构建一个本地代理对象,通过代理对象来进行远程调用,使得使用者产生使用的是本地方法的错觉。。
动态代理,主要分为JDK动态代理和CGLIB动态代理,不懂原理的可以去看我写的文章。为了简单,这里使用JDK动态代理
public class RpcClientProxy { publicT clientProxy(final Class interfaces, final String host, final int port) { return (T) Proxy.newProxyInstance(interfaces.getClassLoader(), new Class[]{interfaces}, new RpcInvocationHandler(host, port)); } }
RpcInvocationHandler为处理代理流程的类,也是网络请求发起类,发起位置在invoke方法,这是JDK动态代理的核心
@Slf4j public class RpcInvocationHandler implements InvocationHandler { private String host; private int port; public RpcInvocationHandler(String host, int port) { this.host = host; this.port = port; } @Override public Object invoke(Object proxy, Method method, Object[] args) { log.info("客户端开始封装参数【RpcRequest】"); RpcRequest rpcRequest = new RpcRequest(); rpcRequest.setClassName(method.getDeclaringClass().getName()); rpcRequest.setMethodName(method.getName()); rpcRequest.setParameterTypes(method.getParameterTypes()); rpcRequest.setParameters(args); log.info("客户端开始获取传输工具【tcpTransport】"); TcpTransport tcpTransport = new TcpTransport(host, port); log.info("客户端开始发送请求参数【RpcRequest】"); return tcpTransport.send(rpcRequest); } }
知道JDK动态代理,也就指定invoke对java方法调用的意义:方法的真正执行流程。这里封装了请求参数、然后发起网络请求,获取到网络请求结果。这个过程替代了方法的执行过程。
服务端接口监听
客户端和服务端是需要网络通信的,这边也要建立网络监听,这里为了简单,将类实例对象存储功能放在了一起
@Slf4j public class RpcServer { private ExecutorService executorService=Executors.newCachedThreadPool(); public void publishServer(final MapserviceRegistCenter, int port){ ServerSocket serverSocket; try { serverSocket=new ServerSocket(port); log.info("服务端启动监听,端口=[{}]",port); while (true) { Socket socket=serverSocket.accept(); log.info("服务端监听到新链接,客户端地址=【{}】",socket.getRemoteSocketAddress()); executorService.execute(new RpcServerProcessor(socket,serviceRegistCenter)); } } catch (IOException e) { e.printStackTrace(); } } }
服务端方法调用
上面的RpcServerProcessor就是服务端对客户端请求的处理方法
@Slf4j public class RpcServerProcessor implements Runnable { private Socket socket; private MapserviceRegistCenter; public RpcServerProcessor(Socket socket, Map serviceRegistCenter) { this.socket = socket; this.serviceRegistCenter = serviceRegistCenter; } @Override public void run() { // 获取客户端传输对象,并反序列化 try (ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream()); ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream())) { RpcRequest rpcRequest = (RpcRequest) inputStream.readObject(); log.info("[{}] 服务端反序列化完毕", LocalDateTime.now()); Object obj = invoke(rpcRequest); log.info("[{}] 服务端调用完毕", LocalDateTime.now()); objectOutputStream.writeObject(obj); objectOutputStream.flush(); log.info("[{}] 服务端序列化完毕——将结果发送给客户端", LocalDateTime.now()); } catch (Exception e) { e.printStackTrace(); } } private Object loadBalance(String className) { return serviceRegistCenter.get(className); } private Object invoke(RpcRequest rpcRequest) throws Exception { Object[] args = rpcRequest.getParameters(); Class>[] types = rpcRequest.getParameterTypes(); String className = rpcRequest.getClassName(); // 服务发现,负载均衡 Object service = loadBalance(className); // 服务调用 Method method = service.getClass().getMethod(rpcRequest.getMethodName(), types); return method.invoke(service, args); } }
这里也有个invoke方法,就是刚开始分析的反射方法调用,了解这块核心也就指定了RPC如何执行远程方法的。这里也涉及到了服务发现与负载均衡。
功能测试
先启动服务端,服务端发布服务
@Slf4j public class ServerBootStrap { public static void main(String[] args) { // 注册中心 MapregistCenter = new HashMap<>(16, 1); // 实例构建 IUserService userService = new OtherUserService(); // 实例注册 registCenter.put(IUserService.class.getName(), userService); RpcServer rpcServer = new RpcServer(); log.info("[{}] 服务端发布对外服务【IUserService】", LocalDateTime.now()); rpcServer.publishServer(registCenter, 8888); } }
控制台
[2020-09-25T20:29:40.699] 服务端发布对外服务【IUserService】 服务端启动监听,端口=[8888]
客户端调用
@Slf4j public class ClientBootStrap { public static void main(String[] args) { RpcClientProxy proxy = new RpcClientProxy(); IUserService userService = proxy.clientProxy(IUserService.class, "localhost", 8888); log.info("[{}] 客户端开始发起RPC调用", LocalDateTime.now()); String name = userService.findNameById(101L); System.out.println(name); } }
客户端控制台
[2020-09-25T20:29:55.737] 客户端开始发起RPC调用 客户端开始封装参数【RpcRequest】 客户端开始获取传输工具【tcpTransport】 客户端开始发送请求参数【RpcRequest】 [2020-09-25T20:29:55.748] 客户端新建连接:服务端地址=【localhost/127.0.0.1:8888】 [2020-09-25T20:29:55.763] 请求参数序列化完毕 [2020-09-25T20:29:55.769] 请求结果接收完毕 other name 101 Process finished with exit code 0
服务端控制台
[2020-09-25T20:29:40.699] 服务端发布对外服务【IUserService】 服务端启动监听,端口=[8888] 服务端监听到新链接,客户端地址=【/127.0.0.1:29456】 [2020-09-25T20:29:55.766] 服务端反序列化完毕 [2020-09-25T20:29:55.767] 服务端调用完毕 [2020-09-25T20:29:55.767] 服务端序列化完毕——将结果发送给客户端
可以通过时间顺序观察下调用流程。
总结
以上示例说明了RPC调用流程:包括服务注册、服务发现、负载均衡、请求参数封装、数据传输、序列化与反序列化等等,这里为了简单示例,只是做了演示。一个完善的RPC也是对上面功能的完善。例如:
服务注册可以选择JVM、Redis、Zookeeper、Nacos等
负载均衡:随机负载、轮询负载、权重负载等
数据传输:可以定制传输协议,例如dubbo、http、tcp/ip等
序列化:开源的序列化工具也很多,选择适宜的序列化能提高RPC能力,常见的有:JDK序列化、Hessian、Hessian2、Kryo、protostuff等
其他优化:服务配置、重试策略、失败拒绝策略、版本控制、权限控制等等。
在与Spring Boot集成中,服务客户端调用对象的代理可以和Sping的代理配合,实现无缝连接,直接注入使用。
最后
感谢大家看到这里,文章有不足,欢迎大家指出;如果你觉得写得不错,那就给我一个赞吧。
也欢迎大家关注我的公众号:程序员麦冬,麦冬每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!