当前位置:首页 » 《我的小黑屋》 » 正文

25 届秋招回暖了?前端、后端热门面试十五题详解

11 人参与  2024年09月22日 15:20  分类 : 《我的小黑屋》  评论

点击全文阅读


秋招正式批已经开始了,面试鸭也更新了 25 届秋招热题,打开主页就能看到!

不久前刷某乎首页,鸭鸭刷到了这么一个问题:

image-20240827173241798

大环境真的变好了吗?鸭鸭不好说。不过今年确实有不少岗位释出。

互联网上关于秋招的问题也变多起来:

2025年计算机秋招情况如何?

Java、python、C、C++、Go、Rust……哪个语言更吃香?

软件开发、产品、测试、运维、算法、大数据……该投什么岗位?

25届秋招,现在学嵌入式软件更好还是硬件好呢?

大家心里都有自己的那份答案了吗?

近来在校的同学也陆陆续续开学了,新学期,可不要忘了鸭鸭,大家继续卷起来!

鸭鸭为大家准备了一些前端和后端开发的热门面试题!希望大家都能好好备战秋招,收获心仪 offer!

面试鸭:https://www.mianshiya.com

前端热题

1,什么是 HTML 语义化?

回答重点

HTML 语义化是指根据内容的结构和含义(内容语义化),选择合适的 HTML 标签(代码语义化),以更好地表达内容的意义和层次。通俗来讲,就是用正确的标签做正确的事情。

语义化的优点

1)对机器友好:

SEO 友好:语义化的标签让搜索引擎更容易理解网页内容,有利于搜索引擎优化。提高可访问性:例如屏幕阅读器,可以通过语义化标签更好地理解和朗读网页内容,帮助视障用户浏览网页。内容组织:语义化标签有助于搜索引擎爬虫和其他自动化工具更好地抓取和索引网页内容,甚至自动生成目录。

2)对开发者友好:

代码可读性:语义化标签增强了代码的可读性,开发者可以更清晰地理解网页结构和内容。维护和协作:清晰的结构有助于团队协作和项目维护,使得开发者能够快速定位和修改代码。
常见的语义化标签

以下是一些常见的 HTML5 语义化标签及其用途:

<header>:定义文档或部分的头部,通常包含导航、logo 等。<nav>:定义导航链接部分,包含一组导航链接。<section>:定义文档中的一个区块,用于分隔内容。<main>:定义文档的主要内容,文档中主体部分的容器。<article>:定义独立的内容单元,例如文章、博客帖子、新闻等。<aside>:定义与主内容相关的辅助内容,通常为侧边栏。

2,HTML 的 src 和 href 属性有什么区别?

HTML 中的 src 和 href 属性虽然都是用于指定资源的地址,但在具体应用标签和资源加载方式上有着明显的区别。

1、应用标签不同

这两个属性分别适用于不同的 HTML 标签。误用 href 替代 src(或者反之)可能会导致页面功能无法正常运作,因为浏览器不会对错误的属性做出正确的响应。
src 属性的作用是 指定要加载的资源路径,常出现于 <script><img><audio><video><iframe> 等标签中,用于加载 JavaScript 脚本、图像、音频、视频或嵌入的网页文件。
href 属性的作用是 **指定超链接的目标地址 **或定义文档与外部资源的关联,主要用在 <a><link><area> 等标签中。例如,当你创建一个超链接 <a> 时,需要用 href 属性指定用户点击后跳转的目标 URL;或者当你在文档头部使用 <link> 标签引入外部样式表时,也是使用 href 属性来指定样式表的地址。

2、资源加载方式不同

当浏览器解析到适用于 src 属性的标签(比如 <script><img>)时,会暂停其他资源的下载和处理,直到将该资源加载、编译(如果是 JavaScript)、执⾏(如果是脚本)完成。这种方式称为阻塞加载,所以⼀般建议将 JavaScript 脚本放在页面底部。
而当浏览器识别到适用于 href 属性的标签(比如 <a><link>)时,会并⾏下载资源,不会停⽌对当前⽂档的处理。这种方式称为非阻塞加载,浏览器可以同时处理超链接或引入样式表。

