這篇文章要跟大家分享一些關於Nodejs模組機制、非同步I/O、V8垃圾回收機制、Buffer模組、webSocket、進程通訊、中介軟體等的面試難題,可以回答上來嗎?能答對幾個?下面一起試試!
Node中,每個檔案模組都是一個對象,它的定義如下:
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 的實例。可以看到,目前模組(module.js)也是 Module 的一個實例。
【推薦學習:《nodejs 教學》】
這道題基本上可以了解到面試者對Node模組機制的了解程度 基本上面試提到
// require 其实内部调用 Module._load 方法 Module._load = function(request, parent, isMain) { // 计算绝对路径 var filename = Module._resolveFilename(request, parent); // 第一步:如果有缓存,取出缓存 var cachedModule = Module._cache[filename]; if (cachedModule) { return cachedModule.exports; // 第二步:是否为内置模块 if (NativeModule.exists(filename)) { return NativeModule.require(filename); } /********************************这里注意了**************************/ // 第三步:生成模块实例,存入缓存 // 这里的Module就是我们上面的1.1定义的Module var module = new Module(filename, parent); Module._cache[filename] = module; /********************************这里注意了**************************/ // 第四步:加载模块 // 下面的module.load实际上是Module原型上有一个方法叫Module.prototype.load try { module.load(filename); hadException = false; } finally { if (hadException) { delete Module._cache[filename]; } } // 第五步:输出模块的exports属性 return module.exports; };
接著上一題繼續提問
// 上面(1.2部分)的第四步module.load(filename) // 这一步,module模块相当于被包装了,包装形式如下 // 加载js模块,相当于下面的代码(加载node模块和json模块逻辑不一样) (function (exports, require, module, __filename, __dirname) { // 模块源码 // 假如模块代码如下 var math = require('math'); exports.area = function(radius){ return Math.PI * radius * radius } });
也就是說,每個module裡面都會傳入__filename, __dirname參數,這兩個參數並不是module本身就有的,是外界傳入的
module.exports vs exports 很多时候,你会看到,在Node环境中,有两种方法可以在一个模块中输出变量: 方法一:对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; 但是你不可以直接对exports赋值: // 代码可以执行,但是模块并没有输出任何变量: exports = { hello: hello, greet: greet }; 如果你对上面的写法感到十分困惑,不要着急,我们来分析Node的加载机制: 首先,Node会把整个待加载的hello.js文件放入一个包装函数load中执行。在执行这个load()函数前,Node准备好了module变量: var module = { id: 'hello', exports: {} }; load()函数最终返回module.exports: var load = function (exports, module) { // hello.js的文件内容 ... // load函数返回: return module.exports; }; var exportes = load(module.exports, module); 也就是说,默认情况下,Node准备的exports变量和module.exports变量实际上是同一个变量,并且初始化为空对象{},于是,我们可以写: exports.foo = function () { return 'foo'; }; exports.bar = function () { return 'bar'; }; 也可以写: module.exports.foo = function () { return 'foo'; }; module.exports.bar = function () { return 'bar'; }; 换句话说,Node默认给你准备了一个空对象{},这样你可以直接往里面加东西。 但是,如果我们要输出的是一个函数或数组,那么,只能给module.exports赋值: module.exports = function () { return 'foo'; }; 给exports赋值是无效的,因为赋值后,module.exports仍然是空对象{}。 结论 如果要输出一个键值对象{},可以利用exports这个已存在的空对象{},并继续在上面添加新的键值; 如果要输出一个函数或数组,必须直接对module.exports对象赋值。 所以我们可以得出结论:直接对module.exports赋值,可以应对任何情况: module.exports = { foo: function () { return 'foo'; } }; 或者: module.exports = function () { return 'foo'; }; 最终,我们强烈建议使用module.exports = xxx的方式来输出模块变量,这样,你只需要记忆一种方法。
本章的答案思路大多藉鑑於樸靈大神的《深入淺出的NodeJS》
在進程啟動時,Node就會創建一個類似while(true)的循環,每執行一次循環體的過程我們變成Tick。
每個Tick的過程就是查看是否有事件待處理。如果有就取出事件及其相關的回呼函數。然後進入下一個循環,如果不再有事件處理,就退出進程。
每個事件循環中都有一個或多個觀察者,而判斷是否有事件需要處理的過程就是向這些觀察者詢問是否有要處理的事件。
在Node中,事件主要來自網路請求、檔案的I/O等,這些事件對應的觀察者有檔案I/O觀察者,網路I/O的觀察者。
事件循環是典型的生產者/消費者模式。非同步I/O,網路請求等則是事件的生產者,源源不斷為Node提供不同類型的事件,這些事件被傳遞到對應的觀察者那裡,事件循環則從觀察者那裡取出事件並處理。
在windows下,這個循環基於IOCP創建,在*nix下則基於多線程創建
使用process.memoryUsage(),傳回如下
{ rss: 4935680, heapTotal: 1826816, heapUsed: 650472, external: 49879 }
heapTotal 和heapUsed 代表V8的記憶體使用量。 external代表V8管理的,綁定到Javascript的C 物件的記憶體使用量。 rss, 駐留集大小, 是給這個進程分配了多少物理內存(佔總分配內存的一部分) 這些物理內存中包含堆,棧,和代碼段。
#64位元系統下是1.4GB,32位元系統下是0.7GB。因為1.5GB的垃圾回收堆內存,V8需要花費50毫秒以上,做一次非增量式的垃圾回收甚至要1秒以上。這是垃圾回收中引起Javascript執行緒暫停執行的事件,在這樣的花銷下,應用的效能和影響力都會直線下降。
在V8中,主要将内存分为新生代和老生代两代。新生代中的对象存活时间较短的对象,老生代中的对象存活时间较长,或常驻内存的对象。
新生代中的对象主要通过Scavenge算法进行垃圾回收。这是一种采用复制的方式实现的垃圾回收算法。它将堆内存一份为二,每一部分空间成为semispace。在这两个semispace空间中,只有一个处于使用中,另一个处于闲置状态。处于使用状态的semispace空间称为From空间,处于闲置状态的空间称为To空间。
当开始垃圾回收的时候,会检查From空间中的存活对象,这些存活对象将被复制到To空间中,而非存活对象占用的空间将会被释放。完成复制后,From空间和To空间发生角色对换。
应为新生代中对象的生命周期比较短,就比较适合这个算法。
当一个对象经过多次复制依然存活,它将会被认为是生命周期较长的对象。这种新生代中生命周期较长的对象随后会被移到老生代中。
老生代主要采取的是标记清除的垃圾回收算法。与Scavenge复制活着的对象不同,标记清除算法在标记阶段遍历堆中的所有对象,并标记活着的对象,只清理死亡对象。活对象在新生代中只占叫小部分,死对象在老生代中只占较小部分,这是为什么采用标记清除算法的原因。
主要问题是每一次进行标记清除回收后,内存空间会出现不连续的状态
闭包和全局变量
什么是内存泄漏
一、全局变量
a = 10; //未声明对象。 global.b = 11; //全局变量引用 这种比较简单的原因,全局变量直接挂在 root 对象上,不会被清除掉。
二、闭包
function out() { const bigData = new Buffer(100); inner = function () { } }
闭包会引用到父级函数中的变量,如果闭包未释放,就会导致内存泄漏。上面例子是 inner 直接挂在了 root 上,那么每次执行 out 函数所产生的 bigData 都不会释放,从而导致内存泄漏。
需要注意的是,这里举得例子只是简单的将引用挂在全局对象上,实际的业务情况可能是挂在某个可以从 root 追溯到的对象上导致的。
三、事件监听
Node.js 的事件监听也可能出现的内存泄漏。例如对同一个事件重复监听,忘记移除(removeListener),将造成内存泄漏。这种情况很容易在复用对象上添加事件时出现,所以事件重复监听可能收到如下警告:
emitter.setMaxListeners() to increase limit
例如,Node.js 中 Agent 的 keepAlive 为 true 时,可能造成的内存泄漏。当 Agent keepAlive 为 true 的时候,将会复用之前使用过的 socket,如果在 socket 上添加事件监听,忘记清除的话,因为 socket 的复用,将导致事件重复监听从而产生内存泄漏。
原理上与前一个添加事件监听的时候忘了清除是一样的。在使用 Node.js 的 http 模块时,不通过 keepAlive 复用是没有问题的,复用了以后就会可能产生内存泄漏。所以,你需要了解添加事件监听的对象的生命周期,并注意自行移除。
排查方法
想要定位内存泄漏,通常会有两种情况:
对于只要正常使用就可以重现的内存泄漏,这是很简单的情况只要在测试环境模拟就可以排查了。
对于偶然的内存泄漏,一般会与特殊的输入有关系。想稳定重现这种输入是很耗时的过程。如果不能通过代码的日志定位到这个特殊的输入,那么推荐去生产环境打印内存快照了。
需要注意的是,打印内存快照是很耗 CPU 的操作,可能会对线上业务造成影响。 快照工具推荐使用 heapdump 用来保存内存快照,使用 devtool 来查看内存快照。
使用 heapdump 保存内存快照时,只会有 Node.js 环境中的对象,不会受到干扰(如果使用 node-inspector 的话,快照中会有前端的变量干扰)。
PS:安装 heapdump 在某些 Node.js 版本上可能出错,建议使用 npm install heapdump -target=Node.js 版本来安装。
不会,Buffer属于堆外内存,不是V8分配的。
Buffer.allocUnsafe创建的 Buffer 实例的底层内存是未初始化的。 新创建的 Buffer 的内容是未知的,可能包含敏感数据。 使用 Buffer.alloc() 可以创建以零初始化的 Buffer 实例。
为了高效的使用申请来的内存,Node采用了slab分配机制。slab是一种动态的内存管理机制。 Node以8kb为界限来来区分Buffer为大对象还是小对象,如果是小于8kb就是小Buffer,大于8kb就是大Buffer。
例如第一次分配一个1024字节的Buffer,Buffer.alloc(1024),那么这次分配就会用到一个slab,接着如果继续Buffer.alloc(1024),那么上一次用的slab的空间还没有用完,因为总共是8kb,1024+1024 = 2048个字节,没有8kb,所以就继续用这个slab给Buffer分配空间。
如果超过8kb,那么直接用C++底层地宫的SlowBuffer来给Buffer对象提供空间。
例如一个份文件test.md里的内容如下:
床前明月光,疑是地上霜,举头望明月,低头思故乡
我们这样读取就会出现乱码:
var rs = require('fs').createReadStream('test.md', {highWaterMark: 11}); // 床前明???光,疑???地上霜,举头???明月,???头思故乡
一般情况下,只需要设置rs.setEncoding('utf8')即可解决乱码问题
首先,WebSocket连接必须由浏览器发起,因为请求协议是一个标准的HTTP请求,格式如下:
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
该请求和普通的HTTP请求有几点不同:
随后,服务器如果接受该请求,就会返回如下响应:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: server-random-string
该响应代码101表示本次连接的HTTP协议即将被更改,更改后的协议就是Upgrade: websocket指定的WebSocket协议。
密钥:密钥是一种参数,它是在明文转换为密文或将密文转换为明文的算法中输入的参数。密钥分为对称密钥与非对称密钥,分别应用在对称加密和非对称加密上。
对称加密:对称加密又叫做私钥加密,即信息的发送方和接收方使用同一个密钥去加密和解密数据。对称加密的特点是算法公开、加密和解密速度快,适合于对大数据量进行加密,常见的对称加密算法有DES、3DES、TDEA、Blowfish、RC5和IDEA。
非对称加密:非对称加密也叫做公钥加密。非对称加密与对称加密相比,其安全性更好。对称加密的通信双方使用相同的密钥,如果一方的密钥遭泄露,那么整个通信就会被破解。而非对称加密使用一对密钥,即公钥和私钥,且二者成对出现。私钥被自己保存,不能对外泄露。公钥指的是公共的密钥,任何人都可以获得该密钥。用公钥或私钥中的任何一个进行加密,用另一个进行解密。
摘要: 摘要算法又称哈希/散列算法。它通过一个函数,把任意长度的数据转换为一个长度固定的数据串(通常用16进制的字符串表示)。算法不可逆。
如果不签名会存在中间人攻击的风险,签名之后保证了证书里的信息,比如公钥、服务器信息、企业信息等不被篡改,能够验证客户端和服务器端的“合法性”。
简要图解如下
面对node单线程对多核CPU使用不足的情况,Node提供了child_process模块,来实现进程的复制,node的多进程架构是主从模式,如下所示:
var fork = require('child_process').fork; var cpus = require('os').cpus(); for(var i = 0; i < cpus.length; i++){ fork('./worker.js'); }
在linux中,我们通过ps aux | grep worker.js查看进程
这就是著名的主从模式,Master-Worker
创建子进程的方法大致有:
var createWorker = function(){ var worker = fork(__dirname + 'worker.js') worker.on('exit', function(){ console.log('Worker' + worker.pid + 'exited'); // 如果退出就创建新的worker createWorker() }) }
我自己没用过Kafka这类消息队列工具,问了java,可以用类似工具来实现进程间通信,更好的方法欢迎留言
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)
执行结果是
1=>2=>3
koa中间件实现源码大致思路如下:
// 注意其中的compose函数,这个函数是实现中间件洋葱模型的关键 // 场景模拟 // 异步 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 实现洋葱模型 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);
现在在重新过一遍node 12版本的主要API,有很多新发现,比如说
const util = require('util'); const fs = require('fs'); const stat = util.promisify(fs.stat); stat('.').then((stats) => { // 处理 `stats`。 }).catch((error) => { // 处理错误。 });
更多编程相关知识,请访问:编程入门!!
以上是這些Nodejs面試難題,你能回答上來嗎?的詳細內容。更多資訊請關注PHP中文網其他相關文章!