开心一刻

  一实习小护士给我挂针,拿着针在我胳膊上扎了好几针也没找到血管

  但这位小姑娘真镇定啊,表情严肃认真,势有不扎到血管不罢休的意思

  十几针之后,我忍着剧痛,带着敬畏的表情问小护士:你这针法跟容嬷嬷学的么?

写在前面

  单机应用中的方法调用很简单,直接调用就行,像这样

  因为调用方与被调用方在一个进程内

  随着业务的发展,单机应用会越来越力不从心,势必会引入分布式来解决单机的问题,那么调用方如何调用另一台机器上的方法呢 ?

  这就涉及到分布式通信方式,从单机走向分布式,产生了很多通信方式

  而 RPC 就是实现远程方法调用的方式之一;说 RPC 不是协议,可能很多小伙伴难以置信,以为我在骗你们

  看着你们这一身腱子肉,我哪敢骗你们;只要你们把下面的看完,骗没骗你们,你们自己说了算

RPC 的演进过程

  先说明一下,下文中的示例虽然是 Java 代码实现的,但原理是通用的,重点是理解其中的原理

  第一版

    两台机器之间进行交互,那么肯定离不开网络通信协议,TCP / IP 也就成了绕不开的点,所以先辈们最初想到的方法就是通过 TCP / IP 来实现远程方法的调用

    而操作系统是没有直接暴露 TCP / IP 接口的,而是通过 Socket 抽象了 TCP / IP 接口,所以我们可以通过 Socket 来实现最初版的远程方法调用

    完整示例代码:rpc-01,核心代码如下

    Server:

package com.qsl.rpc;

import com.qsl.rpc.entity.User;
import com.qsl.rpc.server.UserServiceImpl;
import com.qsl.rpc.service.IUserService; import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket; /**
* @author 青石路
* @date 2021/1/16 19:49
*/
public class Server {
private static boolean is_running = true; public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(8888);
while (is_running) {
System.out.println("等待 client 连接");
Socket client = serverSocket.accept();
System.out.println("获取到 client...");
handle(client);
client.close();
}
serverSocket.close();
} private static void handle(Socket client) throws Exception {
InputStream in = client.getInputStream();
OutputStream out = client.getOutputStream();
DataInputStream dis = new DataInputStream(in);
DataOutputStream dos = new DataOutputStream(out); // 从 socket 读取参数
int id = dis.readInt();
System.out.println("id = " + id); // 查询本地数据
IUserService userService = new UserServiceImpl();
User user = userService.getUserById(id); // 往 socket 写响应值
dos.writeInt(user.getId());
dos.writeUTF(user.getName());
dos.flush(); dis.close();
dos.close();
}
}

    Client:

package com.qsl.rpc;

import com.qsl.rpc.entity.User;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket; /**
* @author 青石路
* @date 2021/1/16 19:49
*/
public class Client { public static void main(String[] args) throws Exception {
Socket s = new Socket("127.0.0.1", 8888); // 网络传输数据
// 往 socket 写请求参数
DataOutputStream dos = new DataOutputStream(s.getOutputStream());
dos.writeInt(18);
// 从 socket 读响应值
DataInputStream dis = new DataInputStream(s.getInputStream());
int id = dis.readInt();
String name = dis.readUTF();
// 将响应值封装成 User 对象
User user = new User(id, name);
dos.close();
dis.close();
s.close(); // 进行业务处理
System.out.println(user);
}
}

    代码很简单,就是一个简单的 Socket 通信;如果看不懂,那就需要去补充下 Socket 和 IO 的知识

    测试结果如下

    可以看到 Client 与 Server 之间是可以进行通信的;但是,这种方式非常麻烦,有太多缺点,最明显的一个就是

      Client 端业务代码 与 网络传输代码 混合在一起,没有明确的模块划分

      如果有多个开发者同时进行 Client 开发,那么他们都需要知道 Socket、IO

  第二版

    针对第一版的缺点,演进出了这一版,引进 Stub (早期的叫法,不用深究,理解成代理就行)实现 Client 端网络传输代码的封装

    完整示例代码:rpc-02,改动部分如下

    Stub:

package com.qsl.rpc;