3,DOCTYPE(文档类型)的作用是什么?

DOCTYPE 是 HTML5 中一种标准通用标记语言的文档类型声明,它的目的是告诉浏览器(解析器)应该以什么样(html 或 xhtml)的文档类型定义来解析文档<!DOCTYPE> 不是一个 HTML 标签,它是一个指令,负责告诉浏览器页面使用哪个 HTML 版本进行编写。

主要作用

1)启用浏览器的标准模式(Standards mode)或怪异模式(Quirks mode)。
2)用于告知浏览器应使用哪种 HTML 版本来解析文档。这一声明对于确保网页在不同浏览器中的正确显示和行为具有至关重要的作用。DOCTYPE 声明位于 HTML 文档的最顶部,是文档的第一行。

标准模式和怪异模式的区别

1)CSS1Compat(标准模式):浏览器按照 W3C 的标准严格解析代码,网页的显示效果和性能表现更加符合现代web标准。例如:语雀官网的文档类型就是 CSS1Compat**。**
2)BackCompat(怪异模式):假如文档缺少 DOCTYPE 或使用错误的 DOCTYPE ,浏览器就可能采用这种模式,页面可能不会按照开发者的预期渲染。

不同的渲染模式会影响浏览器对 CSS 代码甚⾄ JavaScript 脚本的解析。

为什么重要?

正确的 DOCTYPE 声明是现代 web 开发的基础,有助于:

提高页面的兼容性,确保在不同的浏览器和设备上都能如预期般正常显示。减少调试和维护的时间,避免了因浏览器解析差异导致的多数常见问题。

示例
对于 HTML5 文档,正确的DOCTYPE声明如下:

<!DOCTYPE html>

这行代码告诉所有浏览器该页面使用 HTML5 标准解析。推荐所有现代网页都使用以上声明,因为它启用了浏览器的标准模式,并支持所有最新的 HTML 和 CSS 特性。

4,HTML5 相比于 HTML 有哪些更新?

HTML5 是 HTML 最新的标准,它⽐ HTML 更加规范,并添加了很多新的特性:
1)语义化更强的 HTML 元素:引入如 article、section、nav、header 和 footer 等元素,帮助创建结构更清晰、语义明确的网页,有利于 SEO 和内容的可访问性。
2)表单控件增强:新增多种表单输入类型(如 email、date),直接支持数据验证,极大地提高了表单的易用性和功能性。
3)音视频支持:原生支持音频(audio)和视频(video)内容,无需依赖外部插件,提高了多媒体内容的访问速度和兼容性。
4)新的 API 支持:引入了多个强大的 API,如 Canvas、Geolocation、Drag and Drop,增强了网页的功能性,使其能支持更复杂的用户交互。
5)Web 存储和 WebSockets:提供了更先进的数据存储方案(localStorage 和 sessionStorage)和实时通信能力(WebSockets),让网页应用更像传统的桌面应用。
6)更好的连接性和离线支持:通过应用程序缓存(Application Cache)支持离线应用,使得 Web 应用能够更灵活地在没有网络的环境下使用。

1、语义化标签
header:定义文档的页眉(头部)。footer:定义文档或节的页脚(底部)。nav:定义导航链接的部分。article:定义独立的文章内容。section:定义文档中的节(section、区段)。aside:定义与页面主内容相关联但又相对独立的内容,如侧边栏。
2、媒体标签

HTML5 提高了原生媒体支持,无需额外插件即可播放音频和视频:
1)Audio 标签:用于嵌入音频内容。

<audio src="audio.mp3" controls autoplay loop></audio>

2)Video 标签:用于嵌入视频内容。

<video src="video.mp4" poster="poster.jpg" controls></video>

3)Source 标签:在音视频标签内使用,为不同的浏览器提供多种格式的媒体文件。

<video controls>    <source src="video.mp4" type="video/mp4">    <source src="video.webm" type="video/webm"></video>
3、表单增强

HTML5 对表单控件也进行了扩展,增加了多种类型的输入控件,使得收集和验证用户输入更加方便:

