Skip to content

Commit bb102ff

Browse files
committed
📚 update books
1 parent 5110982 commit bb102ff

File tree

3 files changed

+294
-6
lines changed

3 files changed

+294
-6
lines changed

docs/distribution/rpc/Hello-RPC.md

Lines changed: 264 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ calculator.add(1,2);
5151

5252
![img](https:////upload-images.jianshu.io/upload_images/7143349-9e00bb104b9e3867.png?imageMogr2/auto-orient/strip|imageView2/2/w/263/format/webp)
5353

54+
![](https://static001.geekbang.org/resource/image/82/59/826a6da653c4093f3dc3f0a833915259.jpg)
55+
5456
以左边的Client端为例,Application就是rpc的调用方,Client Stub就是我们上面说到的代理对象,也就是那个看起来像是Calculator的实现类,其实内部是通过rpc方式来进行远程调用的代理对象,至于Client Run-time Library,则是实现远程调用的工具包,比如jdk的Socket,最后通过底层网络实现实现数据的传输。
5557

5658
这个过程中最重要的就是**序列化****反序列化**了,因为数据传输的数据包必须是二进制的,你直接丢一个Java对象过去,人家可不认识,你必须把Java对象序列化为二进制格式,传给Server端,Server端接收到之后,再反序列化为Java对象。
@@ -174,6 +176,14 @@ RPC仅仅是微服务中的一部分。
174176

175177

176178

179+
180+
181+
182+
183+
184+
185+
186+
177187
维度 RPC REST
178188
耦合性 强耦合 松散耦合
179189
消息协议 二进制thrift、protobuf、avro 文本型XML、JSON
@@ -205,23 +215,271 @@ RPC框架的调用原理图如下:
205215

206216

207217

208-
## **三:RPC框架核心技术点**
218+
## RPC框架核心技术点
209219

210220
RPC框架实现的几个核心技术点总结如下:
211221

212222
1)远程服务提供者需要以某种形式提供服务调用相关的信息,包括但不限于服务接口定义、数据结构,或者中间态的服务定义文件,例如 Thrift的IDL文件, WS-RPC的WSDL文件定义,甚至也可以是服务端的接口说明文档;服务调用者需要通过一定的途径获取远程服务调用相关信息,例如服务端接口定义Jar包导入,获取服务端1DL文件等。
213223

214-
2)远程代理对象:服务调用者调用的服务实际是远程服务的本地代理,对于Java语言,它的实现就是JDK的动态代理,通过动态代理的拦截机制,将本地调用封装成远程服务调用.
224+
### 远程代理
215225

216-
3)通信:RPC框架与具体的协议无关,例如Spring的远程调用支持 HTTP Invoke、RMI Invoke, MessagePack使用的是私有的二进制压缩协议。
226+
动态代理,用 Spring AOP 的同学就不陌生了,他就是远程调用的魔法
217227

218-
4)序列化:远程通信,需要将对象转换成二进制码流进行网络传输,不同的序列化框架,支持的数据类型、数据包大小、异常类型及性能等都不同。不同的RPC框架应用场景不同,因此技术选择也会存在很大差异。一些做得比较好的RPC框架可以支持多种序列化方式,有的甚至支持用户自定义序列化框架( Hadoop Avro)
228+
当我们作为调用方使用接口时,RPC 会自动给接口生成一个代理类,我们在项目中注入接口的时候,运行过程中实际绑定的是这个接口生成的代理类。这样在接口方法被调用的时候,它实际上是被生成代理类拦截到了,这样我们就可以在生成的代理类里 面,加入远程调用逻辑
219229