import com.qsl.rpc.entity.User;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket; /**
* 相当于一个静态代理,封装了网络数据传输
* @author 青石路
* @date 2021/1/17 9:38
*/
public class Stub { public User getUserById(Integer id) throws Exception {
Socket s = new Socket("127.0.0.1", 8888); // 网络传输数据
// 往 socket 写请求参数
DataOutputStream dos = new DataOutputStream(s.getOutputStream());
dos.writeInt(id);
// 从 socket 读响应值
DataInputStream dis = new DataInputStream(s.getInputStream());
int userId = dis.readInt();
String name = dis.readUTF();
// 将响应值封装成 User 对象
User user = new User(userId, name);
dos.close();
dis.close();
s.close(); return user;
}
}

    Client:

package com.qsl.rpc;

import com.qsl.rpc.entity.User;

/**
* @author 青石路
* @date 2021/1/16 19:49
*/
public class Client { public static void main(String[] args) throws Exception { // 不再关注网络传输
Stub stub = new Stub();
User user = stub.getUserById(18); // 进行业务处理
System.out.println(user);
}
}

    Client 不再关注网络数据传输,一心关注业务代码就好

    有小伙伴可能就杠上了:这不就是把网络传输代码移了个位置嘛,这也算改进?

    迭代开发是一个逐步完善的过程,而这也算是一个改进哦

    但这一版还是有很多缺点,最明显的一个就是

      Stub 只能代理 IUserService 的一个方法 getUserById ,局限性太大,不够通用

      如果想在 IUserService 新增一个方法: getUserByName ,那么需要在 Stub 中新增对应的方法,Server 端也需要做对应的修改来支持

  第三版

    第二版中的 Stub 代理功能太弱了,那有没有什么方式可以增强 Stub 的代理功能了?

    前面的 Stub 相当于是一个静态代理,所以功能有限,那静态代理的增强版是什么了,没错,就是:动态代理

    不熟悉动态代理的小伙伴,一定要先弄懂动态代理:设计模式之代理,手动实现动态代理,揭秘原理实现

    JDK 有动态代理的 API,我们就用它来实现

    完整示例代码:rpc-03,相较于第二版,改动比较大,大家需要仔细看

    Server:

package com.qsl.rpc;

import com.qsl.rpc.entity.User;
import com.qsl.rpc.server.UserServiceImpl;
import com.qsl.rpc.service.IUserService; import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.Socket; /**
* @author 青石路
* @date 2021/1/16 19:49
*/
public class Server {
private static boolean is_running = true; public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(8888);
while (is_running) {
System.out.println("等待 client 连接");
Socket client = serverSocket.accept();
System.out.println("获取到 client...");
handle(client);
client.close();
}
serverSocket.close();
} private static void handle(Socket client) throws Exception {
InputStream in = client.getInputStream();
OutputStream out = client.getOutputStream();
ObjectInputStream ois = new ObjectInputStream(in);
ObjectOutputStream oos = new ObjectOutputStream(out); // 获取方法名、方法的参数类型、方法的参数值
String methodName = ois.readUTF();
Class[] parameterTypes = (Class[]) ois.readObject();
Object[] args = (Object[]) ois.readObject(); IUserService userService = new UserServiceImpl();
Method method = userService.getClass().getMethod(methodName, parameterTypes);
User user = (User) method.invoke(userService, args); // 往 socket 写响应值;直接写可序列化对象(实现 Serializable 接口)
oos.writeObject(user);
oos.flush(); ois.close();
oos.close();
}
}

    Stub:

package com.qsl.rpc;

