RPC 是通信协议吗 ?→ 我们来看下它的演进过程
开心一刻
一实习小护士给我挂针,拿着针在我胳膊上扎了好几针也没找到血管
但这位小姑娘真镇定啊,表情严肃认真,势有不扎到血管不罢休的意思
十几针之后,我忍着剧痛,带着敬畏的表情问小护士:你这针法跟容嬷嬷学的么?
写在前面
单机应用中的方法调用很简单,直接调用就行,像这样
因为调用方与被调用方在一个进程内
随着业务的发展,单机应用会越来越力不从心,势必会引入分布式来解决单机的问题,那么调用方如何调用另一台机器上的方法呢 ?
这就涉及到分布式通信方式,从单机走向分布式,产生了很多通信方式
而 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 是通信协议吗 ? 欢迎评论区留言
参考
RPC 是通信协议吗 ?→ 我们来看下它的演进过程的更多相关文章
- (4)一起来看下mybatis框架的缓存原理吧
本文是作者原创,版权归作者所有.若要转载,请注明出处.本文只贴我觉得比较重要的源码,其他不重要非关键的就不贴了 我们知道.使用缓存可以更快的获取数据,避免频繁直接查询数据库,节省资源. MyBatis ...
- Windows操作系统下创建进程的过程
进程(Process)是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是系统进行资源分配和调度的一个独立单位.程序只是一组指令的有序集合,它本身没有任何运行的含义,只是一个静态实体.而进程则 ...
- Linux环境下Python的安装过程
Linux环境下Python的安装过程 前言 一般情况下,Linux都会预装 Python了,但是这个预装的Python版本一般都非常低,很多 Python的新特性都没有,必须重新安装新一点的版本,从 ...
- MySQL5.7.25(解压版)Windows下详细的安装过程
大家好,我是浅墨竹染,以下是MySQL5.7.25(解压版)Windows下详细的安装过程 1.首先下载MySQL 推荐去官网上下载MySQL,如果不想找,那么下面就是: Windows32位地址:点 ...
- Linux系统CentOS6.2版本下安装JDK7详细过程
Linux系统CentOS6.2版本下安装JDK7详细过程 分类: Linux 2014-08-25 09:17 1933人阅读 评论(0) 收藏 举报 前言: java 是一种可以撰写 ...
- [oracle] oracle的三种密码验证机制以及在windows和linux下的不同启动过程
oracle数据库的密码验证机制: ① 操作系统验证 拥有SYSDBA和SYSOPER的用户用该方式验证此时数据库无需启动,也无需开启监听和实例服务. 要求:本地组ora_dba中有该操作系统的登录用 ...
- Windows下OSGEarth的编译过程
目录 1. 依赖 1) OpenSceneGraph 2) GDAL 3) CURL 4) GEOS 5) 其他 2. 编译 1) 设置参数 2) 配置路径 3) 生成编译 3. 参考文献 1. 依赖 ...
- Day28--Python--网络通信协议 tcp与udp下的socket
昨日内容回顾: 1. CS架构 服务端客户端架构 软件CS架构: 京东,淘宝,QQ,微信,暴风影音,快播 硬件CS架构: 打印机 服务端: 提供服务的 客户端: 享受服务的 BS架构: 浏览器和服务端 ...
- 【已解决】checkout 配置无效的问题可以进来看下
在日常工作中,我们经常会遇到要更新一个项目,但是由于更改了配置,需要将这些配置commit或者checkout,但是有的同学不想commit怎么办呢,只能通过checkout,那么问题又来了,改了很多 ...
随机推荐
- springboot中使用h2数据库(内存模式)
使用H2的优点,不需要装有服务端和客户端,在项目中包含一个jar即可,加上初始化的SQL就可以使用数据库了 在springboot中引入,我的版本是2.1.4,里面就包含有h2的版本控制 <!- ...
- tornado 作业 简单首页 登录页 个人中心
s4 index.py 1 import tornado.ioloop 2 import tornado.web 3 import time 4 5 6 class IndexHandler(torn ...
- vmvare workstation虚拟机连接外网
在使用网上的yum源的时候,我们就需要我们的虚拟机能连接外网,在这里记录下配置vmvare workstation虚拟机连接外网的方法. 配置步骤: 1.打开主机的 "网络和Internet ...
- python 的基本语法
python3 默认的编码格式 :utf-8 标识符: 1.可以是数字,字母,下划线组成 2.不能以数字开头 3.不能以保留字命名(保留字即是关键字,比如"print") 4.区分 ...
- js下 Day09、事件(二)
一.事件流 事件流描述的是从页面中接收事件的顺序,目前主要有三个模型: #1. 事件冒泡: 事件开始时由最具体的元素接收,然后逐级向上传播到较为不具体的元素
- Spark内核-任务调度机制
作者:十一喵先森 链接:https://juejin.im/post/5e1c414fe51d451cad4111d1 来源:掘金 著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. ...
- 算法(Java实现)—— KMP算法
KMP算法 应用场景 字符串匹配问题 有一个字符串str1 = " hello hello llo hhello lloh helo" 一个子串str2 = "hello ...
- openstack高可用集群20-openstack计算节点宕机迁移方案
openstack计算节点宕机迁移方案 情景一:/var/lib/nova/instances/ 目录不共享的处理方法(类似手动迁移云主机到其他节点)
- 【python爬虫】一个简单的爬取百家号文章的小爬虫
需求 用"老龄智能"在百度百家号中搜索文章,爬取文章内容和相关信息. 观察网页 红色框框的地方可以选择资讯来源,我这里选择的是百家号,因为百家号聚合了来自多个平台的新闻报道.首先看 ...
- pygal之掷骰子 - 2颗面数为6的骰子
python之使用pygal模拟掷两颗面数为6的骰子的直方图,包含三个文件,主文件,die.py,dice_visual.py,20200527.svg.其中最后一个文件为程序运行得到的结果. 1,d ...