开心一刻

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

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

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

写在前面

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

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

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

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

  而 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. 戴尔iDRAC+Ubuntu 18.04系统安装

    Ubuntu镜像下载链接:http://mirrors.aliyun.com/ubuntu-releases/18.04/ 1.登录戴尔管理口 2.点击虚拟控制台 3.选择镜像 4.挂载镜像 5.选择 ...

  2. 一文搞懂I/O多路复用机及其技术

    前言 ​ 高性能是每个程序员的追求,无论写一行代码还是做一个系统,都希望能够达到高性能的效果.高性能架构设计主要集中在两方面: 尽量提升单服务器的性能,将单服务器的性能发挥到极致 如果单服务器无法支撑 ...

  3. Django DRF 分页

    Django DRF 分页 分页在DRF当中可以一共有三种,可以通过setttings设置,也可也通过自定义设置 PageNumberPagination 使用URL http://127.0.0.1 ...

  4. 08-flask-使用pymysql

    代码 from flask import Flask from flask import render_template import pymysql # 创建flask对象 app = Flask( ...

  5. css进阶 02-CSS布局

    02-CSS布局 #前言 #常见的布局属性 (1)display 确定元素的显示类型: block:块级元素. inline:行内元素. inline-block:对外的表现是行内元素(不会独占一行) ...

  6. antdv的Upload组件实现前端压缩图片并自定义上传功能

    Ant Design of Vue的Upload组件有几个重要的api属性: beforeUpload: 上传文件之前的钩子函数,支持返回一个Promise对象. customRequest: 覆盖组 ...

  7. python去除文件中重复的行

    去除文件中重复的行 import os with open('db.txt','r',encoding='utf-8') as read_f,\ open('.db.txt.swap','w',enc ...

  8. github无法访问解决方法

    windows 系统下找到目录 C:\Windows\System32\drivers\etc 打开 hosts 文件 添加以下配置 #github140.82.114.3 github.com 保存 ...

  9. 最新 obs-studio vs2019 开发环境搭建 代码编译

    距离上一篇文章很久了,重新开始记录 OBS 开发相关情况,第一步就是环境搭建,第二步是构建 OBS-Studio VS 2019 开发环境搭建 下载软件和资源 软件安装没有特别说明的,下载安装即可. ...

  10. sqoop用法之mysql与hive数据导入导出

    目录 一. Sqoop介绍 二. Mysql 数据导入到 Hive 三. Hive数据导入到Mysql 四. mysql数据增量导入hive 1. 基于递增列Append导入 1). 创建hive表 ...