type=“email”、type=“url”:自动验证用户输入格式。type=“number”、type=“range”:输入数字或范围。type=“search”:优化的搜索框。type=“color”:颜色选择器。placeholder:输入框为空时显示的提示文字。required、pattern:简化了数据验证过程。time:时分秒data:日期选择年月日datatime:时间和日期(目前只有Safari支持)datatime-local:日期时间控件week:周控件month:月控件
4、新的 API

HTML5 引入了许多强大的 JavaScript API,支持更复杂的网页应用:
1)拖放API:允许用户拖放文件直接到网页中。

<img draggable="true" />

2)Web Storage:提供 localStorage 和 sessionStorage,用于在客户端存储数据。
3)Canvas API:用于在网页上绘制图形。

<canvas id="myCanvas" width="200" height="100"></canvas>

4)Geolocation API:允许网站访问用户的地理位置。

5、进度条和度量器

progress:显示操作的进度(IE、Safari不支持)。
meter:显示磁盘使用情况的标量值。(IE、Safari不支持)

6、更好的连接性

Offline Web Applications:HTML5 提供了应用程序缓存,允许网站在离线状态下运行。

7、 WebSockets

提供了一种在单个连接上进行全双工通讯的方式,使得实时数据通讯(如聊天应用)更加有效和资源节约。

8、 更丰富的图形和效果

CSS3 Integration:HTML5 与 CSS3 紧密集成,支持更复杂和动态的视觉效果。
SVG Integration:支持可缩放矢量图形 (SVG) 的直接嵌入。

9、移除过时元素

比如纯表现的元素:basefont,big,center,font, s,strike,tt,u;
对可用性产生负面影响的元素:frame,frameset,noframes;

5,JavaScript 有哪些数据类型?它们的区别是什么?

JavaScript 有八种基本数据类型,分为原始类型(Primitive Types)和引用类型(Reference Types):

原始类型

1)Undefined:表示变量未初始化。一个变量声明后但未赋值时,它的默认值是 undefined。

2)Null:表示一个空的值或一个不存在的对象。null 是一个特殊的关键字,它代表“无值”。

3)Boolean:只有两个值:true 和 false,用于逻辑判断。

4)Number:表示双精度 64 位二进制格式的浮点数,可以表示整数和浮点数。特殊值包括 NaN(Not a Number)和 Infinity。

5)String:表示字符序列,可以用单引号、双引号或反引号括起来的文本。

6)Symbol:用来创建唯一且不可变的值,主要用于对象属性的唯一标识,避免属性名冲突。

7)BigInt:用于表示任意精度的大整数,允许操作超过 Number 能表示的范围的整数。

引用类型

Object(包括普通对象、数组、函数等)

两者区别

存储区别
1)原始类型存储在栈(stack)中,值直接保存在变量访问的位置,由于其大小固定且频繁使用,存储在栈中具有更高的性能。

2)引用类型存储在堆(heap)中,占用空间较大且大小不固定,变量保存的是对实际对象的引用(即指针),这些引用存储在栈中。
赋值方式区别
1)原始类型:复制的是值本身。例如,将一个 number 类型的变量赋值给另一个变量,两个变量互不影响。

2)引用类型:复制的是引用(指针)。多个变量引用同一个对象时,一个变量的修改会影响其他变量。

6,JavaScript 中 null 和 undefined 的区别是什么?

undefined 是 JavaScript 的一种内置数据类型,表示变量声明了但未赋值。null 同样是一种内置数据类型,表示一个空对象引用。

两者区别

类型检测
1)使用 typeof 检测 undefined 会返回 “undefined”。
2)使用 typeof 检测 null 会返回 “object”,这是一个历史遗留问题。

console.log(typeof undefined); // 输出: "undefined"console.log(typeof null); // 输出: "object"

比较操作
1)undefined 和 null 使用双等号 == 比较时会被认为相等,因为它们都代表“没有值”的概念。
2)使用严格等号 === 比较时,它们是不相等的,因为它们是不同类型的值。

console.log(undefined == null); // 输出: trueconsole.log(undefined === null); // 输出: false

变量赋值
1)undefined 是 JavaScript 引擎自动赋予未赋值变量的值,而 null 是开发者显式赋值以表示变量没有值。