import com.qsl.rpc.entity.User;
import com.qsl.rpc.service.IUserService; import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.Socket; /**
* 动态代理,封装了网络数据传输
* @author 青石路
* @date 2021/1/17 9:38
*/
public class Stub { public static IUserService getStub() {
Object obj = Proxy.newProxyInstance(IUserService.class.getClassLoader(),
new Class[]{IUserService.class}, new NetInvocationHandler());
return (IUserService)obj;
} static class NetInvocationHandler implements InvocationHandler { /**
*
* @param proxy
* @param method
* @param args
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Socket s = new Socket("127.0.0.1", 8888); // 网络传输数据
ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream());
// 传输方法名、方法参数类型、方法参数值;可能会有方法重载,所以要传参数列表
oos.writeUTF(method.getName());
Class[] parameterTypes = method.getParameterTypes();
oos.writeObject(parameterTypes);
oos.writeObject(args); // 从 socket 读响应值
ObjectInputStream ois = new ObjectInputStream(s.getInputStream());
User user = (User) ois.readObject(); oos.close();
ois.close();
s.close(); return user;
}
}
}

    Client:

package com.qsl.rpc;

import com.qsl.rpc.entity.User;
import com.qsl.rpc.service.IUserService; /**
* @author 青石路
* @date 2021/1/16 19:49
*/
public class Client { public static void main(String[] args) throws Exception { IUserService userService = Stub.getStub();
//User user = userService.getUserById(23); User user = userService.getUserByName("李小龙");
// 进行业务处理
System.out.println(user);
}
}

    我们来看下效果

    此时, IUserService 接口的方法都能被代理了,即使它新增接口, Stub 不用做任何修改也能代理上

    另外, Server 端的响应值改成了对象,而不是单个属性逐一返回,那么无论 User 是新增属性,还是删减属性,Client 和 Server 都不受影响了

    这一版的改进是非常大的进步;但还是存在比较明显的缺点

      只支持 IUserService ,通用性还是不够完美

      如果新引进了一个 IPersonService ,那怎么办 ?

  第四版

    第三版相当于 Client 与 Server 端约定好了,只进行 User 服务的交互,所以 User 之外的服务,两边是通信不上的

    如果还需要进行其他服务的交互,那么 Client 就需要将请求的服务名作为参数传递给 Server,告诉 Server 我需要和哪个服务进行交互

    所以,Client 和 Server 都需要进行改造

    完整示例代码:rpc-04,相较于第三版,改动比较小,相信大家都能看懂

    Server:

package com.qsl.rpc;

import com.qsl.rpc.server.PersonServiceImpl;
import com.qsl.rpc.server.UserServiceImpl; import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap; /**
* @author 青石路
* @date 2021/1/16 19:49
*/
public class Server {
private static boolean is_running = true; private static final HashMap<String, Object> REGISTRY_MAP = new HashMap(); public static void main(String[] args) throws Exception { // 向注册中心注册服务
REGISTRY_MAP.put("com.qsl.rpc.service.IUserService", new UserServiceImpl());
REGISTRY_MAP.put("com.qsl.rpc.service.IPersonService", new PersonServiceImpl()); ServerSocket serverSocket = new ServerSocket(8888);
while (is_running) {
System.out.println("等待 client 连接");
Socket client = serverSocket.accept();
System.out.println("获取到 client...");
handle(client);
client.close();
}
serverSocket.close();
} private static void handle(Socket client) throws Exception {
InputStream in = client.getInputStream();
OutputStream out = client.getOutputStream();
ObjectInputStream ois = new ObjectInputStream(in);
ObjectOutputStream oos = new ObjectOutputStream(out); // 获取服务名
String serviceName = ois.readUTF();
System.out.println("serviceName = " + serviceName); // 获取方法名、方法的参数类型、方法的参数值
String methodName = ois.readUTF();
Class[] parameterTypes = (Class[]) ois.readObject();
Object[] args = (Object[]) ois.readObject(); // 获取服务;从服务注册中心获取服务
Object serverObject = REGISTRY_MAP.get(serviceName); // 通过反射调用服务的方法
Method method = serverObject.getClass().getMethod(methodName, parameterTypes);
Object resp = method.invoke(serverObject, args); // 往 socket 写响应值;直接写可序列化对象(实现 Serializable 接口)
oos.writeObject(resp);
oos.flush(); ois.close();
oos.close();
}
}

    Stub:

package com.qsl.rpc;

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.Socket; /**
* 动态代理,封装了网络数据传输
* @author 青石路
* @date 2021/1/17 9:38
*/
public class Stub { public static Object getStub(Class clazz) {
Object obj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new NetInvocationHandler(clazz));
return obj;
} static class NetInvocationHandler implements InvocationHandler { private Class clazz; NetInvocationHandler(Class clazz){
this.clazz = clazz;
} @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Socket s = new Socket("127.0.0.1", 8888); // 网络传输数据
ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream()); // 传输接口名,告诉服务端,我要哪个服务
oos.writeUTF(clazz.getName()); // 传输方法名、方法参数类型、方法参数值;可能会有方法重载,所以要传参数列表
oos.writeUTF(method.getName());
Class[] parameterTypes = method.getParameterTypes();
oos.writeObject(parameterTypes);
oos.writeObject(args); // 从 socket 读响应值
ObjectInputStream ois = new ObjectInputStream(s.getInputStream());
Object resp = ois.readObject(); oos.close();
ois.close();
s.close(); return resp;
}
}
}

    Client:

package com.qsl.rpc;

import com.qsl.rpc.entity.Person;
import com.qsl.rpc.entity.User;
import com.qsl.rpc.service.IPersonService;
import com.qsl.rpc.service.IUserService; /**
* @author 青石路
* @date 2021/1/16 19:49
*/
public class Client { public static void main(String[] args) throws Exception { /*IUserService userService = (IUserService)Stub.getStub(IUserService.class);
User user = userService.getUserByName("青石路");
System.out.println(user);*/ IPersonService personService = (IPersonService)Stub.getStub(IPersonService.class);
Person person = personService.getPersonByPhoneNumber("123");
System.out.println(person);
}
}

    此版本抽象的比较好了,屏蔽了底层细节,支持任何服务的任意方法,算是一个比较完美的版本了

    至此,一个最基础的 RPC 就已经实现了

    但是,还是有大量的细节可以改善,序列化与反序列化就是其中之一

      网络中数据的传输都是二进制,所以请求参数需要序列化成二进制,响应参数需要反序列化成对象

      而 JDK 自带的序列化与反序列化,具有语言局限性、效率慢、序列化后的长度太长等缺点

    序列化与反序列化协议非常多,常见的有

    

    这些协议孰好孰坏,本文不做过多阐述,这里提出来只是想告诉大家:序列化与反序列化协议是 RPC 中的重要一环

总结

  1、RPC 的演进过程

  2、RPC 的组成要素

    三要素:动态代理、序列化与反序列化协议、网络通信协议

    网络通信协议可以是 TCP、UDP,也可以是 HTTP 1.x、HTTP 2,甚至有能力可以是自定义协议

  3、RPC 框架

    RPC 不等同于 RPC 框架,RPC 是一个概念,是一个分布式通信方式

    基于 RPC 产生了很多 RPC 框架:Dubbo、Netty、gRPC、BRPC、Thrift、JSON-RPC 等等

    RPC 框架对 RPC 进行了功能丰富,包括:服务注册、服务发现、服务治理、服务监控、服务负载均衡等功能

  现在回到标题:RPC 是通信协议吗 ?  欢迎评论区留言

参考

  36行代码透彻解析RPC

  序列化和反序列化

  你应该知道的RPC原理