220-
230+
通过这种“偷梁换柱”的手法,就可以帮用户屏蔽远程调用的细节,实现像调用本地一样地调用远程的体验,整体流程如下图所示:
231+
232+
![](https://static001.geekbang.org/resource/image/05/53/05cd18e7e33c5937c7c39bf8872c5753.jpg)
233+
234+
远程代理对象:服务调用者调用的服务实际是远程服务的本地代理,对于Java语言,它的实现就是JDK的动态代理,通过动态代理的拦截机制,将本地调用封装成远程服务调用.
235+
236+
237+
238+
### 通信
239+
240+
一次 RPC 调用,本质就是服务消费者与服务提供者间的一次网络信息交换的过程。
241+
242+
服务调用者通过网络 IO 发送一条 请求消息,服务提供者接收并解析,处理完相关的业务逻辑之后,再发送一条响应消息给服务调用者,服务调用者接收并解析响应消息,处理完相关的响应逻辑,一次 RPC 调用便结 束了。可以说,网络通信是整个 RPC 调用流程的基础。
243+
244+
#### 常见网络 IO 模型
245+
246+
那说到网络通信,就不得不提一下网络 IO 模型。为什么要讲网络 IO 模型呢?因为所谓的
247+
248+
两台 PC 机之间的网络通信,实际上就是两台 PC 机对网络 IO 的操作。
249+
250+
常见的网络 IO 模型分为四种:同步阻塞 IO(BIO)、同步非阻塞 IO(NIO)、IO 多路复 用和异步非阻塞 IO(AIO)。在这四种 IO 模型中,只有 AIO 为异步 IO,其他都是同步 IO。
251+
252+
其中,最常用的就是同步阻塞 IO 和 IO 多路复用,这一点通过了解它们的机制,你会 get 到。至于其他两种 IO 模型,因为不常用,则不作为本讲的重点,有兴趣的话我们可以在留 言区中讨论。
253+
254+
255+
256+
##### 阻塞 IO(blocking IO)
221257

258+
同步阻塞 IO 是最简单、最常见的 IO 模型,在 Linux 中,默认情况下所有的 socket 都是blocking 的,先看下操作流程。
259+
260+
首先,应用进程发起 IO 系统调用后,应用进程被阻塞,转到内核空间处理。之后,内核开 始等待数据,等待到数据之后,再将内核中的数据拷贝到用户内存中,整个 IO 处理完毕后 返回进程。最后应用的进程解除阻塞状态,运行业务逻辑。
261+
262+
这里我们可以看到,系统内核处理 IO 操作分为两个阶段——等待数据和拷贝数据。而在这 两个阶段中,应用进程中 IO 操作的线程会一直都处于阻塞状态,如果是基于 Java 多线程 开发,那么每一个 IO 操作都要占用线程,直至 IO 操作结束。
263+
264+
这个流程就好比我们去餐厅吃饭,我们到达餐厅,向服务员点餐,之后要一直在餐厅等待后
265+
厨将菜做好,然后服务员会将菜端给我们,我们才能享用。
266+
267+
268+
269+
##### IO 多路复用(IO multiplexing)
270+
271+
多路复用 IO 是在高并发场景中使用最为广泛的一种 IO 模型,如 Java 的 NIO、Redis、 Nginx 的底层实现就是此类 IO 模型的应用,经典的 Reactor 模式也是基于此类 IO 模型。
272+
273+
那么什么是 IO 多路复用呢? 通过字面上的理解,多路就是指多个通道,也就是多个网络连接的 IO,而复用就是指多个通道复用在一个复用器上。
274+
275+
多个网络连接的 IO 可以注册到一个复用器(select)上,当用户进程调用了 select,那么 整个进程会被阻塞。同时,内核会“监视”所有 select 负责的 socket,当任何一个 socket 中的数据准备好了,select 就会返回。这个时候用户进程再调用 read 操作,将数据从内核中拷贝到用户进程。
276+
277+
这里我们可以看到,当用户进程发起了 select 调用,进程会被阻塞,当发现该 select 负责 的 socket 有准备好的数据时才返回,之后才发起一次 read,整个流程要比阻塞 IO 要复杂,似乎也更浪费性能。但它最大的优势在于,用户可以在一个线程内同时处理多个 socket 的 IO 请求。用户可以注册多个 socket,然后不断地调用 select 读取被激活的 socket,即可达到在同一个线程内同时处理多个 IO 请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
278+
279+
280+
281+
### 序列化
282+
283+
网络传输的数据必须是二进制数据,远程通信时需要将对象转换成二进制码流进行网络传输,不同的序列化框架,支持的数据类型、数据包大小、异常类型及性能等都不同
284+
285+
> 序列化就是将对象转换成二进制数据的过程,而反序列就是反过来将二进制转换为对象的过程。
286+
287+
不同的RPC框架应用场景不同,因此技术选择也会存在很大差异。一些做得比较好的RPC框架可以支持多种序列化方式,有的甚至支持用户自定义序列化框架( Hadoop Avro)
288+
289+
![](https://static001.geekbang.org/resource/image/d2/04/d215d279ef8bfbe84286e81174b4e704.jpg)
290+
291+
**有哪些常用的序列化?**
292+
293+
#### JDK 原生序列化
294+
295+
Javer 肯定对这种原生的序列化方式最熟悉不过了
296+
297+
```java
298+
299+
import java.io.*;
300+
301+
public class Student implements Serializable {
302+
//学号
303+
private int no;
304+
//姓名
305+
private String name;
306+
307+
public int getNo() {
308+
return no;
309+
}
310+
311+
public void setNo(int no) {
312+
this.no = no;
313+
}
314+
315+
public String getName() {
316+
return name;
317+
}
318+
319+
public void setName(String name) {
320+
this.name = name;
321+
}
322+
323+
@Override
324+
public String toString() {
325+
return "Student{" +
326+
"no=" + no +
327+
", name='" + name + '\'' +
328+
'}';
329+
}
330+
331+
public static void main(String[] args) throws IOException, ClassNotFoundException {
332+
String home = System.getProperty("user.home");
333+
String basePath = home + "/Desktop";
334+
FileOutputStream fos = new FileOutputStream(basePath + "student.dat");
335+
Student student = new Student();
336+
student.setNo(100);
337+
student.setName("TEST_STUDENT");
338+
ObjectOutputStream oos = new ObjectOutputStream(fos);
339+
oos.writeObject(student);
340+
oos.flush();
341+
oos.close();
342+
343+
FileInputStream fis = new FileInputStream(basePath + "student.dat");
344+
ObjectInputStream ois = new ObjectInputStream(fis);
345+
Student deStudent = (Student) ois.readObject();
346+
ois.close();
347+
348+
System.out.println(deStudent);
349+
350+
}
351+
}
352+
```
353+
354+
我们可以看到,JDK 自带的序列化机制对使用者而言是非常简单的。
355+
356+
序列化具体的实现是由 ObjectOutputStream 完成的,而反序列化的具体实现是由 ObjectInputStream 完成的。
357+
358+
那么 JDK 的序列化过程是怎样完成的呢?我们看下下面这张图:
359+
360+
![](https://static001.geekbang.org/resource/image/7e/9f/7e2616937e3bc5323faf3ba4c09d739f.jpg)
361+
362+
序列化过程就是在读取对象数据的时候,不断加入一些特殊分隔符,这些特殊分隔符用于在反序列化过程中截断用。
363+
364+
- 头部数据用来声明序列化协议、序列化版本,用于高低版本向后兼容
365+
- 对象数据主要包括类名、签名、属性名、属性类型及属性值,当然还有开头结尾等数据,除了属性值属于真正的对象值,其他都是为了反序列化用的元数据
366+
- 存在对象引用、继承的情况下,就是递归遍历“写对象”逻辑
367+
368+
实际上任何一种序列化框架,核心思想就是设计一种序列化协议,将对象的类型、属性类型、属性值一一按照固定的格式写到二进制字节流中来完成序列化,再按照固定的格式一一读出对象的类型、属性类型、属性值,通过这些信息重新创建出一个新的对象,来完成反序列化。
369+
370+
371+
372+
#### JSON
373+
374+
JSON 是典型的 Key-Value 方式,没有数据类型,是一种文本型序列化框架
375+
376+
但用 JSON 进行序列化有这样两个问题,你需要格外注意:
377+
378+
- JSON 进行序列化的额外空间开销比较大,对于大数据量服务这意味着需要巨大的内存和磁盘开销;
379+
- JSON 没有类型,但像 Java 这种强类型语言,需要通过反射统一解决,所以性能不会太好。
380+
381+
所以如果 RPC 框架选用 JSON 序列化,服务提供者与服务调用者之间传输的数据量要相对较小,否则将严重影响性能。
382+
383+
384+
385+
#### Hessian
386+
387+
Hessian 是动态类型、二进制、紧凑的,并且可跨语言移植的一种序列化框架。Hessian 协议要比 JDK、JSON 更加紧凑,性能上要比 JDK、JSON 序列化高效很多,而且生成的字节数也更小。
388+
389+
```java
390+
391+
Student student = new Student();
392+
student.setNo(101);
393+
student.setName("HESSIAN");
394+
395+
//把student对象转化为byte数组
396+
ByteArrayOutputStream bos = new ByteArrayOutputStream();
397+
Hessian2Output output = new Hessian2Output(bos);
398+
output.writeObject(student);
399+
output.flushBuffer();
400+
byte[] data = bos.toByteArray();
401+
bos.close();
402+
403+
//把刚才序列化出来的byte数组转化为student对象
404+
ByteArrayInputStream bis = new ByteArrayInputStream(data);
405+
Hessian2Input input = new Hessian2Input(bis);
406+
Student deStudent = (Student) input.readObject();
407+
input.close();
408+
409+
System.out.println(deStudent);
410+
```
411+
412+
相对于 JDK、JSON,由于 Hessian 更加高效,生成的字节数更小,有非常好的兼容性和稳定性,所以 Hessian 更加适合作为 RPC 框架远程通信的序列化协议。
413+
414+
但 Hessian 本身也有问题,官方版本对 Java 里面一些常见对象的类型不支持,比如:
415+
416+
- Linked 系列,LinkedHashMap、LinkedHashSet 等,但是可以通过扩展 CollectionDeserializer 类修复;
417+
- Locale 类,可以通过扩展 ContextSerializerFactory 类修复;
418+
- Byte/Short 反序列化的时候变成 Integer。
419+
420+
421+
422+
#### Protobuf
423+
424+
Protobuf 是 Google 公司内部的混合语言数据标准,是一种轻便、高效的结构化数据存储格式,可以用于结构化数据序列化,支持 Java、Python、C++、Go 等语言。
425+
426+
Protobuf 使用的时候需要定义 IDL(Interface description language),然后使用不同语言的 IDL 编译器,生成序列化工具类,它的优点是:
427+
428+
- 序列化后体积相比 JSON、Hessian 小很多;
429+
- IDL 能清晰地描述语义,所以足以帮助并保证应用程序之间的类型不会丢失,无需类似 XML 解析器;
430+
- 序列化反序列化速度很快,不需要通过反射获取类型;
431+
- 消息格式升级和兼容性不错,可以做到向后兼容。
432+
433+
```protobuf
434+
435+
/**
436+
*
437+
* // IDl 文件格式
438+
* synax = "proto3";
439+
* option java_package = "com.test";
440+
* option java_outer_classname = "StudentProtobuf";
441+
*
442+
* message StudentMsg {
443+
* //序号
444+
* int32 no = 1;
445+
* //姓名
446+
* string name = 2;
447+
* }
448+
*
449+
*/
222450
451+
StudentProtobuf.StudentMsg.Builder builder = StudentProtobuf.StudentMsg.newBuilder();
452+
builder.setNo(103);
453+
builder.setName("protobuf");
454+
455+
//把student对象转化为byte数组
456+
StudentProtobuf.StudentMsg msg = builder.build();
457+
byte[] data = msg.toByteArray();
458+
459+
//把刚才序列化出来的byte数组转化为student对象
460+
StudentProtobuf.StudentMsg deStudent = StudentProtobuf.StudentMsg.parseFrom(data);
461+
462+
System.out.println(deStudent);
463+
```
464+
465+
466+
467+
以上只是些常见的序列化协议,还有 Message pack、kryo 等
468+
469+
470+
471+
RPC 框架如何选择序列化?需要考虑的因素
472+
473+
- 传输性能和效率
474+
- 空间开销(序列化后的二进制数据体积不能太大)
475+
- 通用性和兼容性
476+
- 安全性(别动不动就安全漏洞)
477+
478+
479+
480+
223481

224-
## **四、业界主流的RPC框架**
482+
## 四、业界主流的RPC框架
225483

226484
业界主流的RPC框架很多,比较出名的RPC主要有以下4种:
227485

docs/distribution/rpc/MyRPC.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
title: 实现自己的 RPC
3+
date: 2022-04-02
4+
tags:
5+
- RPC
6+
categories: RPC
7+
---
8+
9+
![](https://cdn.pixabay.com/photo/2022/03/06/01/51/activity-7050634_960_720.jpg)
10+
11+
12+

docs/distribution/rpc/RPC.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
## 服务发现
2+
3+
先举个例子,假如你要给一位以前从未合作过的同事发邮件请求帮助,但你却没有他的邮箱地址。这个时候你会怎么办呢?如果是我,我会选择去看公司的企业“通信录”。
4+
5+
同理,为了高可用,在生产环境中服务提供方都是以集群的方式对外提供服务,集群里面的 这些 IP 随时可能变化,我们也需要用一本“通信录”及时获取到对应的服务节点,这个获 取的过程我们一般叫作“服务发现”。
6+
7+
对于服务调用方和服务提供方来说,其契约就是接口,相当于“通信录”中的姓名,服务节 点就是提供该契约的一个具体实例。服务 IP 集合作为“通信录”中的地址,从而可以通过 接口获取服务 IP 的集合来完成服务的发现。这就是我要说的 PRC 框架的服务发现机制, 如下图所示:
8+
9+
![](https://static001.geekbang.org/resource/image/51/5d/514dc04df2b8b2f3130b7d44776a825d.jpg)
10+
11+
1. 服务注册:在服务提供方启动的时候,将对外暴露的接口注册到注册中心之中,注册中 心将这个服务节点的 IP 和接口保存下来。
12+
13+
2. 服务订阅:在服务调用方启动的时候,去注册中心查找并订阅服务提供方的 IP,然后缓 存到本地,并用于后续的远程调用。
14+
15+
16+
17+
18+

0 commit comments

Comments
 (0)