let x; // 未赋值,默认是 undefinedlet y = null; // 明确赋值为 null

7,JavaScript 的 BigInt 和 Number 类型有什么区别?

回答重点

JavaScript 的 BigIntNumber 类型主要区别在于它们表示和处理的数值范围不同。Number 使用的是 IEEE 754 双精度浮点数表示法,能表示的数值范围大约是 -2^53 到 2^53 之间。而 BigInt 则可以表示任意大小(理论上无限大)的整数,从而克服了 Number 类型表示大整数的局限。

数值范围

Number: 大约从 -2^53 到 2^53 之间。BigInt: 可以表示非常大的整数(远超 Number 的范围)。

数据类型

Number: 任意大小的浮点数。BigInt: 仅表示大整数,不支持浮点数。

操作方式

Number 类型可以进行 +, -, *, / 等操作,不过在超大数值运算时精度可能会有损失。BigInt 类型主要用于整数操作,这样的运算不会出现精度损失,但是需要使用 BigInt 特有的运算符和方法。

表示方式

Number: 常规表示法,如 423.14。BigInt: 整数后添加 n,如 9007199254740991n

8,什么是 JavaScript 的尾调用?使用尾调用有什么好处?

回答重点

尾调用是指函数内部的最后一个操作是调用另一个函数的情况。在 JavaScript 中,当一个函数调用发生在另一个函数的尾部(即调用结束后直接返回其结果,而无需进一步操作)时,这种调用称为尾调用。

使用尾调用的主要好处在于其对栈内存的优化。通常情况下,每一个函数调用都会在栈内存中占据一个新的框架(frame),直到函数执行完成。而尾调用因为不需要保留当前函数的执行上下文,因此可以直接复用当前的栈帧,从而使递归操作更加高效,避免栈溢出(stack overflow)的风险。

9,说说你对 JS 作用域的理解?

回答重点

作用域,其实就是一个变量或函数在代码中的可访问范围。在 JavaScript 中,主要有两种作用域:全局作用域和局部作用域。全局作用域的变量在整个脚本中都可访问,而局部作用域的变量只能在特定的代码块、函数内使用。

全局作用域:定义在所有函数体以及其他代码块之外的变量,称为全局变量。它们在脚本的任何地方都是可访问的。

局部作用域:局部变量定义在函数内或代码块内(如 iffor 块),它们只能在函数内或代码块内访问。局部作用域又可细分为函数作用域和块作用域。

函数作用域:只在函数内部可见的变量,这种作用域在早期的 JavaScript 中非常常见。

块作用域:ES6 引入的 letconst 关键字,使得可以在块级代码(类似 {})内部定义变量,即所谓的块作用域。

10,TypeScript 有哪些常用类型?

TypeScript 的常用类型包括:

基础类型:string、number、boolean、null、undefined、symbol、bigint复杂类型:array、tuple、enum、object特殊类型:any、unknown、never、void

下面分别讲解:

基础类型
string:表示字符串。例如:let name: string = “John”number:表示数字数据,包括整数和浮点数。例如:let age: number = 30boolean:表示布尔值,只有 true 和 false 两种取值。例如:let isActive: boolean = truenull:表示空值,通常与 undefined 一起使用undefined:表示未定义的值symbol:表示独一无二的值,主要用于对象属性的唯一标识。例如:let sym: symbol = Symbol()bigint:表示任意精度的整数。例如 12345678901234567890123456数组 []元组 Tuple
复杂类型

1)array 数组:表示元素类型固定的列表。例如:

// 1、在元素类型后面接上[],表示由此类型元素组成的一个数组let numbers: number[] = [1, 2, 3];// 2、使用数组泛型,Array<元素类型>let numbers: Array<number> = [1, 2, 3];

2)tuple 元祖:表示已知数量和类型的数组。例如:

let x: [string, number] = ["hello", 10];当访问一个已知索引的元素,会得到正确的类型:console.log(x[0].substr(1)); // OKconsole.log(x[1].substr(1)); // Error, 'number' does not have 'substr'当访问一个越界的元素,会使用联合类型替代x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toStringx[6] = true; // Error, 布尔不是(string | number)类型

