Node.js 有難度的面試題,你能答對(duì)幾個(gè)?
點(diǎn)擊上方 前端瓶子君,關(guān)注公眾號(hào)
回復(fù)算法,加入前端編程面試算法每日一題群

看到一些 Node.js 面試相關(guān)的文章分享下,也歡迎推薦,正文從下面開(kāi)始~
1、Node 模塊機(jī)制
1.1 請(qǐng)介紹一下 node 里的模塊是什么
Node 中,每個(gè)文件模塊都是一個(gè)對(duì)象,它的定義如下:
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
this.filename = null;
this.loaded = false;
this.children = [];
}
module.exports = Module;
var module = new Module(filename, parent);
所有的模塊都是 Module 的實(shí)例。可以看到,當(dāng)前模塊(module.js)也是 Module 的一個(gè)實(shí)例。
1.2 請(qǐng)介紹一下 require 的模塊加載機(jī)制
這道題基本上就可以了解到面試者對(duì) Node 模塊機(jī)制的了解程度 基本上面試提到
-
1、先計(jì)算模塊路徑 -
2、如果模塊在緩存里面,取出緩存 -
3、加載模塊 -
4、的輸出模塊的 exports 屬性即可
// require 其實(shí)內(nèi)部調(diào)用 Module._load 方法
Module._load = function(request, parent, isMain) {
// 計(jì)算絕對(duì)路徑
var filename = Module._resolveFilename(request, parent);
// 第一步:如果有緩存,取出緩存
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
// 第二步:是否為內(nèi)置模塊
if (NativeModule.exists(filename)) {
return NativeModule.require(filename);
}
/********************************這里注意了**************************/
// 第三步:生成模塊實(shí)例,存入緩存
// 這里的Module就是我們上面的1.1定義的Module
var module = new Module(filename, parent);
Module._cache[filename] = module;
/********************************這里注意了**************************/
// 第四步:加載模塊
// 下面的module.load實(shí)際上是Module原型上有一個(gè)方法叫Module.prototype.load
try {
module.load(filename);
hadException = false;
} finally {
if (hadException) {
delete Module._cache[filename];
}
}
// 第五步:輸出模塊的exports屬性
return module.exports;
};
接著上一題繼續(xù)發(fā)問(wèn)
1.3 加載模塊時(shí),為什么每個(gè)模塊都有__dirname,__filename 屬性呢,new Module 的時(shí)候我們看到 1.1 部分沒(méi)有這兩個(gè)屬性的,那么這兩個(gè)屬性是從哪里來(lái)的
// 上面(1.2部分)的第四步module.load(filename)
// 這一步,module模塊相當(dāng)于被包裝了,包裝形式如下
// 加載js模塊,相當(dāng)于下面的代碼(加載node模塊和json模塊邏輯不一樣)
(function (exports, require, module, __filename, __dirname) {
// 模塊源碼
// 假如模塊代碼如下
var math = require('math');
exports.area = function(radius){
return Math.PI * radius * radius
}
});
也就是說(shuō),每個(gè) module 里面都會(huì)傳入__filename, __dirname 參數(shù),這兩個(gè)參數(shù)并不是 module 本身就有的,是外界傳入的
1.4 我們知道 node 導(dǎo)出模塊有兩種方式,一種是 exports.xxx=xxx 和 Module.exports={}有什么區(qū)別嗎
-
exports 其實(shí)就是 module.exports -
其實(shí) 1.3 問(wèn)題的代碼已經(jīng)說(shuō)明問(wèn)題了,接著我引用廖雪峰大神的講解,希望能講的更清楚
module.exports vs exports
很多時(shí)候,你會(huì)看到,在Node環(huán)境中,有兩種方法可以在一個(gè)模塊中輸出變量:
方法一:對(duì)module.exports賦值:
// hello.js
function hello() {
console.log('Hello, world!');
}
function greet(name) {
console.log('Hello, ' + name + '!');
}
module.exports = {
hello: hello,
greet: greet
};
方法二:直接使用exports:
// hello.js
function hello() {
console.log('Hello, world!');
}
function greet(name) {
console.log('Hello, ' + name + '!');
}
function hello() {
console.log('Hello, world!');
}
exports.hello = hello;
exports.greet = greet;
但是你不可以直接對(duì)exports賦值:
// 代碼可以執(zhí)行,但是模塊并沒(méi)有輸出任何變量:
exports = {
hello: hello,
greet: greet
};
如果你對(duì)上面的寫(xiě)法感到十分困惑,不要著急,我們來(lái)分析Node的加載機(jī)制:
首先,Node會(huì)把整個(gè)待加載的hello.js文件放入一個(gè)包裝函數(shù)load中執(zhí)行。在執(zhí)行這個(gè)load()函數(shù)前,Node準(zhǔn)備好了module變量:
var module = {
id: 'hello',
exports: {}
};
load()函數(shù)最終返回module.exports:
var load = function (exports, module) {
// hello.js的文件內(nèi)容
...
// load函數(shù)返回:
return module.exports;
};
var exportes = load(module.exports, module);
也就是說(shuō),默認(rèn)情況下,Node準(zhǔn)備的exports變量和module.exports變量實(shí)際上是同一個(gè)變量,并且初始化為空對(duì)象{},于是,我們可以寫(xiě):
exports.foo = function () { return 'foo'; };
exports.bar = function () { return 'bar'; };
也可以寫(xiě):
module.exports.foo = function () { return 'foo'; };
module.exports.bar = function () { return 'bar'; };
換句話說(shuō),Node默認(rèn)給你準(zhǔn)備了一個(gè)空對(duì)象{},這樣你可以直接往里面加?xùn)|西。
但是,如果我們要輸出的是一個(gè)函數(shù)或數(shù)組,那么,只能給module.exports賦值:
module.exports = function () { return 'foo'; };
給exports賦值是無(wú)效的,因?yàn)橘x值后,module.exports仍然是空對(duì)象{}。
結(jié)論
如果要輸出一個(gè)鍵值對(duì)象{},可以利用exports這個(gè)已存在的空對(duì)象{},并繼續(xù)在上面添加新的鍵值;
如果要輸出一個(gè)函數(shù)或數(shù)組,必須直接對(duì)module.exports對(duì)象賦值。
所以我們可以得出結(jié)論:直接對(duì)module.exports賦值,可以應(yīng)對(duì)任何情況:
module.exports = {
foo: function () { return 'foo'; }
};
或者:
module.exports = function () { return 'foo'; };
最終,我們強(qiáng)烈建議使用module.exports = xxx的方式來(lái)輸出模塊變量,這樣,你只需要記憶一種方法。
2、Node 的異步 I/O
本章的答題思路大多借鑒于樸靈大神的《深入淺出的 NodeJS》
2.1 請(qǐng)介紹一下 Node 事件循環(huán)的流程
-
在進(jìn)程啟動(dòng)時(shí),Node 便會(huì)創(chuàng)建一個(gè)類(lèi)似于 while(true)的循環(huán),每執(zhí)行一次循環(huán)體的過(guò)程我們成為 Tick。
-
每個(gè) Tick 的過(guò)程就是查看是否有事件待處理。如果有就取出事件及其相關(guān)的回調(diào)函數(shù)。然后進(jìn)入下一個(gè)循環(huán),如果不再有事件處理,就退出進(jìn)程。
2.2 在每個(gè) tick 的過(guò)程中,如何判斷是否有事件需要處理呢?
-
每個(gè)事件循環(huán)中有一個(gè)或者多個(gè)觀察者,而判斷是否有事件需要處理的過(guò)程就是向這些觀察者詢問(wèn)是否有要處理的事件。
-
在 Node 中,事件主要來(lái)源于網(wǎng)絡(luò)請(qǐng)求、文件的 I/O 等,這些事件對(duì)應(yīng)的觀察者有文件 I/O 觀察者,網(wǎng)絡(luò) I/O 的觀察者。
-
事件循環(huán)是一個(gè)典型的生產(chǎn)者/消費(fèi)者模型。異步 I/O,網(wǎng)絡(luò)請(qǐng)求等則是事件的生產(chǎn)者,源源不斷為 Node 提供不同類(lèi)型的事件,這些事件被傳遞到對(duì)應(yīng)的觀察者那里,事件循環(huán)則從觀察者那里取出事件并處理。
-
在 windows 下,這個(gè)循環(huán)基于 IOCP 創(chuàng)建,在*nix 下則基于多線程創(chuàng)建
2.3 請(qǐng)描述一下整個(gè)異步 I/O 的流程
3、V8 的垃圾回收機(jī)制
3.1 如何查看 V8 的內(nèi)存使用情況
使用 process.memoryUsage(),返回如下
{
rss: 4935680,
heapTotal: 1826816,
heapUsed: 650472,
external: 49879
}
heapTotal 和 heapUsed 代表 V8 的內(nèi)存使用情況。external 代表 V8 管理的,綁定到 Javascript 的 C++對(duì)象的內(nèi)存使用情況。rss, 駐留集大小, 是給這個(gè)進(jìn)程分配了多少物理內(nèi)存(占總分配內(nèi)存的一部分) 這些物理內(nèi)存中包含堆,棧,和代碼段。
3.2 V8 的內(nèi)存限制是多少,為什么 V8 這樣設(shè)計(jì)
64 位系統(tǒng)下是 1.4GB, 32 位系統(tǒng)下是 0.7GB。因?yàn)?1.5GB 的垃圾回收堆內(nèi)存,V8 需要花費(fèi) 50 毫秒以上,做一次非增量式的垃圾回收甚至要 1 秒以上。這是垃圾回收中引起 Javascript 線程暫停執(zhí)行的事件,在這樣的花銷(xiāo)下,應(yīng)用的性能和影響力都會(huì)直線下降。
3.3 V8 的內(nèi)存分代和回收算法請(qǐng)簡(jiǎn)單講一講
在 V8 中,主要將內(nèi)存分為新生代和老生代兩代。新生代中的對(duì)象存活時(shí)間較短的對(duì)象,老生代中的對(duì)象存活時(shí)間較長(zhǎng),或常駐內(nèi)存的對(duì)象。
3.3.1 新生代
新生代中的對(duì)象主要通過(guò) Scavenge 算法進(jìn)行垃圾回收。這是一種采用復(fù)制的方式實(shí)現(xiàn)的垃圾回收算法。它將堆內(nèi)存一份為二,每一部分空間成為 semispace。在這兩個(gè) semispace 空間中,只有一個(gè)處于使用中,另一個(gè)處于閑置狀態(tài)。處于使用狀態(tài)的 semispace 空間稱(chēng)為 From 空間,處于閑置狀態(tài)的空間稱(chēng)為 To 空間。
-
當(dāng)開(kāi)始垃圾回收的時(shí)候,會(huì)檢查 From 空間中的存活對(duì)象,這些存活對(duì)象將被復(fù)制到 To 空間中,而非存活對(duì)象占用的空間將會(huì)被釋放。完成復(fù)制后,F(xiàn)rom 空間和 To 空間發(fā)生角色對(duì)換。
-
應(yīng)為新生代中對(duì)象的生命周期比較短,就比較適合這個(gè)算法。
-
當(dāng)一個(gè)對(duì)象經(jīng)過(guò)多次復(fù)制依然存活,它將會(huì)被認(rèn)為是生命周期較長(zhǎng)的對(duì)象。這種新生代中生命周期較長(zhǎng)的對(duì)象隨后會(huì)被移到老生代中。
3.3.2 老生代
老生代主要采取的是標(biāo)記清除的垃圾回收算法。與 Scavenge 復(fù)制活著的對(duì)象不同,標(biāo)記清除算法在標(biāo)記階段遍歷堆中的所有對(duì)象,并標(biāo)記活著的對(duì)象,只清理死亡對(duì)象。活對(duì)象在新生代中只占叫小部分,死對(duì)象在老生代中只占較小部分,這是為什么采用標(biāo)記清除算法的原因。
3.3.3 標(biāo)記清楚算法的問(wèn)題
主要問(wèn)題是每一次進(jìn)行標(biāo)記清除回收后,內(nèi)存空間會(huì)出現(xiàn)不連續(xù)的狀態(tài)
-
這種內(nèi)存碎片會(huì)對(duì)后續(xù)內(nèi)存分配造成問(wèn)題,很可能出現(xiàn)需要分配一個(gè)大對(duì)象的情況,這時(shí)所有的碎片空間都無(wú)法完成此次分配,就會(huì)提前觸發(fā)垃圾回收,而這次回收是不必要的。 -
為了解決碎片問(wèn)題,標(biāo)記整理被提出來(lái)。就是在對(duì)象被標(biāo)記死亡后,在整理的過(guò)程中,將活著的對(duì)象往一端移動(dòng),移動(dòng)完成后,直接清理掉邊界外的內(nèi)存。
3.3.4 哪些情況會(huì)造成 V8 無(wú)法立即回收內(nèi)存
閉包和全局變量
3.3.5 請(qǐng)談一下內(nèi)存泄漏是什么,以及常見(jiàn)內(nèi)存泄漏的原因,和排查的方法
什么是內(nèi)存泄漏
-
內(nèi)存泄漏(Memory Leak)指由于疏忽或錯(cuò)誤造成程序未能釋放已經(jīng)不再使用的內(nèi)存的情況。 -
如果內(nèi)存泄漏的位置比較關(guān)鍵,那么隨著處理的進(jìn)行可能持有越來(lái)越多的無(wú)用內(nèi)存,這些無(wú)用的內(nèi)存變多會(huì)引起服務(wù)器響應(yīng)速度變慢。 -
嚴(yán)重的情況下導(dǎo)致內(nèi)存達(dá)到某個(gè)極限(可能是進(jìn)程的上限,如 v8 的上限;也可能是系統(tǒng)可提供的內(nèi)存上限)會(huì)使得應(yīng)用程序崩潰。常見(jiàn)內(nèi)存泄漏的原因 內(nèi)存泄漏的幾種情況:
一、全局變量
a = 10;
//未聲明對(duì)象。
global.b = 11;
//全局變量引用
這種比較簡(jiǎn)單的原因,全局變量直接掛在 root 對(duì)象上,不會(huì)被清除掉。
二、閉包
function out() {
const bigData = new Buffer(100);
inner = function () {
}
}
閉包會(huì)引用到父級(jí)函數(shù)中的變量,如果閉包未釋放,就會(huì)導(dǎo)致內(nèi)存泄漏。上面例子是 inner 直接掛在了 root 上,那么每次執(zhí)行 out 函數(shù)所產(chǎn)生的 bigData 都不會(huì)釋放,從而導(dǎo)致內(nèi)存泄漏。需要注意的是,這里舉得例子只是簡(jiǎn)單的將引用掛在全局對(duì)象上,實(shí)際的業(yè)務(wù)情況可能是掛在某個(gè)可以從 root 追溯到的對(duì)象上導(dǎo)致的。三、事件監(jiān)聽(tīng)Node.js 的事件監(jiān)聽(tīng)也可能出現(xiàn)的內(nèi)存泄漏。例如對(duì)同一個(gè)事件重復(fù)監(jiān)聽(tīng),忘記移除(removeListener),將造成內(nèi)存泄漏。這種情況很容易在復(fù)用對(duì)象上添加事件時(shí)出現(xiàn),所以事件重復(fù)監(jiān)聽(tīng)可能收到如下警告:
emitter.setMaxListeners() to increase limit
例如,Node.js 中 Agent 的 keepAlive 為 true 時(shí),可能造成的內(nèi)存泄漏。當(dāng) Agent keepAlive 為 true 的時(shí)候,將會(huì)復(fù)用之前使用過(guò)的 socket,如果在 socket 上添加事件監(jiān)聽(tīng),忘記清除的話,因?yàn)?socket 的復(fù)用,將導(dǎo)致事件重復(fù)監(jiān)聽(tīng)從而產(chǎn)生內(nèi)存泄漏。原理上與前一個(gè)添加事件監(jiān)聽(tīng)的時(shí)候忘了清除是一樣的。在使用 Node.js 的 http 模塊時(shí),不通過(guò) keepAlive 復(fù)用是沒(méi)有問(wèn)題的,復(fù)用了以后就會(huì)可能產(chǎn)生內(nèi)存泄漏。所以,你需要了解添加事件監(jiān)聽(tīng)的對(duì)象的生命周期,并注意自行移除。排查方法想要定位內(nèi)存泄漏,通常會(huì)有兩種情況:
-
對(duì)于只要正常使用就可以重現(xiàn)的內(nèi)存泄漏,這是很簡(jiǎn)單的情況只要在測(cè)試環(huán)境模擬就可以排查了。
-
對(duì)于偶然的內(nèi)存泄漏,一般會(huì)與特殊的輸入有關(guān)系。想穩(wěn)定重現(xiàn)這種輸入是很耗時(shí)的過(guò)程。如果不能通過(guò)代碼的日志定位到這個(gè)特殊的輸入,那么推薦去生產(chǎn)環(huán)境打印內(nèi)存快照了。
-
需要注意的是,打印內(nèi)存快照是很耗 CPU 的操作,可能會(huì)對(duì)線上業(yè)務(wù)造成影響。快照工具推薦使用 heapdump 用來(lái)保存內(nèi)存快照,使用 devtool 來(lái)查看內(nèi)存快照。
-
使用 heapdump 保存內(nèi)存快照時(shí),只會(huì)有 Node.js 環(huán)境中的對(duì)象,不會(huì)受到干擾(如果使用 node-inspector 的話,快照中會(huì)有前端的變量干擾)。
-
PS:安裝 heapdump 在某些 Node.js 版本上可能出錯(cuò),建議使用 npm install heapdump -target=Node.js 版本來(lái)安裝。
4、Buffer 模塊
4.1 新建 Buffer 會(huì)占用 V8 分配的內(nèi)存嗎
不會(huì),Buffer 屬于堆外內(nèi)存,不是 V8 分配的。
4.2 Buffer.alloc 和 Buffer.allocUnsafe 的區(qū)別
Buffer.allocUnsafe 創(chuàng)建的 Buffer 實(shí)例的底層內(nèi)存是未初始化的。新創(chuàng)建的 Buffer 的內(nèi)容是未知的,可能包含敏感數(shù)據(jù)。使用 Buffer.alloc() 可以創(chuàng)建以零初始化的 Buffer 實(shí)例。
4.3 Buffer 的內(nèi)存分配機(jī)制
為了高效的使用申請(qǐng)來(lái)的內(nèi)存,Node 采用了 slab 分配機(jī)制。slab 是一種動(dòng)態(tài)的內(nèi)存管理機(jī)制。Node 以 8kb 為界限來(lái)來(lái)區(qū)分 Buffer 為大對(duì)象還是小對(duì)象,如果是小于 8kb 就是小 Buffer,大于 8kb 就是大 Buffer。例如第一次分配一個(gè) 1024 字節(jié)的 Buffer,Buffer.alloc(1024),那么這次分配就會(huì)用到一個(gè) slab,接著如果繼續(xù) Buffer.alloc(1024),那么上一次用的 slab 的空間還沒(méi)有用完,因?yàn)榭偣彩?8kb,1024+1024 = 2048 個(gè)字節(jié),沒(méi)有 8kb,所以就繼續(xù)用這個(gè) slab 給 Buffer 分配空間。如果超過(guò) 8kb,那么直接用 C++底層地宮的 SlowBuffer 來(lái)給 Buffer 對(duì)象提供空間。
4.4 Buffer 亂碼問(wèn)題
例如一個(gè)份文件 test.md 里的內(nèi)容如下:
床前明月光,疑是地上霜,舉頭望明月,低頭思故鄉(xiāng)
我們這樣讀取就會(huì)出現(xiàn)亂碼:
var rs = require('fs').createReadStream('test.md', {highWaterMark: 11});
// 床前明???光,疑???地上霜,舉頭???明月,???頭思故鄉(xiāng)
一般情況下,只需要設(shè)置 rs.setEncoding('utf8')即可解決亂碼問(wèn)題
5、webSocket
5.1 webSocket 與傳統(tǒng)的 http 有什么優(yōu)勢(shì)
-
客戶端與服務(wù)器只需要一個(gè) TCP 連接,比 http 長(zhǎng)輪詢使用更少的連接 -
webSocket 服務(wù)端可以推送數(shù)據(jù)到客戶端 -
更輕量的協(xié)議頭,減少數(shù)據(jù)傳輸量
5.2 webSocket 協(xié)議升級(jí)時(shí)什么,能簡(jiǎn)述一下嗎?
首先,WebSocket 連接必須由瀏覽器發(fā)起,因?yàn)檎?qǐng)求協(xié)議是一個(gè)標(biāo)準(zhǔn)的 HTTP 請(qǐng)求,格式如下:
GET ws://localhost:3000/ws/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Origin: http://localhost:3000
Sec-WebSocket-Key: client-random-string
Sec-WebSocket-Version: 13
該請(qǐng)求和普通的 HTTP 請(qǐng)求有幾點(diǎn)不同:
-
GET 請(qǐng)求的地址不是類(lèi)似/path/,而是以 ws://開(kāi)頭的地址; -
請(qǐng)求頭 Upgrade: websocket 和 Connection: Upgrade 表示這個(gè)連接將要被轉(zhuǎn)換為 WebSocket 連接; -
Sec-WebSocket-Key 是用于標(biāo)識(shí)這個(gè)連接,并非用于加密數(shù)據(jù); -
Sec-WebSocket-Version 指定了 WebSocket 的協(xié)議版本。
隨后,服務(wù)器如果接受該請(qǐng)求,就會(huì)返回如下響應(yīng):
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: server-random-string
該響應(yīng)代碼 101 表示本次連接的 HTTP 協(xié)議即將被更改,更改后的協(xié)議就是 Upgrade: websocket 指定的 WebSocket 協(xié)議。
6、https
6.1 https 用哪些端口進(jìn)行通信,這些端口分別有什么用
-
443 端口用來(lái)驗(yàn)證服務(wù)器端和客戶端的身份,比如驗(yàn)證證書(shū)的合法性 -
80 端口用來(lái)傳輸數(shù)據(jù)(在驗(yàn)證身份合法的情況下,用來(lái)數(shù)據(jù)傳輸)
6.2 身份驗(yàn)證過(guò)程中會(huì)涉及到密鑰, 對(duì)稱(chēng)加密,非對(duì)稱(chēng)加密,摘要的概念,請(qǐng)解釋一下
-
密鑰:密鑰是一種參數(shù),它是在明文轉(zhuǎn)換為密文或?qū)⒚芪霓D(zhuǎn)換為明文的算法中輸入的參數(shù)。密鑰分為對(duì)稱(chēng)密鑰與非對(duì)稱(chēng)密鑰,分別應(yīng)用在對(duì)稱(chēng)加密和非對(duì)稱(chēng)加密上。
-
對(duì)稱(chēng)加密:對(duì)稱(chēng)加密又叫做私鑰加密,即信息的發(fā)送方和接收方使用同一個(gè)密鑰去加密和解密數(shù)據(jù)。對(duì)稱(chēng)加密的特點(diǎn)是算法公開(kāi)、加密和解密速度快,適合于對(duì)大數(shù)據(jù)量進(jìn)行加密,常見(jiàn)的對(duì)稱(chēng)加密算法有 DES、3DES、TDEA、Blowfish、RC5 和 IDEA。
-
非對(duì)稱(chēng)加密:非對(duì)稱(chēng)加密也叫做公鑰加密。非對(duì)稱(chēng)加密與對(duì)稱(chēng)加密相比,其安全性更好。對(duì)稱(chēng)加密的通信雙方使用相同的密鑰,如果一方的密鑰遭泄露,那么整個(gè)通信就會(huì)被破解。而非對(duì)稱(chēng)加密使用一對(duì)密鑰,即公鑰和私鑰,且二者成對(duì)出現(xiàn)。私鑰被自己保存,不能對(duì)外泄露。公鑰指的是公共的密鑰,任何人都可以獲得該密鑰。用公鑰或私鑰中的任何一個(gè)進(jìn)行加密,用另一個(gè)進(jìn)行解密。
-
摘要:摘要算法又稱(chēng)哈希/散列算法。它通過(guò)一個(gè)函數(shù),把任意長(zhǎng)度的數(shù)據(jù)轉(zhuǎn)換為一個(gè)長(zhǎng)度固定的數(shù)據(jù)串(通常用 16 進(jìn)制的字符串表示)。算法不可逆。
6.3 為什么需要 CA 機(jī)構(gòu)對(duì)證書(shū)簽名
如果不簽名會(huì)存在中間人攻擊的風(fēng)險(xiǎn),簽名之后保證了證書(shū)里的信息,比如公鑰、服務(wù)器信息、企業(yè)信息等不被篡改,能夠驗(yàn)證客戶端和服務(wù)器端的“合法性”。
6.4 https 驗(yàn)證身份也就是 TSL/SSL 身份驗(yàn)證的過(guò)程
簡(jiǎn)要圖解如下
7、進(jìn)程通信
7.1 請(qǐng)簡(jiǎn)述一下 node 的多進(jìn)程架構(gòu)
面對(duì) node 單線程對(duì)多核 CPU 使用不足的情況,Node 提供了 child_process 模塊,來(lái)實(shí)現(xiàn)進(jìn)程的復(fù)制,node 的多進(jìn)程架構(gòu)是主從模式,如下所示:
var fork = require('child_process').fork;
var cpus = require('os').cpus();
for(var i = 0; i < cpus.length; i++){
fork('./worker.js');
}
在 linux 中,我們通過(guò) ps aux | grep worker.js 查看進(jìn)程
這就是著名的主從模式,Master-Worker
7.2 請(qǐng)問(wèn)創(chuàng)建子進(jìn)程的方法有哪些,簡(jiǎn)單說(shuō)一下它們的區(qū)別
創(chuàng)建子進(jìn)程的方法大致有:
-
spawn():?jiǎn)?dòng)一個(gè)子進(jìn)程來(lái)執(zhí)行命令 -
exec(): 啟動(dòng)一個(gè)子進(jìn)程來(lái)執(zhí)行命令,與 spawn()不同的是其接口不同,它有一個(gè)回調(diào)函數(shù)獲知子進(jìn)程的狀況 -
execFlie(): 啟動(dòng)一個(gè)子進(jìn)程來(lái)執(zhí)行可執(zhí)行文件 -
fork(): 與 spawn()類(lèi)似,不同電在于它創(chuàng)建 Node 子進(jìn)程需要執(zhí)行 js 文件 -
spawn()與 exec()、execFile()不同的是,后兩者創(chuàng)建時(shí)可以指定 timeout 屬性設(shè)置超時(shí)時(shí)間,一旦創(chuàng)建的進(jìn)程超過(guò)設(shè)定的時(shí)間就會(huì)被殺死 -
exec()與 execFile()不同的是,exec()適合執(zhí)行已有命令,execFile()適合執(zhí)行文件。
7.3 請(qǐng)問(wèn)你知道 spawn 在創(chuàng)建子進(jìn)程的時(shí)候,第三個(gè)參數(shù)有一個(gè) stdio 選項(xiàng)嗎,這個(gè)選項(xiàng)的作用是什么,默認(rèn)的值是什么。
-
選項(xiàng)用于配置在父進(jìn)程和子進(jìn)程之間建立的管道。 -
默認(rèn)情況下,子進(jìn)程的 stdin、 stdout 和 stderr 會(huì)被重定向到 ChildProcess 對(duì)象上相應(yīng)的 subprocess.stdin、subprocess.stdout 和 subprocess.stderr 流。 -
這相當(dāng)于將 options.stdio 設(shè)置為 ['pipe', 'pipe', 'pipe']。
7.4 請(qǐng)問(wèn)實(shí)現(xiàn)一個(gè) node 子進(jìn)程被殺死,然后自動(dòng)重啟代碼的思路
-
在創(chuàng)建子進(jìn)程的時(shí)候就讓子進(jìn)程監(jiān)聽(tīng) exit 事件,如果被殺死就重新 fork 一下
var createWorker = function(){
var worker = fork(__dirname + 'worker.js')
worker.on('exit', function(){
console.log('Worker' + worker.pid + 'exited');
// 如果退出就創(chuàng)建新的worker
createWorker()
})
}
7.5 在 7.4 的基礎(chǔ)上,實(shí)現(xiàn)限量重啟,比如我最多讓其在 1 分鐘內(nèi)重啟 5 次,超過(guò)了就報(bào)警給運(yùn)維
-
思路大概是在創(chuàng)建 worker 的時(shí)候,就判斷創(chuàng)建的這個(gè) worker 是否在 1 分鐘內(nèi)重啟次數(shù)超過(guò) 5 次 -
所以每一次創(chuàng)建 worker 的時(shí)候都要記錄這個(gè) worker 創(chuàng)建時(shí)間,放入一個(gè)數(shù)組隊(duì)列里面,每次創(chuàng)建 worker 都去取隊(duì)列里前 5 條記錄 -
如果這 5 條記錄的時(shí)間間隔小于 1 分鐘,就說(shuō)明到了報(bào)警的時(shí)候了
7.6 如何實(shí)現(xiàn)進(jìn)程間的狀態(tài)共享,或者數(shù)據(jù)共享
我自己沒(méi)用過(guò) Kafka 這類(lèi)消息隊(duì)列工具,問(wèn)了 java,可以用類(lèi)似工具來(lái)實(shí)現(xiàn)進(jìn)程間通信,更好的方法歡迎留言
8、中間件
8.1 如果使用過(guò) koa、egg 這兩個(gè) Node 框架,請(qǐng)簡(jiǎn)述其中的中間件原理,最好用代碼表示一下
-
上面是在網(wǎng)上找的一個(gè)示意圖,就是說(shuō)中間件執(zhí)行就像洋蔥一樣,最早 use 的中間件,就放在最外層。處理順序從左到右,左邊接收一個(gè) request,右邊輸出返回 response -
一般的中間件都會(huì)執(zhí)行兩次,調(diào)用 next 之前為第一次,調(diào)用 next 時(shí)把控制傳遞給下游的下一個(gè)中間件。當(dāng)下游不再有中間件或者沒(méi)有執(zhí)行 next 函數(shù)時(shí),就將依次恢復(fù)上游中間件的行為,讓上游中間件執(zhí)行 next 之后的代碼 -
例如下面這段代碼
const Koa = require('koa')
const app = new Koa()
app.use((ctx, next) => {
console.log(1)
next()
console.log(3)
})
app.use((ctx) => {
console.log(2)
})
app.listen(3001)
執(zhí)行結(jié)果是1=>2=>3
koa 中間件實(shí)現(xiàn)源碼大致思路如下:
// 注意其中的compose函數(shù),這個(gè)函數(shù)是實(shí)現(xiàn)中間件洋蔥模型的關(guān)鍵
// 場(chǎng)景模擬
// 異步 promise 模擬
const delay = async () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 2000);
});
}
// 中間間模擬
const fn1 = async (ctx, next) => {
console.log(1);
await next();
console.log(2);
}
const fn2 = async (ctx, next) => {
console.log(3);
await delay();
await next();
console.log(4);
}
const fn3 = async (ctx, next) => {
console.log(5);
}
const middlewares = [fn1, fn2, fn3];
// compose 實(shí)現(xiàn)洋蔥模型
const compose = (middlewares, ctx) => {
const dispatch = (i) => {
let fn = middlewares[i];
if(!fn){ return Promise.resolve() }
return Promise.resolve(fn(ctx, () => {
return dispatch(i+1);
}));
}
return dispatch(0);
}
compose(middlewares, 1);
9、其它
現(xiàn)在在重新過(guò)一遍 node 12 版本的主要 API,有很多新發(fā)現(xiàn),比如說(shuō)
-
fs.watch 這個(gè)模塊,事件的回調(diào)函數(shù)有一個(gè)參數(shù)是觸發(fā)的事件名稱(chēng),但是呢,無(wú)論我增刪改,都是觸發(fā) rename 事件(如果更改是 update 事件,刪除 delete 事件,重命名是 rename 事件,這樣語(yǔ)義明晰該多好)。后來(lái)網(wǎng)上找到一個(gè) node-watch 模塊,此模塊增刪改都有對(duì)應(yīng)的事件, 并且還高效的支持遞歸 watch 文件。 -
util 模塊有個(gè) promisify 方法,可以讓一個(gè)遵循異常優(yōu)先的回調(diào)風(fēng)格的函數(shù),即 (err, value) => ... 回調(diào)函數(shù)是最后一個(gè)參數(shù),返回一個(gè)返回值是一個(gè) promise 版本的函數(shù)。
const util = require('util');
const fs = require('fs');
const stat = util.promisify(fs.stat);
stat('.').then((stats) => {
// 處理 `stats`。
}).catch((error) => {
// 處理錯(cuò)誤。
});
9.1 雜想
-
crypto 模塊,可以考察基礎(chǔ)的加密學(xué)知識(shí),比如摘要算法有哪些(md5, sha1, sha256,加鹽的 md5,sha256 等等),接著可以問(wèn)如何用 md5 自己模擬一個(gè)加鹽的 md5 算法, 接著可以問(wèn)加密算法(crypto.createCiphe)中的 aes,eds 算法的區(qū)別,分組加密模式有哪些(比如 ECB,CBC,為什么 ECB 不推薦),node 里的分組加密模式是哪種(CMM),這些加密算法里的填充和向量是什么意思,接著可以問(wèn)數(shù)字簽名和 https 的流程(為什么需要 CA,為什么要對(duì)稱(chēng)加密來(lái)加密公鑰等等) -
tcp/ip,可以問(wèn)很多基礎(chǔ)問(wèn)題,比如鏈路層通過(guò)什么協(xié)議根據(jù) IP 地址獲取物理地址(arp),網(wǎng)關(guān)是什么,ip 里的 ICMP 協(xié)議有什么用,tcp 的三次握手,四次分手的過(guò)程是什么,tcp 如何控制重發(fā),網(wǎng)絡(luò)堵塞 TCP 會(huì)怎么辦等等,udp 和 tcp 的區(qū)別,udp 里的廣播和組播是什么,組播在 node 里通過(guò)什么模塊實(shí)現(xiàn)。 -
os,操作系統(tǒng)相關(guān)基礎(chǔ),io 的流程是什么(從硬盤(pán)里讀取數(shù)據(jù)到內(nèi)核的內(nèi)存中,然后內(nèi)核的內(nèi)存將數(shù)據(jù)傳入到調(diào)用 io 的應(yīng)用程序的進(jìn)程內(nèi)存中),馮諾依曼體系是什么,進(jìn)程和線程的區(qū)別等等(我最近在看馬哥 linux 教程,因?yàn)樽约翰皇强瓢喑錾恚?tīng)了很多基礎(chǔ)的計(jì)算機(jī)知識(shí),受益匪淺,建議去 bilibili 看) -
linux 相關(guān)操作知識(shí)(node 涉及到后臺(tái),雖然是做中臺(tái),不涉及數(shù)據(jù)庫(kù),但是基本的 linux 操作還是要會(huì)的) -
node 性能監(jiān)控(自己也正在學(xué)習(xí)中) -
測(cè)試,因?yàn)橛玫?egg 框架,有很完善的學(xué)習(xí)單元測(cè)試的文檔,省略這部分 -
數(shù)據(jù)庫(kù)可以問(wèn)一些比如事務(wù)的等級(jí)有哪些,mysql 默認(rèn)的事務(wù)等級(jí)是什么,會(huì)產(chǎn)生什么問(wèn)題,然后考一些 mysql 查詢的筆試題。。。和常用優(yōu)化技巧,node 的 mysql 的 orm 工具使用過(guò)沒(méi)有。。。(比如我自己是看的尚硅谷 mysql 初級(jí)+高級(jí)視頻,書(shū)是看的 mysql 必知必會(huì),我自己出于愛(ài)好學(xué)習(xí)一下。。。沒(méi)有實(shí)戰(zhàn)過(guò))
最后
