笔者以前仅看过RPC这个单词,彻底没有了解过,不想终于仍是碰上了。原由:这边想提升并发量而去看kafka(最后折中使用了redis),其中kafka须要安装ZooKeeper,而ZooKeeper又与分布式相关,再继续就发现分布式的基础是RPC,因而写下了这篇博文java
RPC(Remote Procedure Call)远程过程调用,即经过网络通讯来调用远程计算机程序上的服务,而这个调用过程就像调用本地方法同样简单透明,而且不须要了解底层的网络技术协议。RPC采用C/S架构,发出请求的程序是Client,提供服务的则是Server,相似于Http请求与响应。简单总结就是:调用的方法实际在远程,而要像调用本地方法同样简单。redis
1)对于客户端的我:调用本地的一个方法(存根)就能得到服务。 这个存根是远程服务的一个代理,其底层如何实现,对于我来讲是透明的。服务器
2)对于远程服务器:监听是否有链接过来,来了就调用对应的方法并返回(服务器端较易理解)网络
其结构图以下:架构
当咱们的业务量愈来愈庞大,垂直增长服务器的数量对提升性能的做用越发微乎,此时不免会采用分布式的架构以便更好地提升性能。分布式架构的每一个服务都是独立的部分,当须要完成某项业务且依赖不一样的服务式时,这些服务就须要互相调用,此时服务之间的调用就须要一种高效的应用程序之间的通信手段了,这就是PRC出现的缘由并发
提供服务:实现所提供的服务负载均衡
服务暴漏:仅仅实现了服务是不够的,还须要将提供的服务暴漏给外界,让外界知道有何,如何使用服务框架
远程代理对象:在调用本地方法时实际调用的是远程的方法,那么势必本地须要一个远程代理对象socket
总结:为了实现RPC须要有:通讯模型(BIO、NIO),服务定位(IP、PORT),远程代理对象(远程服务的本地代理),序列化(网络传输转换成二进制)分布式
其主要的对象有:服务端接口、服务端接口实现、服务暴漏、客户端接口(与服务端共享同个接口)、服务的引用
public interface Service { // 提供两个服务,说hello和整数相加 public String hello(); public int sum(int a, int b); }
public class ServiceImpl implements Service { @Override public String hello() { return "Hello World"; } @Override public int sum(int a, int b) { return a + b; } }
public static void export(Object service, int port) { if (service == null || port <= 0 || port > 65535) { throw new RuntimeException("Arguments error"); } System.out.println(service.getClass().getName() + ": " + port + "服务暴露"); new Thread( () -> { try (ServerSocket server = new ServerSocket(port);) { while(true){ try ( Socket socket = server.accept(); ObjectInputStream in = new ObjectInputStream(socket.getInputStream()); ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream()); ) { // 读取方法名 String methodName = in.readUTF(); // 读取参数类型 Class<?>[] parameterTypes = (Class<?>[])in.readObject(); // 读取参数值 Object[] arguments = (Object[])in.readObject(); // 获取方法 Method method = service.getClass().getMethod(methodName, parameterTypes); // 处理结果 Object result = method.invoke(service, arguments); // 写入结果 out.writeObject(result); } catch (Exception e) { e.printStackTrace(); } } } catch (IOException e1) { e1.printStackTrace(); } }).start(); }
这个暴露的逻辑是服务端监听特定端口,等客户端发起请求后链接,而后经过Java的IO流获取方法名,参数等相关信息,最后经过反射实现方法的调用并将结果响应给客户端
public interface ClientService { // 提供两个服务,说hello和整数相加 public String hello(); public int sum(int a, int b); }
public static <T>T refer(Class<T> interfaceClass, String host, int port){ if(interfaceClass == null || !interfaceClass.isInterface() || host == null || port <= 0 || port > 65535){ throw new RuntimeException("Arguments error"); } System.out.println("正在调用远程服务"); @SuppressWarnings("unchecked") T proxy = (T)Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class<?>[] {interfaceClass}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result = null; try ( Socket socket = new Socket(host, port); ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream()); ObjectInputStream in = new ObjectInputStream(socket.getInputStream()); ) { out.writeUTF(method.getName()); out.writeObject(method.getParameterTypes()); out.writeObject(args); result = in.readObject(); } catch (Exception e) { e.printStackTrace(); } return result; } }); return proxy; }
而引用服务的逻辑是:建立Socket套接字链接,序列化相关请求信息发送给服务端,而后等待响应结果。其中透明调用是使用了动态代理
public class Test { public static void main(String[] args) { // 暴露服务 ServiceImpl service = new ServiceImpl(); RPCFramework.export(service, 8080); // 调用服务 Client client = RPCFramework.refer(Client.class, "127.0.0.1", 8080); int sum = client.sum(1, 2); String rs = client.hello(); System.out.println("远程响应:" + sum); System.out.println("远程响应:" + rs); } }
RPC.ServiceImpl:8080----- 服务暴露 正在调用远程服务 远程响应:3 远程响应:Hello World
RPC与具体协议无关,可基于Http、TCP,但由于TCP性能相对较好。Http属于应用层协议,TCP属于传输层协议,相对在底层少了一层封装,并且为了可靠传输而选择TCP不选择UDP
Dubbo(阿里巴巴)、SpringCloud、RMI(JDK内置)
由于要像本地调用同样,对于使用者来讲是透明的。
Object result = XXX(String method, String host, int port)
上面这样其实也行,但并不能感受到是调用本地方法同样,并且若是一个接口有多个方法的话,每调用一次方法就须要发送一次host / port
// 动态代理能够这样使用 ProxyObject.方法1 ProxyObject.方法2 // 没有使用动态代理则不人性化 XXX(String method1, String host, int port) XXX(String method2, String host, int port)
为了方便分辨方法的重载,下面获取方法须要方法名和参数类型
service.getClass().getMethod(methodName, parameterTypes)
上面事例中采用BIO形式,阻塞访问而致使并发量不高,能够用NIO代替
这里用了JDK原生方法只能序列化实现了Serializable接口的类,可使用第三方的类库来提升性能
服务的自动发现,客户端能动态感知服务端的变化,从实现热部署,可用定时轮询的方法,eg:ZooKeeper
集群化,这样即可以提供负载均衡
请求与响应能够进行编码封装,而不是这样单独一个一个发送