3)enum 枚举:用于定义一组命名常量。例如:

enum 类型是对 JavaScript 标准数据类型的一个补充。 像 C# 等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。

enum Color {  Red,  Green,  Blue}let c: Color = Color.Green;

默认情况下,从 0 开始为元素编号。 你也可以手动的指定成员的数值。或者,全部都采用手动赋值

// 1、手动的指定成员的数值enum Color {Red = 1, Green, Blue}let c: Color = Color.Green;// 2、全部都采用手动赋值enum Color {Red = 1, Green = 2, Blue = 4}let c: Color = Color.Green;

4)object:表示非原始类型的值,例如对象、数组等。例如:

let person: { name: string; age: number } = { name: "John", age: 30 };
特殊类型

1)any:表示任意类型,允许任何类型的值。通常用于处理动态内容或逐步迁移到 TypeScript 的项目。例如:

let anything: any = "hello";anything = 10;

2)unknown:表示未知类型,与 any 类似,但更安全,必须在使用之前进行类型检查。例如:

let notSure: unknown = 4;if (typeof notSure === "number") {    let sure: number = notSure;}

3)never:表示不会发生的值,通常用于标识函数从不会返回(如抛出异常)或永远不会有结果的情况。例如:

function error(message: string): never {    throw new Error(message);}

4)void:表示没有返回值的函数。例如:

function warnUser(): void {    console.log("This is a warning message");}

某种程度上来说,void 类型像是与any类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void 。

刷题就来面试鸭:https://www.mianshiya.com

后端热题

11,MySQL 的 Change Buffer 是什么?它有什么作用?

我们来看一下官网的一张图:

710.png

从上面的图我们可以看到, buffer pool 里面其实有一块内存是留给 change buffer 用的。

1)那 change buffer 具体是个什么东西呢?

假设我们直接执行一条 update table set name = 'yes' where id = 1,如果此时 buffer pool 里没有 id 为 1 的这条数据,那怎么办?

难道把这条数据先加载到 buffer pool 中,然后再执行修改吗?

当然不是。

如果当前数据页不在 buffer pool 中,那么 innodb 会把更新操作缓存到 change buffer 中,当下次访问到这条数据后,会把数据页加载到 buffer pool 中,并且应用上 change buffer 里面的变更,这样就保证了数据的一致性。

2) change buffer 有什么好处?

当二级索引页不在 buffer pool 中时,change buffer 可以避免立即从磁盘读取对应索引页导致的昂贵的随机I/O ,对应的更改可以在后面当二级索引页读入 buffer pool 时候被批量应用。

注意, change buffer 只能用于二级索引的更改,不适用于主键索引,空间索引以及全文索引

还有,唯一索引也不行,因为唯一索引需要读取数据然后检查数据的一致性。

3)那更改先缓存在 change buffer 中,假如数据库出问题,更改不是丢了吗?

别怕,change buffer 也是要落盘存储的,从上图我们看到 change buffer 会落盘到系统表空间里面,然后 redo log 也会记录 chang buffer 的修改来保证数据一致性。

change buffer 主要用来避免于二级索引页修改产生的随机I/O。如果你的内存够大能装下所有数据,或者二级索引很少,或者你的磁盘是固态的对随机访问影响不大,其实可以关闭 change buffer,因为它也增加了复杂度,当然最终还是得看压测结果。

12,Redis 主从复制的实现原理是什么?

回答重点

下图就是一个 Redis 主从架构图:

image.png

主从架构可以实现读写分离。写操作可以请求主节点,而读操作只请求从节点,这样就能减轻主节点的压力。

image.png

整个主从集群仅主节点可以写入,其它从节点都通过复制来同步数据,这样就能保证数据的一致性。并且对读请求分散到多个节点,提高了 Redis 的吞吐量,从一定程度上也提高了 Redis 的可用性。

主从复制原理

Redis 之间主从复制主要有两种数据同步方式,分别是全量同步增量同步

1) 全量同步

image.png runid 指的是主服务器的 run ID,从节点第一次同步不知道主节点 ID,于是传递 “?”。offset 为复制进度,第一次同步值为 -1。