RPC 是通信协议吗 ?→ 我们来看下它的演进过程的更多相关文章

  1. (4)一起来看下mybatis框架的缓存原理吧

    本文是作者原创,版权归作者所有.若要转载,请注明出处.本文只贴我觉得比较重要的源码,其他不重要非关键的就不贴了 我们知道.使用缓存可以更快的获取数据,避免频繁直接查询数据库,节省资源. MyBatis ...

  2. Windows操作系统下创建进程的过程

    进程(Process)是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是系统进行资源分配和调度的一个独立单位.程序只是一组指令的有序集合,它本身没有任何运行的含义,只是一个静态实体.而进程则 ...

  3. Linux环境下Python的安装过程

    Linux环境下Python的安装过程 前言 一般情况下,Linux都会预装 Python了,但是这个预装的Python版本一般都非常低,很多 Python的新特性都没有,必须重新安装新一点的版本,从 ...

  4. MySQL5.7.25(解压版)Windows下详细的安装过程

    大家好,我是浅墨竹染,以下是MySQL5.7.25(解压版)Windows下详细的安装过程 1.首先下载MySQL 推荐去官网上下载MySQL,如果不想找,那么下面就是: Windows32位地址:点 ...

  5. Linux系统CentOS6.2版本下安装JDK7详细过程

    Linux系统CentOS6.2版本下安装JDK7详细过程 分类: Linux 2014-08-25 09:17 1933人阅读 评论(0) 收藏 举报 前言:        java 是一种可以撰写 ...

  6. [oracle] oracle的三种密码验证机制以及在windows和linux下的不同启动过程

    oracle数据库的密码验证机制: ① 操作系统验证 拥有SYSDBA和SYSOPER的用户用该方式验证此时数据库无需启动,也无需开启监听和实例服务. 要求:本地组ora_dba中有该操作系统的登录用 ...

  7. Windows下OSGEarth的编译过程

    目录 1. 依赖 1) OpenSceneGraph 2) GDAL 3) CURL 4) GEOS 5) 其他 2. 编译 1) 设置参数 2) 配置路径 3) 生成编译 3. 参考文献 1. 依赖 ...

  8. Day28--Python--网络通信协议 tcp与udp下的socket

    昨日内容回顾: 1. CS架构 服务端客户端架构 软件CS架构: 京东,淘宝,QQ,微信,暴风影音,快播 硬件CS架构: 打印机 服务端: 提供服务的 客户端: 享受服务的 BS架构: 浏览器和服务端 ...

  9. 【已解决】checkout 配置无效的问题可以进来看下

    在日常工作中,我们经常会遇到要更新一个项目,但是由于更改了配置,需要将这些配置commit或者checkout,但是有的同学不想commit怎么办呢,只能通过checkout,那么问题又来了,改了很多 ...

随机推荐

  1. eclipse提示JVM版本太低

    解决方案:去eclipse的安装路径找到eclipse.ini文件,然后在头部指定JVM的版本(第一第二行) -vm C:/Program Files/Java/jdk-11.0.9/bin -sta ...

  2. 用 Roslyn 做个 JIT 的 AOP

    0. 前言 上接:AOP有几种实现方式 接下来说说怎么做AOP的demo,先用csharp 说下动态编织和静态编织,有时间再说点java的对应内容. 第一篇先说Roslyn 怎么做个JIT的AOP d ...

  3. 记一次HBase的NotServingRegionException问题

    1. 问题 在hbase测试集群上,访问一些hbase表,均报类似下面的错误:region不可用 Wed Oct 28 14:00:56 CST 2020, RpcRetryingCaller{glo ...

  4. python 批量压缩手机视频

    先安装ffmpeg pip install ffmpeg-python -i https://pypi.tuna.tsinghua.edu.cn/simple 下面是代码,新建video_compre ...

  5. Appium常用的3种元素定位方式

    以最右App为例 .apk文件网盘地址: 链接:https://pan.baidu.com/s/1L4MYkhpb5ECe8XeaneTx_Q 提取码:0jqm 1.find_element_by_i ...

  6. HCIP --- BGP综合实验

    实验要求: 实验拓扑: 一.配置IP地址 L:代表环回地址(loop back 0) Y:代表业务网段的地址(loop back 1) 二.因为BGP基于IGP之上,给AS 2内配置OSPF 在R2上 ...

  7. C# AutoMapper:流行的对象映射框架,可减少大量硬编码,很小巧灵活,性能表现也可接受。

    AutoMapper 是一个对象-对象映射器,可以将一个对象映射到另一个对象. 官网地址:http://automapper.org/ 官方文档:https://docs.automapper.org ...

  8. C#中RDLC控制某列的显示隐藏

    使用 1.添加参数IsEnable(用于控制显示或隐藏)2.在RDLC页面,需要控制的列上,右键,列的可见性...,基于表达式隐藏或显示3.输入 IIF(Parameters!IsEnable.Val ...

  9. [译] ConstraintLayout 可视化[Design]编辑器(这到底是什么)[第四部分]

    原文地址:Testing Views in Isolation with Espresso 原文作者:Ataul Munim 译文出自:掘金翻译计划 译者:yazhi1992 校对者:lovexiao ...

  10. request常用方法servlet初步

    1 package com.ycw.newservlet; 2 3 import java.io.IOException; 4 import javax.servlet.ServletExceptio ...