待同步完毕后,主从之间会保持一个长连接,主节点会通过这个连接将后续的写操作传递给从节点执行,来保证数据的一致。

2) 增量同步

主从之间的网络可能不稳定,如果连接断开,主节点部分写操作未传递给从节点执行,主从数据就不一致了。

此时有一种选择是再次发起全量同步,但是全量同步数据量比较大,非常耗时。因此 Redis 在 2.8 版本引入了增量同步(psync 其实就是 2.8 引入的命令),仅需把连接断开其间的数据同步给从节点就好了。

增量同步也是 psync 命令,如果主节点判断从节点传递的 runid 和主节点一致,且根据 offset 判断数据还在repl_backlog_buffer中,则说明可以进行增量同步。

于是就去 repl_backlog_buffer 查找对应 offset 之后的命令数据,写入到 replication buffer 中,最终将其发送给 slave 节点。slave 节点收到指令之后执行对应的命令,一次增量同步的过程就完成了。

如果根据 offset 判断数据已经被覆盖了,此时只能触发全量同步!

因此可以调整 repl_backlog_buffer 大小,尽量避免出现全量同步。

13,Redis 集群的实现原理是什么?

当单机 Redis 缓存的数据量太大,请求量也高,这个时候可以采用 Redis 集群(Redis Cluster)方案。

Redis 集群会将数据分片存储到多台 Redis 上,多个 Redis 实例都可进行读写操作。

集群内每个节点都会保存集群的完整拓扑信息,包括每个节点的 ID、IP 地址、端口、负责的哈希槽范围等,它们直接通过 Gossip 协议保持通信,会周期性地发送 PING 和 PONG 消息,交换集群信息,使得集群信息得以同步。

简单点说,集群就是通过多台机器分担单台机器上的压力,如下图所示:

Snipaste_2024-05-19_01-50-05.jpg

从图中可以看到,每个分片内部还是有主从的结构,这个目的是为了提高集群的可用性。

Redis 集群分片原理

Redis 集群会将数据分散到 16384 (2 ^ 14)个哈希槽中,集群中的每个节点负责一定范围的哈希槽,如下图所示:

image.png

每个节点会拥有一部分的槽位,然后对应的键值会根据其本身的 key,映射到一个哈希槽中,其主要流程如下:

根据键值的 key,按照 CRC 16 算法计算一个 16 bit 的值,然后将 16 bit 的值对 16384 进行取余运算,最后得到一个对应的哈希槽编号。根据每个节点分配的哈希槽区间,对应编号的数据落在对应的区间上,就能找到对应的分片实例。

为了方便大家理解,我这里画一个对应的关系图,这里以我们上面的三主多从节点为例:

image.png

这里还有一点需要强调下,redis 客户端可以访问集群中任意一台实例,正常情况下这个实例包含这个数据。

但如果槽被转移了,客户端还未来得及更新槽的信息,当前实例没有这个数据,则返回 MOVED 响应给客户端,将其重定向到对应的实例(因 Gossip 集群内每个节点都会保存集群的完整拓扑信息)

14,HTTP 1.0 和 2.0 有什么区别?

回答重点

HTTP/1.0 版本主要增加以下几点:

增加了 HEAD、POST 等新方法。增加了响应状态码。引入了头部,即请求头和响应头。在请求中加入了 HTTP 版本号。引入了 Content-Type ,使得传输的数据不再限于文本。

HTTP/1.1 版本主要增加以下几点:

新增了连接管理即 keepalive ,允许持久连接。支持 pipeline,无需等待前面的请求响应,即可发送第二次请求。允许响应数据分块(chunked),即响应的时候不标明Content-Length,客户端就无法断开连接,直到收到服务端的 EOF ,利于传输大文件。新增缓存的控制和管理。加入了 Host 头,用在你一台机子部署了多个主机,然后多个域名解析又是同一个 IP,此时加入了 Host 头就可以判断你到底是要访问哪个主机。

HTTP/2 版本主要增加以下几点:

是二进制协议,不再是纯文本。支持一个 TCP 连接发起多请求,移除了 pipeline。利用 HPACK 压缩头部,减少数据传输量。允许服务端主动推送数据。

15,让你设计一个 RPC 框架,怎么设计?

RPC 框架基础的核心点如下:

1)动态代理(屏蔽底层调用细节)
2)序列化(网络数据传输需要扁平的数据)
3)协议(规定协议,才能识别数据)
4)网络传输(I/O模型相关内容,一般用 Netty 作为底层通信框架即可)

这属于 RPC 框架的基础,生产级别的框架还需要注册中心作为服务的发现,且还需提供路由分组、负载均衡、异常重试、限流熔断等其他功能。

下面我们来深入剖析一下 RPC。

RPC 全称是 Remote Procedure Call ,即远程过程调用,其对应的是我们的本地调用。

远程其实指的就是需要网络通信,可以理解为调用远程机器上的方法。

那可能有人说:我用 HTTP 调用不就是远程调用了,那不也叫 RPC 了?

不是的,RPC 的目的是:让我们调用远程方法像调用本地方法一样无差别。

来看下代码就很清晰,比如本来没有拆分服务都是本地调用的时候方法是这样写的:

public String getSth(String str) {     return yesService.get(str);}

如果 yesSerivce 被拆分出去,此时需要远程调用了,如果用 HTTP 方式,可能就是:

public String getSth(String str) {    RequestParam param = new RequestParam();    ......    return HttpClient.get(url, param,.....);}

此时需要关心远程服务的地址,还需要组装请求等等,而如果采用 RPC 调用那就是:

    public String getSth(String str) {        // 看起来和之前调用没差?哈哈没唬你,        // 具体的实现已经搬到另一个服务上了,这里只有接口。        // 看完下面就知道了。         return yesService.get(str);      }

所以说 RPC 其实就是用来屏蔽远程调用网络相关的细节,使得远程调用和本地调用使用一致,让开发的效率更高。

在了解了 RPC 的作用之后,我们来看看 RPC 调用需要经历哪些步骤。

RPC 调用基本流程

按上面的例子来说,yesService 服务实现被移到了远程服务上,本地没有具体的实现只有一个接口。

那这时候我们需要调用 yesService.get(str) ,该怎么办呢?

我们所要做的就是把传入的参数和调用的接口全限定名通过网络通信告知到远程服务那里。

然后远程服务接收到参数和接口全限定名就能选中具体的实现并进行调用。

业务处理完之后再通过网络返回结果。

上面的操作这些就是由yesService.get(str) 触发的。

不过我们知道 yesService 就是一个接口,没有实现的,所以这些操作是怎么来的?

是通过动态代理来的。

RPC 会给接口生成一个代理类,所以我们调用这个接口实际调用的是动态生成的代理类,由代理类来触发远程调用,这样我们调用远程接口就无感知了。

动态代理想必大家都比较熟悉,最常见的就是 Spring 的 AOP 了,涉及的有 JDK 动态代理和 cglib。

在 Dubbo 中用的是 Javassist,至于为什么用这个其实梁飞大佬已经写了博客说明了。

他当时对比了 JDK 自带的、ASM、CGLIB(基于ASM包装)、Javassist。

经过测试最终选用了 Javassist。

梁飞:最终决定使用JAVAASSIST的字节码生成代理方式。
虽然ASM稍快,但并没有快一个数量级,而JAVAASSIST的字节码生成方式比ASM方便,JAVAASSIST只需用字符串拼接出Java源码,便可生成相应字节码,而ASM需要手工写字节码。

可以看到选择一个框架的时候性能是一方面,易用性也很关键。

说回 RPC 。

现在我们知道动态代理屏蔽了 RPC 调用的细节,使得用户无感知的调用远程服务,那调用的细节有哪些呢?

序列化

像我们的请求参数都是对象,有时候是定义的 DTO ,有时候是 Map ,这些对象是无法直接在网络中传输的。

你可以理解为对象是“立体”的,而网络传输的数据是“扁平”的,最终需要转化成“扁平”的二进制数据在网络中传输。

你想想,各对象分配在内存不同位置,各种引用,这看起来是不是有种立体的感觉?最终都是要变成一段01组成的数字传输给对方,这种就01组成的数字看起来是不是很“扁平”?

把对象转化成二进制数据的过程称为序列化,把二进制数据转化成对象的过程称为反序列化。

当然如何选择序列化格式也很重要。

比如采用二进制的序列化格式数据更加紧凑,采用 JSON 等文本型序列化格式可读性更佳,排查问题比较方便。

还有很多序列化选择,一般需要综合考虑通用性、性能、可读性和兼容性

具体本文就不分析了,之后再专门写一篇分析各种序列化协议的。

RPC 协议

刚才也提到了只有二进制数据才能在网络中传输,那一堆二进制在底层看来是连起来的,它可不会管你哪些数据是哪个请求的,那接收方得知道呀,不然就不能顺利的把二进制数据还原成对应的一个个请求了。

于是就需要定义一个协议,来约定一些规范,制定一些边界使得二进制数据可以被还原。

比如下面一串数字按照不同位数来识别得到的结果是不同的。

所以协议其实就定义了到底如何构造和解析这些二进制数据。

我们的参数肯定比上面的复杂,因为参数值长度是不定的,而且协议常常伴随着升级而扩展,毕竟有时候需要加一些新特性,那么协议就得变了。

一般 RPC 协议都是采用协议头+协议体的方式。

协议头放一些元数据,包括:魔法位、协议的版本、消息的类型、序列化方式、整体长度、头长度、扩展位等。

协议体就是放请求的数据了。

通过魔法位可以得知这是不是咱们约定的协议,比如魔法位固定叫 233 ,一看我们就知道这是 233 协议。

然后协议的版本是为了之后协议的升级。

从整体长度和头长度我们就能知道这个请求到底有多少位,前面多少位是头,剩下的都是协议体,这样就能识别出来,扩展位就是留着日后扩展备用。

贴一下 Dubbo 协议:

可以看到有 Magic 位,请求 ID, 数据长度等等。

网络传输

组装好数据就等着发送了,这时候就涉及网络传输了。

网络通信那就离不开网络 IO 模型了。

网络 IO 分为这四种模型,具体以后单独写文章分析,这篇就不展开了。

一般而言我们用的都是 IO 多路复用,因为大部分 RPC 调用场景都是高并发调用,IO 复用可以利用较少的线程 hold 住很多请求。

一般 RPC 框架会使用已经造好的轮子来作为底层通信框架。

例如 Java 语言的都会用 Netty ,人家已经封装的很好了,也做了很多优化,拿来即用,便捷高效。

小结

RPC 通信的基础流程已经讲完了,回顾下之前的图:

响应返回就没画了,反正就是倒着来。

我再用一段话来总结一下:

服务调用方,面向接口编程,利用动态代理屏蔽底层调用细节将请求参数、接口等数据组合起来并通过序列化转化为二进制数据,再通过 RPC 协议的封装利用网络传输到服务提供方。

服务提供方根据约定的协议解析出请求数据,然后反序列化得到参数,找到具体调用的接口,然后执行具体实现,再返回结果。

这里面还有很多细节。

比如请求都是异步的,所以每个请求会有唯一 ID,返回结果会带上对应的 ID, 这样调用方就能通过 ID 找到对应的请求塞入相应的结果。

有人会问为什么要异步,那是为了提高吞吐。

当然还有很多细节,会在之后剖析 Dubbo 的时候提到,结合实际中间件体会才会更深。


除了上面这15道热门面试题,Java、go、vue 等等秋招热题也已经准备好,鸭鸭还有 5000 多道面试题期待大家一起来刷!

image-20240828200424088

从 Java 基础到 Spring ,SpringBoot,微服务, Kafka ,分布式,Redis ,分布式事务,设计模式,算法,数据结构,MySQL ……还有 408 考研相关面试题,都可以在面试鸭找到详尽题解。我们花了大量时间和人力在题解质量上,希望能让大家的就业求职更为顺利~

面试鸭网页和小程序双端可用,题库持续更新中,和鸭鸭一起快乐卷起来吧~


点击全文阅读


本文链接:http://zhangshiyu.com/post/163127.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1