Node.js

关于 Node.js® Node.js® 是一个基于 Chrome V8 引擎 的 JavaScript 运行时。 作为异步驱动的 JavaScript 运行时,Node.js 被设计成可升级的网络应用。在下面的“Hello World”示例中,许多连接可以并行处理。每一个连接都会触发一个回调,但是如果没有可做的事情,Node.js 就进入睡眠状态。 这与今天使用 OS 线程的更常见并发模型形成了对比。基于线程的网络效率相对低下,使用起来非常困难。此外,Node.js 的用户不必担心死锁过程,因为没有锁。Node 中几乎没有函数直接执行 I/O 操作,因此进程从不阻塞。由于没有任何阻塞,可伸缩系统在 Node 中开发是非常合理的。 如果你对这门语言其中的一部分尚未熟悉理解,这里有一篇专门关于阻塞对比非阻塞的文章供你参考。

关于 | Node.js

关于 Node.js®

作为异步驱动的 JavaScript 运行时,Node.js 被设计成可升级的网络应用。在下面的“Hello World”示例中,许多连接可以并行处理。每一个连接都会触发一个回调,但是如果没有可做的事情,Node.js 就进入睡眠状态。

const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

这与今天使用 OS 线程的更常见并发模型形成了对比。基于线程的网络效率相对低下,使用起来非常困难。此外,Node.js 的用户不必担心死锁过程,因为没有锁。Node 中几乎没有函数直接执行 I/O 操作,因此进程从不阻塞。由于没有任何阻塞,可伸缩系统在 Node 中开发是非常合理的。

如果你对这门语言其中的一部分尚未熟悉理解,这里有一篇专门关于阻塞对比非阻塞的文章供你参考。

Node.js 在设计上类似于 Ruby 的事件机或 Python 的 Twisted之类的系统。Node.js 更深入地考虑事件模型。它呈现一个事件轮询作为运行时构造而不是库。在其它系统中,总是有一个阻止调用来启动事件循环。

通常 Node.js 的行为是通过在脚本开头的回调定义的,在结束时通过阻塞调用(如 EventMachine::run() )启动服务器。在 Node.js 中没有这样的启动-事件循环调用。Node.js 在执行输入脚本后只需输入事件循环即可。 当没有更多要执行的回调时,Node.js 退出事件循环。此行为类似于浏览器中的 JavaScript ——事件循环总是对用户不可见的。

HTTP 是 Node.js 中的一等公民。它设计的是流式和低延迟。这使得 Node.js 非常适合于 web 库或框架的基础。

仅仅因为 Node.js 是在没有线程的情况下设计的,这并不意味着您无法利用环境中的多个内核。子进程可以通过使用我们的 child_process.fork() API 来生成,并且被设计为易于沟通。建立在同一接口上的是 cluster 模块,它允许您在进程之间共享套接字,以便在核心上启用负载平衡。

Node.js 安装配置

本章节我们将向大家介绍在 Windows 和 Linux 上安装 Node.js 的方法。

目前的最新版 LTS (长期支持版本) Node.js v12.16.1

Node.js 安装包及源码下载地址为:https://nodejs.org/en/download/。

你可以根据不同平台系统选择你需要的 Node.js 安装包。

Node.js 历史版本下载地址:https://nodejs.org/dist/

注意:Linux 上安装 Node.js 需要安装 Python 2.6 或 2.7 ,不建议安装 Python 3.0 以上版本。

Windows 上安装 Node.js

你可以采用以下三种方式来安装。

1、Windows 绿色版 node-v10.19.0-win-x64

C:\Software\Node 为例

2、Windows 安装包(.msi)

本文实例以 v0.10.26 版本为例,其他版本类似, 安装步骤:

检测 PATH 环境变量是否配置了Node.js,点击开始 => 运行 => 输入"cmd" => 输入命令"path",输出如下结果:

PATH=C:\oraclexe\app\oracle\product\10.2.0\server\bin;C:\Windows\system32;
C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;
c:\python32\python;C:\MinGW\bin;C:\Program Files\GTK2-Runtime\lib;
C:\Program Files\MySQL\MySQL Server 5.5\bin;C:\Program Files\nodejs\;
C:\Users\rg\AppData\Roaming\npm

我们可以看到环境变量中已经包含了C:\Program Files\nodejs\

检查Node.js版本

3、Windows 二进制文件 (.exe)安装

32 位安装包下载地址 : http://nodejs.org/dist/v0.10.26/node.exe

64 位安装包下载地址 : http://nodejs.org/dist/v0.10.26/x64/node.exe

安装步骤

版本测试

进入 node.exe 所在的目录,如下所示:

如果你获得以上输出结果,说明你已经成功安装了Node.js。

Linux 上安装 Node.js

直接使用已编译好的包

mkdir -p /usr/software/nodejs
cd /usr/software/nodejs

# wget https://nodejs.org/dist/v10.19.0/node-v10.19.0-linux-x64.tar.xz    // 下载
# tar xf  node-v10.19.0-linux-x64.tar.xz       // 解压
# cd node-v10.19.0-linux-x64/                  // 进入解压目录
# ./bin/node -v                                // 执行node命令 查看版本
v10.19.0
ln -s /usr/software/nodejs/node-v10.19.0-linux-x64/bin/npm   /usr/local/bin/ 
ln -s /usr/software/nodejs/node-v10.19.0-linux-x64/bin/node   /usr/local/bin/

Ubuntu 源码安装 Node.js

以下部分我们将介绍在 Ubuntu Linux 下使用源码安装 Node.js 。 其他的 Linux 系统,如 Centos 等类似如下安装步骤。

在 Github 上获取 Node.js 源码:

$ sudo git clone https://github.com/nodejs/node.git
Cloning into 'node'...
$ sudo chmod -R 755 node
$ cd node
$ sudo ./configure
$ sudo make
$ sudo make install
$ node -v
v10.19.0

Ubuntu apt-get命令安装

命令格式如下:

sudo apt-get install nodejs
sudo apt-get install npm

CentOS 下源码安装 Node.js(推荐方式

cd /usr/local/src/
wget http://nodejs.org/dist/v10.19.0/node-v10.19.0-linux-x64.tar.gz
tar zxvf node-v10.19.0-linux-x64.tar.gz
cd node-v10.19.0-linux-x64
./configure --prefix=/usr/local/node/node-v10.19.0-linux-x64
make
make install
vim /etc/profile

设置 nodejs 环境变量,在 export PATH USER LOGNAME MAIL HOSTNAME HISTSIZE HISTCONTROL 一行的上面添加如下内容:

#set for nodejs
export NODE_HOME=/usr/local/node/node-v10.19.0-linux-x64
export PATH=$NODE_HOME/bin:$PATH

:wq 保存并退出,编译 /etc/profile 使配置生效

source /etc/profile

验证是否安装配置成功

node -v

输出 v10.19.0 表示配置成功

npm模块安装路径

/usr/local/node/v10.19.0/lib/node_modules/

注: Nodejs 官网提供了编译好的 Linux 二进制包,你也可以下载下来直接应用。

Mac OS 上安装

你可以通过以下两种方式在 Mac OS 上来安装 node:

brew install node

Node.js 基础

# 认识 Node.js - Node 是一个服务器端 JavaScript 解释器 - Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境 - Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效 - Node.js 的包管理器 npm,是全球最大的开源库生态系统 - Node.js 是一门动态语言,运行在服务端的 Javascript # 版本介绍 - 在命令窗口中输入 node -v 可以查看版本 - 0.x 完全不技术 ES6 - 4.x 部分支持 ES6 特性 - 5.x 部分支持ES6特性(比4.x多些),属于过渡产品,现在来说应该没有什么理由去用这个了 - 6.x 支持98%的 ES6 特性 - 8.x 支持 ES6 特性 https://niefee.github.io/node-interview-knowledge

Node.js 基础

Node.js 基础

认识 Node.js

版本介绍

环境搭建

REPL(交互式解释器)

在命令窗口输入 node 后回车,便可进入到 REPL 模式,在这个模式里可以输入 Javascript 的脚本语法,node 会自动将语法执行。类似于在浏览器的开发人员工具的控制台。不同的在于 REPL 是在服务端解析 Javascript,而控制台是在客户端解析 Javascript。按 CTRL + C 可退出 REPL 模式。

运行 Node.js

REPL 只适用于一些简单的 Javascript 语法,对于稍复杂的程序,可以直接写到 js 文件当中。在前端可以直接在 html 页面中通过 script 标签引入 js 然后在浏览器运行,则可以通过浏览器来解析 js 文件。在 node 环境下,可通过命令窗口输入命令: node *.js ,便可直接执行 js 文件。

Node.js 模块

模块系统是 Node.js 最基本也是最常用的。一般情况模块可分为四类:

自定义模块

  1. 创建模块(b.js)
//b.js
function FunA(){
    return 'Tom';
}
//暴露方法 FunA
module.exports = FunA;
  1. 加载模块(a.js)
//a.js
var FunA = require('./b.js');//得到 b.js => FunA
var name = FunA();// 运行 FunA,name = 'Tom'
console.log(name); // 输出结果

module.exports

module.exports 就 Node.js 用于对外暴露,或者说对外开放指定访问权限的一个对象。如上面的案例,如果没有这段代码

module.exports = FunA;

那么 require('./b.js') 就会为 undefined。 一个模块中有且仅有一个 module.exports,如果有多个那后面的则会覆盖前面的。

exports

exports 是 module 对象的一个属性,同时它也是一个对象。在很多时候一个 js 文件有多个需要暴露的方法或是对象,module.exports 又只能暴露一个,那这个时候就要用到 exports:

function FunA(){
    return 'Tom';
}

function FunB(){
    return 'Sam';
}

exports.FunA = FunA;
exports.FunB = FunB;
//FunA = exports,exports 是一个对象
var FunA = require('./b.js');
var name1 = FunA.FunA();// 运行 FunA,name = 'Tom'
var name2 = FunA.FunB();// 运行 FunB,name = 'Sam'
console.log(name1);
console.log(name2);

当然在引入的时候也可以这样写

//FunA = exports,exports 是一个对象
var {FunA, FunB} = require('./b.js');
var name1 = FunA();// 运行 FunA,name = 'Tom'
var name2 = FunB();// 运行 FunB,name = 'Sam'
console.log(name1);
console.log(name2);

npm scripts

什么是 npm 脚本

npm 允许在package.json文件里面,使用scripts字段定义脚本命令。package.json 里面的scripts 字段是一个对象。它的每一个属性,对应一段脚本。定义在package.json里面的脚本,就称为 npm 脚本。

查看当前项目的所有 npm 脚本命令,可以使用不带任何参数的npm run命令。

使用

简写形式

forever

forever 介绍

forever是一个简单的命令式nodejs的守护进程,能够启动,停止,重启App应用。forever完全基于命令行操作,在forever进程之下,创建node的子进程,通过monitor监控node子进程的运行情况,一旦文件更新,或者进程挂掉,forever会自动重启node服务器,确保应用正常运行。

forever 安装

forever 命令行的中文解释

子命令actions:

- start:启动守护进程
- stop:停止守护进程
- stopall:停止所有的forever进程
- restart:重启守护进程
- restartall:重启所有的foever进程
- list:列表显示forever进程
- config:列出所有的用户配置项
- set <key> <val>: 设置用户配置项
- clear <key>: 清楚用户配置项
- logs: 列出所有forever进程的日志
- logs <script|index>: 显示最新的日志
- columns add <col>: 自定义指标到forever list
- columns rm <col>: 删除forever list的指标
- columns set<cols>: 设置所有的指标到forever list
- cleanlogs: 删除所有的forever历史日志

forever 常用命令

配置参数options:

- -m MAX: 运行指定脚本的次数
- -l LOGFILE: 输出日志到LOGFILE
- -o OUTFILE: 输出控制台信息到OUTFILE
- -e ERRFILE: 输出控制台错误在ERRFILE
- -p PATH: 根目录
- -c COMMAND: 执行命令,默认是node
- -a, –append: 合并日志
- -f, –fifo: 流式日志输出
- -n, –number: 日志打印行数
- –pidFile: pid文件
- –sourceDir: 源代码目录
- –minUptime: 最小spinn更新时间(ms)
- –spinSleepTime: 两次spin间隔时间
- –colors: 控制台输出着色
- –plain: –no-colors的别名,控制台输出无色
- -d, –debug: debug模式
- -v, –verbose: 打印详细输出
- -s, –silent: 不打印日志和错误信息
- -w, –watch: 监控文件改变
- –watchDirectory: 监控顶级目录
- –watchIgnore: 通过模式匹配忽略监控
- -h, –help: 命令行帮助信息
Node.js 基础

Node 的部署方案

pm2

安装pm2

npm install pm2 -g

新建一份 index.js 测试,运行以下命令测试

pm2 start index.js

运行

你可以执行以下命令来重启和暂停服务

pm2 stop     <app_name|id|'all'|json_conf>
pm2 restart  <app_name|id|'all'|json_conf>
pm2 delete   <app_name|id|'all'|json_conf>

比如 pm2 stop index.js ,暂停上面的 index.js 服务

自动重启

当文件改动则自动重启服务

pm2 start app.js --watch

这里是监控整个项目的文件,如果只想监听指定文件和目录,建议通过下面配置文件的 watchignore_watch 字段来设置

配置文件

编写一份 ecosystem.json 文件,完整配置说明请参考官方文档

{
    "name": "test", // 应用名称
    "script": "./bin/www", // 实际启动脚本
    "cwd": "./", // 当前工作路径
    "watch": [ // 监控变化的目录,一旦变化,自动重启
        "bin",
        "routers"
    ],
    "ignore_watch": [ // 从监控目录中排除
        "node_modules",
        "logs",
        "public"
    ],
    "watch_options": {
        "followSymlinks": false
    },
    "max_memory_restart": "100M", //超过最大内存重启
    "error_file": "./logs/app-err.log", // 错误日志路径
    "out_file": "./logs/app-out.log", // 普通日志路径
    "env": {
        "NODE_ENV": "production" // 环境参数,当前指定为生产环境
    }
}

配置完后你可以执行以下命令

# Start all apps
pm2 start ecosystem.json

# Stop
pm2 stop ecosystem.json

# Restart
pm2 start ecosystem.json
## Or
pm2 restart ecosystem.json

# Reload
pm2 reload ecosystem.json

# Delete from PM2
pm2 delete ecosystem.json

这里注意的是配置文件改变了之后要先 deletestart 配置文件才能生效

负载均衡

命令如下,表示开启三个进程。如果-i 0,则会根据机器当前核数自动开启尽可能多的进程

pm2 start app.js -i 3      //开启三个进程
pm2 start app.js -i max //根据机器CPU核数,开启对应数目的进程

日志查看

除了可以打开日志文件查看日志外,还可以通过 pm2 logs 来查看实时日志。这点对于线上问题排查非常重要

比如某个node服务突然异常重启了,那么可以通过 pm2 提供的日志工具来查看实时日志,看是不是脚本出错之类导致的异常重启。

pm2 logs

内存使用超过上限自动重启

如果想要你的应用,在超过使用内存上限后自动重启,那么可以加上 --max-memory-restart 参数。(有对应的配置项)

pm2 start big-array.js --max-memory-restart 20M

参考文档

Node.js 基础

异步嵌套解决方案

promise

每一个异步请求立刻返回一个Promise对象,由于是立刻返回,所以可以采用同步操作的流程。而Promise的then方法,允许指定回调函数,在异步任务完成后调用

下面的setTimeout()可以代替理解为一个ajax请求,所以ajax请求同理

function a() {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log('执行任务a');
            resolve('执行任务a成功');
        }, 1000);
    });
}

function b(value) {
    console.log(value)
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log('执行任务b');
            resolve('执行任务b成功');
        }, 2000);
    });
}

function c() {
    console.log('最后执行c')
}
a().then(b).then(c);

类promise

很多像promise的异步封装方法,比如angular1.x内置封装的$http方法,如下,可以实现多个回调的链式调用,避免了金字塔式的回调地狱

//1
$http({
	method: 'GET',
	url: 'news.json',
}).then(function successCallback(response) {
	console.log(response)
}, function errorCallback(response) {
	console.log(response)
})
//2
.then(function() {
	return $http({
		method: 'GET',
		url: 'data.json',
	})
}).then(function(data) {
	console.log(data)
})
//3
.then(function() {
	setTimeout(function() {
		console.log("定时器")
	}, 1000)
})

await

await 是个运算符,用于组成表达式,await 表达式的运算结果取决于它等的东西。

如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。

如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。

function a() {
  return new Promise(function(resolve){
    setTimeout(()=>{
    	console.log("a")
    	resolve()
    },1000)
  });
}

function b() {
  return new Promise(function(resolve){
    setTimeout(()=>{
    	console.log("b")
    	resolve()
    },1000)
  });
}

function c() {
  return new Promise(function(resolve){
    setTimeout(()=>{
    	console.log("c")
    	resolve()
    },1000)
  });
}

//ES6
a()
  .then(b)
  .then(c);

//ES2017
await a();
await b();
await c();

await等待的虽然是promise对象,但不必写.then(..),直接可以得到返回值,所以使用await就没有了多个then的链式调用

var sleep = function (time) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            resolve();
        }, time);
    })
};

var start = async function () {
    // 在这里使用起来就像同步代码那样直观
    console.log('start');
    await sleep(3000);
    console.log('end');
};
start();

deferred

$.ajax()操作完成后,如果使用的是低于1.5.0版本的jQuery,返回的是XHR对象,你没法进行链式操作;如果高于1.5.0版本,返回的是deferred对象,可以进行链式操作,done()相当于success方法,fail()相当于error方法。采用链式写法以后,代码的可读性大大提高,deferred对象的一大好处,就是它允许你自由添加多个回调函数

$.when(function(dtd) {
	var dtd = $.Deferred(); // 新建一个deferred对象
	setTimeout(function() {
		console.log(0);
		dtd.resolve(1); // 改变deferred对象的执行状态 触发done回调
		//dtd.reject(); //跟resolve相反,触发fail回调
	}, 1000);
	return dtd;
}()).done(function(num) {
	console.log(num);
}).done(function() {
	console.log(2);
}).done(function() {
	console.log(2);
})

//ajax默认就是返回deferred对象
$.when($.post("index.php", {
		name: "wscat",
	}), $.post("other.php"))
	.done(function(data1, data2) {
		//两个ajax成功才可以进入done回调
		console.log(data1, data2);
	}).fail(function(err) {
		//其中一个ajax失败都会进入fail回调
		console.log(err)
	})

event loop

先处理微任务队列再处理宏任务队列

微任务 宏任务
then setTimeout
console.log('script start');
setTimeout(function() {
  console.log('setTimeout');
}, 0);//定时器为宏任务
Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {//then为微任务
  console.log('promise2');
});
console.log('script end');
//先同步后异步
//先清空微任务再清空宏任务

输出的顺序是: script start, script end, promise1, promise2, setTimeout

console.log('script start');
setTimeout(function() {
  console.log('timeout1');
}, 10);
new Promise(resolve => {
    console.log('promise1');
    resolve();
    setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
    console.log('then1')
})
console.log('script end');

输出的顺序是: script start, promise1, script end, then1, timeout1, timeout2

配置await/async环境

安装一下依赖

npm i -D babel-core babel-polyfill babel-preset-es2015 babel-preset-stage-0 babel-loader

新建 .babelrc 文件,输入以下内容

{
    "presets": [
        "stage-0",
        "es2015"
    ]
}

新建一份index.js,把你的逻辑文件 app.js,后面require的任何模块都交给babel处理,polyfill支持 awaitasync

require("babel-core/register");
require("babel-polyfill");
require("./app.js");

参考Babel 6 regeneratorRuntime is not defined

await,async,promise三者配合

//async定义里面的异步函数顺序执行
((async() => {
	try {
		//await相当于等待每个异步函数执行完成,然后继续下一个await函数
		const a = await (() => {
			return new Promise((resolve, reject) => {
				setTimeout(() => {
					console.log(1)
					resolve(2);
					//reject(3)
				}, 1000)
			});
		})();
		const b = await (() => {
			return new Promise((resolve, reject) => {
				setTimeout(() => {
					console.log(1)
					//resolve(2);
					reject(3)
				}, 1000)
			});
		})();
		console.log(4)
		console.log(a) //3
		return b;
	} catch(err) {
		//上面try中一旦触发reject则进入这个分支
		console.log(err);
		return err;
	}
})()).then((data) => {
	console.error(data)
}).catch((err) => {
	console.error(err)
})
//分别输出213

注意点:

当然我个人觉得下面写法比较清晰点

//利用try...catch捕捉Promise的reject
async function ajax(data) {
	try {
		return await new Promise((resolve, reject) => {
			setTimeout(() => {
				console.log(data)
				resolve(data); //成功
			}, 2000);
		});
	} catch(err) {}
}
async function io() {
	try {
		const response = await new Promise((resolve, reject) => {
			setTimeout(() => {
				reject("io"); //失败
			}, 1000);
		});
		//resolve执行才会执行下面这句return 
		return response
	} catch(err) {
		console.log(err);
	}
}
//异步串行
(async() => {
	await ajax("ajax1");
	await ajax("ajax2");
	await io();
})()

(async() => {
    let [ajax1, ajax2] = await Promise.all([ajax("ajax1"), ajax("ajax2"),io()]);
    return [ajax1,ajax2]
})()

worker

Web Worker 是 HTML5 标准的一部分,这一规范定义了一套 API,允许一段 JavaScript 程序运行在主线程之外的另外一个线程中。将一些任务分配给后者运行。在主线程运行的同时,子线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程通常负责 UI 交互就会很流畅,不会被阻塞或拖慢。

<input id="btn" type="button" value="通信" />
<script>
    let myWorker = new Worker('./worker.js');
    let button = document.querySelector('#btn');
    // myWorker.onmessage = function (event) { // 接收
    //     console.log('子线程通知主线程:' + event.data);
    //     myWorker.terminate(); // 暂停
    // }
    myWorker.addEventListener('message', function (e) {
        console.log('子线程通知主线程:' + event.data);
        myWorker.terminate(); // 暂停
    });
    // 监听 error 事件
    myWorker.addEventListener('error', function (e) {
        console.log('错误', e);
    });
    button.onclick = function () {
        myWorker.postMessage("主线程通知子线程");  // 启动消息发送线程发送消息
    }
</script>

worker.js

addEventListener('message', function (e) {
    postMessage('子线程通知主线程: ' + e.data);
}, false);

由于多线程需要在同域情况下进行,所以我们可以借助 blob 把它放到同一个文件下执行

let script = 'console.log("hello world!");'
let workerBlob = new Blob([script], { type: "text/javascript" });
let url = URL.createObjectURL(workerBlob);
let myWorker = new Worker(url);

参考 JavaScript多线程编程

堆和栈

区别 堆(heap) 栈(stack)
结构 heap是没有结构的,数据可以任意存放。heap用于复杂数据类型(引用类型)分配空间 stack是有结构的,每个区块按照一定次序存放(后进先出),stack中主要存放一些基本类型的变量和对象的引用,存在栈中的数据大小与生存期必须是确定的。可以明确知道每个区块的大小,因此,stack的寻址速度要快于heap
速度
图示 image image
类型 引用类型:对象,数组的内容 Boolean、Number、String、Undefined、Null,以及对象变量的指针
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便 栈由系统自动分配,速度较快。但程序员是无法控制的
对堆而言,数据项位置没有固定的顺序。你可以以任何顺序插入和删除,因为他们没有顶部数据这一概念 对栈而言,栈中的新加数据项放在其他数据的顶部,移除时你也只能移除最顶部的数据(不能越位获取)

使用new关键字初始化的之后是不存储在栈内存中的。为什么呢?new大家都知道,根据构造函数生成新实例,这个时候生成的是对象,而不是基本类型。再看一个例子

var a = new String('123')
var b = String('123')
var c = '123'
console.log(a==b, a===b, b==c, b===c, a==c, a===c)  
>>> true false true true true false
console.log(typeof a)
>>> 'object'

我们可以看到new一个String,出来的是对象,而直接字面量赋值和工厂模式出来的都是字符串。但是根据我们上面的分析大小相对固定可预期的即便是对象也可以存储在栈内存的,比如null,为啥这个不是呢?再继续看

var a = new String('123')
var b = new String('123')
console.log(a==b, a===b)
>>> false false

很明显,如果a,b是存储在栈内存中的话,两者应该是明显相等的,就像null === null是true一样,但结果两者并不相等,说明两者都是存储在堆内存中的,指针指向不一致

说到这里,再去想一想我们常说的值类型和引用类型其实说的就是栈内存变量和堆内存变量,再想想值传递和引用传递、深拷贝和浅拷贝,都是围绕堆栈内存展开的,一个是处理值,一个是处理指针

堆、栈、队列之间的区别是?

内存分配和垃圾回收

一般来说栈内存线性有序存储,容量小,系统分配效率高。而堆内存首先要在堆内存新分配存储区域,之后又要把指针存储到栈内存中,效率相对就要低一些了

垃圾回收方面,栈内存变量基本上用完就回收了,而推内存中的变量因为存在很多不确定的引用,只有当所有调用的变量全部销毁之后才能回收

参考浅析JS中的堆内存与栈内存

传值和传址

从一个向另一个变量复制引用类型的值,复制的其实是指针,因此两个变量最终指向同一个对象。即复制的是栈中的地址而不是堆中的对象

从一个变量复向另一个变量复制基本类型的值,会创建这个值的副本

参考文章

Node.js 基础

饿了么 Node.js 面试知识学习

饿了么 Node.js 面试知识学习

饿了么 Node.js 面试,建议优先阅读原版教程内容。

如何通过饿了么 Node.js 面试

Hi, 欢迎来到 ElemeFE, 如标题所示本教程的目的是教你如何通过饿了么大前端的面试, 职位是 2~3 年经验的 Node.js 服务端程序员 (并不是全栈), 如果你对这个职位感兴趣或者学习 Node.js 一些进阶的内容, 那么欢迎围观.

需要注意的是, 本文针对的并不是零基础的同学, 你需要有一定的 JavaScript/Node.js 基础, 并且有一定的工作经验. 另外本教程的重点更准确的说是服务端基础中 Node.js 程序员需要了解的部分.

如果你觉得大多不了解, 就不用投简历了 (这样两边都节约了时间), 如果你觉得大都有了解或者光看大纲都都觉得很简单那么欢迎投递简历至 ElemeFe (fe.job@ele.me).

导读

虽然说目的是要通过面试, 但是本教程并不是简单的把所有面试题列出来, 而主要是将面试中需要确认你是否懂的点列举出来, 并进行一定程度的讨论.

本文将一些常见的问题划分归类, 每类标明涵盖的一些覆盖点, 并且列举几个常见问题, 通常这些问题都是 2~3 年工作经验需要了解或者面对的. 如果你对某类问题感兴趣, 或者想知道其中列举问题的答案, 可以通过该类下方的 阅读更多 查看更多的内容.

整体上大纲列举的并不是很全面, 细节上覆盖率不高, 很多讨论只是点到即止, 希望大家带着问题去思考.

Js 基础问题

原版教程内容

模块

原版教程内容

事件/异步

原版教程内容

进程

原版教程内容

IO

原版教程内容

Network

原版教程内容

OS

原版教程内容

错误处理/调试

原版教程内容

测试

原版教程内容

util

原版教程内容

存储

原版教程内容

安全

原版教程内容

Node.js 基础

模块

模块

模块机制

CommonJS规范

在 Node.js 模块系统中,每个文件都被视为独立的模块,有自己独立的作用域。

Node.js 模块系统遵循的是CommonJS规范。CommonJS规范加载模块是同步加载,只有加载完才能执行后续操作。 CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};
// main.js
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;

console.log(counter);  // 3
incCounter();
console.log(counter); // 3

模块分类

Node的模块分类:

  1. 核心模块:大多以C/C++编写,编译成二进制文件。部分核心模块在Node启动时已加载到内存中。
  2. 文件模块:开发者编写的的模块,运行时动态加载,速度慢于核心模块。文件模块中,又分为 3 类模块
    1. .js。通过 fs 模块同步读取 js 文件并编译执行。
    2. .node。通过 C/C++ 进行编写的 Addon。通过 dlopen 方法进行加载。
    3. .json。读取文件,调用 JSON.parse 解析加载。

模块引入

在Node中引入模块的一般步骤:

  1. 路径分析
  2. 文件定位
  3. 编译执行

require命令是CommonJS规范之中,用来加载其他模块的命令。它其实不是一个全局命令,而是指向当前模块的module.require命令,而后者又调用Node的内部命令Module._load

Module._load = function(request, parent, isMain) {
  // 1. 检查 Module._cache,是否缓存之中有指定模块
  // 2. 如果缓存之中没有,就创建一个新的Module实例
  // 3. 将它保存到缓存
  // 4. 使用 module.load() 加载指定的模块文件,
  //    读取文件内容之后,使用 module.compile() 执行文件代码
  // 5. 如果加载/解析过程报错,就从缓存删除该模块
  // 6. 返回该模块的 module.exports
};

module.compile()逻辑:

Module.prototype._compile = function(content, filename) {
  // 1. 生成一个require函数,指向module.require
  // 2. 加载其他辅助方法到require
  // 3. 将文件内容放到一个函数之中,该函数可调用 require
  // 4. 执行该函数
};

一旦require函数准备完毕,整个所要加载的脚本内容,就被放到一个新的函数之中,这样可以避免污染全局环境。

(function (exports, require, module, __filename, __dirname) {
  // YOUR CODE INJECTED HERE!
});

参考:
https://www.infoq.cn/article/nodejs-module-mechanism
http://nodejs.cn/api/modules.html#modules_modules
http://javascript.ruanyifeng.com/nodejs/module.html

热更新

热更新就是不重启程序的情况下,通过替换模块达到更新程序的过程。

在node中,require模块,若模块已经存在缓存中,则直接返回缓存中的模块。 可通过require.cache查看已缓存的模块。

// c.js
console.log(require.cache)

// 打印结果

{ 'D:\\code\\c.js':
   Module {
     id: '.',
     exports: {},
     parent: null,
     filename: 'D:\\code\\c.js',
     loaded: false,
     children: [],
     paths: [ 'D:\\code\\node_modules', 'D:\\node_modules' ] } }

一般的热更新思路是监听修改的文件,然后在require.cache中删除这个文件的缓存,最后就是重新require这个文件。

// a.js
module.exports = function () {
  const a = 18;
  console.log("the number is ", a)
}
// b.js
var fs = require('fs');
var a = require('./a.js');

function cleanCache(module){
    var path = require.resolve(module);
    require.cache[path] = null;
}
b();
fs.watch(require.resolve('./a'),function(){
    console.log('change')
    cleanCache(require.resolve('./a'));
    try{
        a = require('./a');
        a();
    }catch(ex){
        console.log('module update failed');
    }
});

如果存在A引用BB引用C,当只删除Brequire.cache缓存,重新读取B中对C的引用数据,C返回的是缓冲中的数据,不会重新读取。

至于浏览器方面的热更新,简单的可以使用同样的思路配合websocket发送更新信息到浏览器实现立刻刷新。 或是使用Webpack HMR,可以做到保存状态不刷新的热替换,当中原理有点复杂,可以参考:Webpack HMR 原理解析

上下文

一个文件就是一个模块,模块包裹在函数里执行,有自己独立的作用域。 可以通过global定义全局变量。

globalVal = 1

// ========

'use strict';
globalVar = 1 // 报错
global.globalVar = 1 // 正常

一般情况下不会污染全局变量,但如果有未经定义的全局变量,可能会产生污染。使用use strict严格模式会抛出错误,从而避免这个问题。

参考:https://www.zhihu.com/question/57375179/answer/152633354

包管理

锁版本

在开发中锁定模块版本,可以保证产品的稳定性与开发环境的一致性。

锁定版本的模块在package.json中写明版本号,不添加其他标记符号。

"dependencies": {
    "vue": "2.2.0"
}

如果添加符号~或者^,代表模块安装更新的版本范围。

~1.2.3  等价于 >=1.2.3 <1.(2+1).0 := >=1.2.3 <1.3.0
~1.2    等价于 >=1.2.0 <1.(2+1).0 := >=1.2.0 <1.3.0 (Same as 1.2.x)
~1      等价于 >=1.0.0 <(1+1).0.0 := >=1.0.0 <2.0.0 (Same as 1.x)
~0.2.3  等价于 >=0.2.3 <0.(2+1).0 := >=0.2.3 <0.3.0
~0.2    等价于 >=0.2.0 <0.(2+1).0 := >=0.2.0 <0.3.0 (Same as 0.2.x)
~0      等价于 >=0.0.0 <(0+1).0.0 := >=0.0.0 <1.0.0 (Same as 0.x)

^1.2.3  等价于 >=1.2.3 <2.0.0
^0.2.3  等价于 >=0.2.3 <0.3.0
^0.0.3  等价于 >=0.0.3 <0.0.4

*       等价于 >=0.0.0 (Any version satisfies)
1.x     等价于 >=1.0.0 <2.0.0 (Matching major version)
1.2.x   等价于 >=1.2.0 <1.3.0 (Matching major and minor versions)

参考:https://github.com/npm/node-semver#ranges

yarn add package-name@1.2.3

可以通过yarn安装指定版本模块,模块版本就会被锁定。这个操作可能随着yarn的版本更迭而失效,但可手动去掉版本号前的符号达到锁版本。

--save-exact/-E参数强制npm在package.json中写死固定的版本号,而不使用如~^这类的范围符号。

关于是否锁版本的意见:https://zhuanlan.zhihu.com/p/22934066

Node.js 基础

事件/异步

事件/异步

promise

promise基础用法可以建议优先阅读阮一峰的文章

Event Loop

浏览器的Event loop和Node的Event loop是两个概念,下面主要讲Node方面的Event loop

Node使用libuv引擎实现事件循环,它的实现比浏览器更加复杂,而且会跟内核交互。

官方相关说明文章:https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/
翻译:https://nodejs.org/zh-cn/docs/guides/event-loop-timers-and-nexttick/

Event loop 的操作顺序:

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

node中的事件循环的顺序:

外部输入数据-->轮询阶段(poll)-->检查阶段(check)-->关闭事件回调阶段(close callback)-->定时器检测阶段(timer)-->I/O事件回调阶段(I/O callbacks)-->闲置阶段(idle, prepare)-->轮询阶段...

每个阶段都有一个先进先出的回调函数队列。只有一个阶段的回调函数队列清空了,该执行的回调函数都执行了,事件循环才会进入下一个阶段。

各阶段的含义:

(1)timers

这个是定时器阶段,处理setTimeout()setInterval()的回调函数。进入这个阶段后,主线程会检查一下当前时间,是否满足定时器的条件。如果满足就执行回调函数,否则就离开这个阶段。

(2)I/O callbacks

除了以下操作的回调函数,其他的回调函数都在这个阶段执行。

 - setTimeout()和setInterval()的回调函数
 - setImmediate()的回调函数
 - 用于关闭请求的回调函数,比如socket.on('close', ...)

(3)idle, prepare

该阶段只供 libuv 内部调用,这里可以忽略。

(4)Poll

这个阶段是轮询时间,用于等待还未返回的 I/O 事件,比如服务器的回应、用户移动鼠标等等。

这个阶段的时间会比较长。如果没有其他异步任务要处理(比如到期的定时器),会一直停留在这个阶段,等待 I/O 请求返回结果。

(5)check

该阶段执行setImmediate()的回调函数。

(6)close callbacks

该阶段执行关闭请求的回调函数,比如socket.on('close', ...)


process.nextTick()并不是event loop的一部分。相反的,process.nextTick()会把回调塞入nextTickQueue,nextTickQueue将在当前阶段操作完成后执行,不管目前处于event loop的哪个阶段。


参考:
http://www.ruanyifeng.com/blog/2018/02/node-event-loop.html
https://github.com/creeperyang/blog/issues/26

Macrotask 和 Microtask

Microtask

  1. process.nextTick
  2. promises
  3. Object.observe(废弃API)
  4. MutationObserver(监听DOM change)

Macrotask

  1. setTimeout
  2. setInterval
  3. setImmediate
  4. I/O
  5. UI rendering
  6. requestAnimationFrame

按照WHATWG 规范,每一次事件循环(one cycle of the event loop),只处理一个 (macro)task。待该 macrotask 完成后,所有的 microtask 会在同一次循环中处理。处理这些 microtask 时,还可以将更多的 microtask 入队,它们会一一执行,直到整个 microtask 队列处理完。

参考:https://github.com/ccforward/cc/issues/47

timers

timer模块是一个全局的 API,调度函数不需要使用require引入。

相关API可以查看官网文档:http://nodejs.cn/api/timers.html

阻塞异步

同步或者异步是有关消息通信机制的概念。同步机制,是指发送方发送请求后,需要等待接收到接收方发回的响应后,才接着发送下一个请求;异步机制,和同步机制正好相反,在异步机制中,发送方发出一个请求后,不等待接收方响应这个请求,就继续发送下个请求。

阻塞非阻塞用来描述进程处理调用的方式。阻塞调用方式是调用结果返回之前,当前线程从运行状态被挂起,一直等到调用结果返回之后在继续执行。在非阻塞方式中,如果调用结果不能马上返回当前线程也不会被挂起,而是立即返回执行下一个调用。

在Node.js中,阻塞 是指程序中,JavaScript 语句的执行,必须等待一个非 JavaScript(IO) 操作完成。这是因为当 阻塞发生时,事件循环无法继续运行JavaScript。

const fs = require('fs');
const data = fs.readFileSync('/file.md'); // 在这里阻塞直到文件被读取

阻塞方法同步执行,非阻塞方法异步执行。一般不使用同步非阻塞异步阻塞

参考:
https://nodejs.org/zh-cn/docs/guides/blocking-vs-non-blocking/#
https://www.zhihu.com/question/19732473

并行并发

并行 (Parallel) 与并发 (Concurrent)

并发 (Concurrent) = 2 队列对应 1 咖啡机.
并行 (Parallel) = 2 队列对应 2 咖啡机.

并发,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。

并行,操作系统中是指,一组程序按独立异步的速度执行,无论从微观还是宏观,程序都是一起执行的。

并发性是指在一段时间内宏观上有多个程序在同时运行,但在单处理机系统中,每一时刻却仅能有一道程序执行,故微观上这些程序只能是分时地交替执行。
倘若在计算机系统中有多个处理机,则这些可以并发执行的程序便可被分配到多个处理机上,实现并行执行,即利用每个处理机来处理一个可并发执行的程序,这样多个程序便可以同时执行。

参考:
https://baike.baidu.com/item/%E5%B9%B6%E5%8F%91#4
https://baike.baidu.com/item/%E5%B9%B6%E8%A1%8C/5806759#reference-[1]-8050484-wrap https://www.zhihu.com/question/33515481

Cluster

nodejs是单进程单线程,在多核机器上无法充分利用性能。nodejs引入cluster模块,可以提供多进程编程力能。

cluster模块通过cluster.fork()方法创建多个进程实例,而cluster.fork()内部又是通过child_process.fork()来创建子进程的。

cluster.fork() --> child_process.for()

示例:

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  console.log(`主进程 ${process.pid} 正在运行`);

  // 衍生工作进程。
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`工作进程 ${worker.process.pid} 已退出`);
  });
} else {
  // 工作进程可以共享任何 TCP 连接。
  // 在本例子中,共享的是一个 HTTP 服务器。
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('你好世界\n');
  }).listen(8000);

  console.log(`工作进程 ${process.pid} 已启动`);
}

打印:

$ node server.js
主进程 3596 正在运行
工作进程 4324 已启动
工作进程 4520 已启动
工作进程 6056 已启动
工作进程 5644 已启动

参考:http://nodejs.cn/api/cluster.html

Node.js 基础

进程

进程

简述

关于 Process, 我们需要讨论的是两个概念, ①操作系统的进程, ② Node.js 中的 Process 对象. 操作进程对于服务端而言, 好比 html 之于前端一样基础. 想做服务端编程是不可能绕过 Unix/Linux 的. 在 Linux/Unix/Mac 系统中运行 ps -ef 命令可以看到当前系统中运行的进程. 各个参数如下:

列名称 意义
UID 执行该进程的用户ID
PID 进程编号
PPID 该进程的父进程编号
C 该进程所在的CPU利用率
STIME 进程执行时间
TTY 进程相关的终端类型
TIME 进程所占用的CPU时间
CMD 创建该进程的指令

关于进程以及操作系统一些更深入的细节推荐阅读 APUE, 即《Unix 高级编程》等书籍来了解.

Process

这里来讨论 Node.js 中的 process 对象. 直接在代码中通过 console.log(process) 即可打印出来. 可以看到 process 对象暴露了非常多有用的属性以及方法, 具体的细节见官方文档, 已经说的挺详细了. 其中包括但不限于:

process.nextTick

上一节已经提到过 process.nextTick 了, 这是一个你需要了解的, 重要的, 基础方法.

   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘

process.nextTick 并不属于 Event loop 中的某一个阶段, 而是在 Event loop 的每一个阶段结束后, 直接执行 nextTickQueue 中插入的 "Tick", 并且直到整个 Queue 处理完. 所以面试时又有可以问的问题了, 递归调用 process.nextTick 会怎么样? (doge

function test() { 
  process.nextTick(() => test());
}

这种情况与以下情况, 有什么区别? 为什么?

function test() { 
  setTimeout(() => test(), 0);
}

配置

配置是开发部署中一个很常见的问题. 普通的配置有两种方式, 一是定义配置文件, 二是使用环境变量.

node-configuration

你可以通过设置环境变量来指定配置, 然后通过 process.env 来获取配置项. 另外也可以通过读取定义好的配置文件来获取, 在这方面有很多不错的库例如 dotenv, node-config 等, 而在使用这些库来加载配置文件的时候, 通常都会碰到一个当前工作目录的问题.

进程的当前工作目录是什么? 有什么作用?

当前进程启动的目录, 通过 process.cwd() 获取当前工作目录 (current working directory), 通常是命令行启动的时候所在的目录 (也可以在启动时指定), 文件操作等使用相对路径的时候会相对当前工作目录来获取文件.

一些获取配置的第三方模块就是通过你的当前目录来找配置文件的. 所以如果你错误的目录启动脚本, 可能没法得到正确的结果. 在程序中可以通过 process.chdir() 来改变当前的工作目录.

标准流

在 process 对象上还暴露了 process.stderr, process.stdout 以及 process.stdin 三个标准流, 熟悉 C/C++/Java 的同学应该对此比较熟悉. 关于这几个流, 常见的面试问题是问 console.log 是同步还是异步? 如何实现一个 console.log?

如果简历中有出现 C/C++ 关键字, 一般都会问到如何实现一个同步的输入 (类似实现C语言的 scanf, C++ 的 cin, Python 的 raw_input 等).

维护方面

熟悉与进程有关的基础命令, 如 top, ps, pstree 等命令.

Child Process

子进程 (Child Process) 是进程中一个重要的概念. 你可以通过 Node.js 的 child_process 模块来执行可执行文件, 调用命令行命令, 比如其他语言的程序等. 也可以通过该模块来将 .js 代码以子进程的方式启动. 比较有名的网易的分布式架构 pomelo 就是基于该模块 (而不是 cluster) 来实现多进程分布式架构的.

child_process.fork 与 POSIX 的 fork 有什么区别?

Node.js 的 child_process.fork() 在 Unix 上的实现最终调用了 POSIX fork(2), 而 POSIX 的 fork 需要手动管理子进程的资源释放 (waitpid), child_process.fork 则不用关心这个问题, Node.js 会自动释放, 并且可以在 option 中选择父进程死后是否允许子进程存活.

其中 exec/execSync 方法会直接调用 bash 来解释命令, 所以如果有命令有外部参数, 则需要注意被注入的情况.

child.kill 与 child.send

常见会问的面试题, 如 child.killchild.send 的区别. 二者一个是基于信号系统, 一个是基于 IPC.

父进程或子进程的死亡是否会影响对方? 什么是孤儿进程?

子进程死亡不会影响父进程, 不过子进程死亡时(线程组的最后一个线程,通常是“领头”线程死亡时),会向它的父进程发送死亡信号. 反之父进程死亡, 一般情况下子进程也会随之死亡, 但如果此时子进程处于可运行态、僵死状态等等的话, 子进程将被进程1(init 进程)收养,从而成为孤儿进程. 另外, 子进程死亡的时候(处于“终止状态”),父进程没有及时调用 wait()waitpid() 来返回死亡进程的相关信息,此时子进程还有一个 PCB 残留在进程表中,被称作僵尸进程.

Cluster

Cluster 是常见的 Node.js 利用多核的办法. 它是基于 child_process.fork() 实现的, 所以 cluster 产生的进程之间是通过 IPC 来通信的, 并且它也没有拷贝父进程的空间, 而是通过加入 cluster.isMaster 这个标识, 来区分父进程以及子进程, 达到类似 POSIX 的 fork 的效果.

const cluster = require('cluster');            // | | 
const http = require('http');                  // | | 
const numCPUs = require('os').cpus().length;   // | |    都执行了
                                               // | | 
if (cluster.isMaster) {                        // |-|-----------------
  // Fork workers.                             //   | 
  for (var i = 0; i < numCPUs; i++) {          //   | 
    cluster.fork();                            //   | 
  }                                            //   | 仅父进程执行 (a.js)
  cluster.on('exit', (worker) => {             //   | 
    console.log(`${worker.process.pid} died`); //   | 
  });                                          //   |
} else {                                       // |-------------------
  // Workers can share any TCP connection      // | 
  // In this case it is an HTTP server         // | 
  http.createServer((req, res) => {            // | 
    res.writeHead(200);                        // |   仅子进程执行 (b.js)
    res.end('hello world\n');                  // | 
  }).listen(8000);                             // | 
}                                              // |-------------------
                                               // | |
console.log('hello');                          // | |    都执行了

在上述代码中 numCPUs 虽然是全局变量但是, 在父进程中修改它, 子进程中并不会改变, 因为父进程与子进程是完全独立的两个空间. 他们所谓的共有仅仅只是都执行了, 并不是同一份.

你可以把父进程执行的部分当做 a.js, 子进程执行的部分当做 b.js, 你可以把他们想象成是先执行了 node a.js 然后 cluster.fork 了几次, 就执行了几次 node b.js. 而 cluster 模块则是二者之间的一个桥梁, 你可以通过 cluster 提供的方法, 让其二者之间进行沟通交流.

How It Works

worker 进程是由 child_process.fork() 方法创建的, 所以可以通过 IPC 在主进程和子进程之间相互传递服务器句柄.

cluster 模块提供了两种分发连接的方式.

第一种方式 (默认方式, 不适用于 windows), 通过时间片轮转法(round-robin)分发连接. 主进程监听端口, 接收到新连接之后, 通过时间片轮转法来决定将接收到的客户端的 socket 句柄传递给指定的 worker 处理. 至于每个连接由哪个 worker 来处理, 完全由内置的循环算法决定.

第二种方式是由主进程创建 socket 监听端口后, 将 socket 句柄直接分发给相应的 worker, 然后当连接进来时, 就直接由相应的 worker 来接收连接并处理.

使用第二种方式时理论上性能应该较高, 然后时间上存在负载不均衡的问题, 比如通常 70% 的连接仅被 8 个进程中的 2 个处理, 而其他进程比较清闲.

进程间通信

IPC (Inter-process communication) 进程间通信技术. 常见的进程间通信技术列表如下:

类型 无连接 可靠 流控制 优先级
普通PIPE N Y Y N
命名PIPE N Y Y N
消息队列 N Y Y N
信号量 N Y Y Y
共享存储 N Y Y Y
UNIX流SOCKET N Y Y N
UNIX数据包SOCKET Y Y N N

Node.js 中的 IPC 通信是由 libuv 通过管道技术实现的, 在 windows 下由命名管道(named pipe)实现也就是上表中的最后第二个, *nix 系统则采用 UDS (Unix Domain Socket) 实现.

普通的 socket 是为网络通讯设计的, 而网络本身是不可靠的, 而为 IPC 设计的 socket 则不然, 因为默认本地的网络环境是可靠的, 所以可以简化大量不必要的 encode/decode 以及计算校验等, 得到效率更高的 UDS 通信.

如果了解 Node.js 的 IPC 的话, 可以问个比较有意思的问题

在 IPC 通道建立之前, 父进程与子进程是怎么通信的? 如果没有通信, 那 IPC 是怎么建立的?

这个问题也挺简单, 只是个思路的问题. 在通过 child_process 建立子进程的时候, 是可以指定子进程的 env (环境变量) 的. 所以 Node.js 在启动子进程的时候, 主进程先建立 IPC 频道, 然后将 IPC 频道的 fd (文件描述符) 通过环境变量 (NODE_CHANNEL_FD) 的方式传递给子进程, 然后子进程通过 fd 连上 IPC 与父进程建立连接.

最后于进程间通信 (IPC) 的问题, 一般不会直接问 IPC 的实现, 而是会问什么情况下需要 IPC, 以及使用 IPC 处理过什么业务场景等.

守护进程

最后的守护进程, 是服务端方面一个很基础的概念了. 很多人可能只知道通过 pm2 之类的工具可以将进程以守护进程的方式启动, 却不了解什么是守护进程, 为什么要用守护进程. 对于水平好的同学, 我们是希望能了解守护进程的实现的.

普通的进程, 在用户退出终端之后就会直接关闭. 通过 & 启动到后台的进程, 之后会由于会话(session组)被回收而终止进程. 守护进程是不依赖终端(tty)的进程, 不会因为用户退出终端而停止运行的进程.

// 守护进程实现 (C语言版本)
void init_daemon()
{
    pid_t pid;
    int i = 0;

    if ((pid = fork()) == -1) {
        printf("Fork error !\n");
        exit(1);
    }

    if (pid != 0) {
        exit(0);        // 父进程退出
    }

    setsid();           // 子进程开启新会话, 并成为会话首进程和组长进程
    if ((pid = fork()) == -1) {
        printf("Fork error !\n");
        exit(-1);
    }
    if (pid != 0) {
        exit(0);        // 结束第一子进程, 第二子进程不再是会话首进程
                        // 避免当前会话组重新与tty连接
    }
    chdir("/tmp");      // 改变工作目录
    umask(0);           // 重设文件掩码
    for (; i < getdtablesize(); ++i) {
       close(i);        // 关闭打开的文件描述符
    }

    return;
}

Node.js 编写守护进程

Node.js 基础

IO

IO

简述

Node.js 是以 IO 密集型业务著称. 那么问题来了, 你真的了解什么叫 IO, 什么又叫 IO 密集型业务吗?

Buffer

Buffer 是 Node.js 中用于处理二进制数据的类, 其中与 IO 相关的操作 (网络/文件等) 均基于 Buffer. Buffer 类的实例非常类似整数数组, 但其大小是固定不变的, 并且其内存在 V8 堆栈外分配原始内存空间. Buffer 类的实例创建之后, 其所占用的内存大小就不能再进行调整.

在 Node.js v6.x 之后 new Buffer() 接口开始被废弃, 理由是参数类型不同会返回不同类型的 Buffer 对象, 所以当开发者没有正确校验参数或没有正确初始化 Buffer 对象的内容时, 以及不了解的情况下初始化 就会在不经意间向代码中引入安全性和可靠性问题.

接口 用途
Buffer.from() 根据已有数据生成一个 Buffer 对象
Buffer.alloc() 创建一个初始化后的 Buffer 对象
Buffer.allocUnsafe() 创建一个未初始化的 Buffer 对象

TypedArray

Node.js 的 Buffer 在 ES6 增加了 TypedArray 类型之后, 修改了原来的 Buffer 的实现, 选择基于 TypedArray 中 Uint8Array 来实现, 从而提升了一波性能.

使用上, 你需要了解如下情况:

const arr = new Uint16Array(2);
arr[0] = 5000;
arr[1] = 4000;

const buf1 = Buffer.from(arr); // 拷贝了该 buffer
const buf2 = Buffer.from(arr.buffer); // 与该数组共享了内存

console.log(buf1);
// 输出: <Buffer 88 a0>, 拷贝的 buffer 只有两个元素
console.log(buf2);
// 输出: <Buffer 88 13 a0 0f>

arr[1] = 6000;
console.log(buf1);
// 输出: <Buffer 88 a0>
console.log(buf2);
// 输出: <Buffer 88 13 70 17>

String Decoder

字符串解码器 (String Decoder) 是一个用于将 Buffer 拿来 decode 到 string 的模块, 是作为 Buffer.toString 的一个补充, 它支持多字节 UTF-8 和 UTF-16 字符. 例如

const StringDecoder = require('string_decoder').StringDecoder;
const decoder = new StringDecoder('utf8');

const cent = Buffer.from([0xC2, 0xA2]);
console.log(decoder.write(cent)); // ¢

const euro = Buffer.from([0xE2, 0x82, 0xAC]);
console.log(decoder.write(euro)); // €

stringDecoder.write 会确保返回的字符串不包含 Buffer 末尾残缺的多字节字符,残缺的多字节字符会被保存在一个内部的 buffer 中用于下次调用 stringDecoder.write() 或 stringDecoder.end()。

const StringDecoder = require('string_decoder').StringDecoder;
const decoder = new StringDecoder('utf8');

decoder.write(Buffer.from([0xE2]));
decoder.write(Buffer.from([0x82]));
console.log(decoder.end(Buffer.from([0xAC])));  // €

Stream

Node.js 内置的 stream 模块是多个核心模块的基础. 但是流 (stream) 是一种很早之前流行的编程方式. 可以用大家比较熟悉的 C语言来看这种流式操作:


int copy(const char *src, const char *dest)
{
    FILE *fpSrc, *fpDest;
    char buf[BUF_SIZE] = {0};
    int lenSrc, lenDest;

    // 打开要 src 的文件
    if ((fpSrc = fopen(src, "r")) == NULL)
    {
        printf("文件 '%s' 无法打开\n", src);
        return FAILURE;
    }

    // 打开 dest 的文件
    if ((fpDest = fopen(dest, "w")) == NULL)
    {
        printf("文件 '%s' 无法打开\n", dest);
        fclose(fpSrc);
        return FAILURE;
    }
    
    // 从 src 中读取 BUF_SIZE 长的数据到 buf 中
    while ((lenSrc = fread(buf, 1, BUF_SIZE, fpSrc)) > 0)
    {
        // 将 buf 中的数据写入 dest 中
        if ((lenDest = fwrite(buf, 1, lenSrc, fpDest)) != lenSrc)
        {
            printf("写入文件 '%s' 失败\n", dest);
            fclose(fpSrc);
            fclose(fpDest);
            return FAILURE;
        }
        // 写入成功后清空 buf
        memset(buf, 0, BUF_SIZE);
    }
  
    // 关闭文件
    fclose(fpSrc);
    fclose(fpDest);
    return SUCCESS;
}

应用的场景很简单, 你要拷贝一个 20G 大的文件, 如果你一次性将 20G 的数据读入到内存, 你的内存条可能不够用, 或者严重影响性能. 但是你如果使用一个 1MB 大小的缓存 (buf) 每次读取 1Mb, 然后写入 1Mb, 那么不论这个文件多大都只会占用 1Mb 的内存.

而在 Node.js 中, 原理与上述 C 代码类似, 不过在读写的实现上通过 libuv 与 EventEmitter 加上了异步的特性. 在 linux/unix 中你可以通过 | 来感受到流式操作.

Stream 的类型

使用场景 重写方法
Readable 只读 _read
Writable 只写 _write
Duplex 读写 _read, _write
Transform 操作被写入数据, 然后读出结果 _transform, _flush

对象模式

通过 Node API 创建的流, 只能够对字符串或者 buffer 对象进行操作. 但其实流的实现是可以基于其他的 JavaScript 类型(除了 null, 它在流中有特殊的含义)的. 这样的流就处在 "对象模式(objectMode)" 中. 在创建流对象的时候, 可以通过提供 objectMode 参数来生成对象模式的流. 试图将现有的流转换为对象模式是不安全的.

缓冲区

Node.js 中 stream 的缓冲区, 以开头的 C语言 拷贝文件的代码为模板讨论, (抛开异步的区别看) 则是从 src 中读出数据到 buf 中后, 并没有直接写入 dest 中, 而是先放在一个比较大的缓冲区中, 等待写入(消费) dest 中. 即, 在缓冲区的帮助下可以使读与写的过程分离.

Readable 和 Writable 流都会将数据储存在内部的缓冲区中. 缓冲区可以分别通过 writable._writableState.getBuffer()readable._readableState.buffer 来访问. 缓冲区的大小, 由构造 stream 时候的 highWaterMark 标志指定可容纳的 byte 大小, 对于 objectMode 的 stream, 该标志表示可以容纳的对象个数.

可读流

当一个可读实例调用 stream.push() 方法的时候, 数据将会被推入缓冲区. 如果数据没有被消费, 即调用 stream.read() 方法读取的话, 那么数据会一直留在缓冲队列中. 当缓冲区中的数据到达 highWaterMark 指定的阈值, 可读流将停止从底层汲取数据, 直到当前缓冲的报备成功消耗为止.

可写流

在一个在可写实例上不停地调用 writable.write(chunk) 的时候数据会被写入可写流的缓冲区. 如果当前缓冲区的缓冲的数据量低于 highWaterMark 设定的值, 调用 writable.write() 方法会返回 true (表示数据已经写入缓冲区), 否则当缓冲的数据量达到了阈值, 数据无法写入缓冲区 write 方法会返回 false, 直到 drain 事件触发之后才能继续调用 write 写入.

// Write the data to the supplied writable stream one million times.
// Be attentive to back-pressure.
function writeOneMillionTimes(writer, data, encoding, callback) {
  let i = 1000000;
  write();
  function write() {
    var ok = true;
    do {
      i--;
      if (i === 0) {
        // last time!
        writer.write(data, encoding, callback);
      } else {
        // see if we should continue, or wait
        // don't pass the callback, because we're not done yet.
        ok = writer.write(data, encoding);
      }
    } while (i > 0 && ok);
    if (i > 0) {
      // had to stop early!
      // write some more once it drains
      writer.once('drain', write);
    }
  }
}

Duplex 与 Transform

Duplex 流和 Transform 流都是同时可读写的, 他们会在内部维持两个缓冲区, 分别对应读取和写入, 这样就可以允许两边同时独立操作, 维持高效的数据流. 比如说 net.Socket 是一个 Duplex 流, Readable 端允许从 socket 获取、消耗数据, Writable 端允许向 socket 写入数据. 数据写入的速度很有可能与消耗的速度有差距, 所以两端可以独立操作和缓冲是很重要的.

pipe

stream 的 .pipe(), 将一个可写流附到可读流上, 同时将可写流切换到流模式, 并把所有数据推给可写流. 在 pipe 传递数据的过程中, objectMode 是传递引用, 非 objectMode 则是拷贝一份数据传递下去.

pipe 方法最主要的目的就是将数据的流动缓冲到一个可接受的水平, 不让不同速度的数据源之间的差异导致内存被占满. 关于 pipe 的实现参见 David Cai 的 通过源码解析 Node.js 中导流(pipe)的实现

Console

console.log 同步还是异步取决于与谁相连和os. 不过一般情况下的实现都是如下 (6.x 源代码),其中this._stdout默认是process.stdout:

// As of v8 5.0.71.32, the combination of rest param, template string
// and .apply(null, args) benchmarks consistently faster than using
// the spread operator when calling util.format.
Console.prototype.log = function(...args) {
  this._stdout.write(`${util.format.apply(null, args)}\n`);
};

自己实现一个 console.log 可以参考如下代码:

let print = (str) => process.stdout.write(str + '\n');

print('hello world');

注意: 该代码并没有处理多参数, 也没有处理占位符 (即 util.format 的功能).

console.log.bind(console) 问题

// 源码出处 https://github.com/nodejs/node/blob/v6.x/lib/console.js
function Console(stdout, stderr) {
  // ... init ...

  // bind the prototype functions to this Console instance
  var keys = Object.keys(Console.prototype);
  for (var v = 0; v < keys.length; v++) {
    var k = keys[v];
    this[k] = this[k].bind(this);
  }
}

File

“一切皆是文件”是 Unix/Linux 的基本哲学之一, 不仅普通的文件、目录、字符设备、块设备、套接字等在 Unix/Linux 中都是以文件被对待, 也就是说这些资源的操作对象均为 fd (文件描述符), 都可以通过同一套 system call 来读写. 在 linux 中你可以通过 ulimit 来对 fd 资源进行一定程度的管理限制.

Node.js 封装了标准 POSIX 文件 I/O 操作的集合. 通过 require('fs') 可以加载该模块. 该模块中的所有方法都有异步执行和同步执行两个版本. 你可以通过 fs.open 获得一个文件的文件描述符.

编码

// TODO

UTF8, GBK, es6 中对编码的支持, 如何计算一个汉字的长度

BOM

stdio

stdio (standard input output) 标准的输入输出流, 即输入流 (stdin), 输出流 (stdout), 错误流 (stderr) 三者. 在 Node.js 中分别对应 process.stdin (Readable), process.stdout (Writable) 以及 process.stderr (Writable) 三个 stream.

输出函数是每个人在学习任何一门编程语言时所需要学到的第一个函数. 例如 C语言的 printf("hello, world!"); python/ruby 的 print 'hello, world!' 以及 JavaScript 中的 console.log('hello, world!');

以 C语言的伪代码来看的话, 这类输出函数的实现思路如下:

int printf(FILE *stream, 要打印的内容)
{
  // ...

  // 1. 申请一个临时内存空间
  char *s = malloc(4096);

  // 2. 处理好要打印的的内容, 其值存储在 s 中
  //      ...

  // 3. 将 s 上的内容写入到 stream 中
  fwrite(s, stream);

  // 4. 释放临时空间
  free(s);

  // ...
}

我们需要了解的是第 3 步, 其中的 stream 则是指 stdout (输出流). 实际上在 shell 上运行一个应用程序的时候, shell 做的第一个操作是 fork 当前 shell 的进程 (所以, 如果你通过 ps 去查看你从 shell 上启动的进程, 其父进程 pid 就是当前 shell 的 pid), 在这个过程中也把 shell 的 stdio 继承给了你当前的应用进程, 所以你在当前进程里面将数据写入到 stdout, 也就是写入到了 shell 的 stdout, 即在当前 shell 上显示了.

输入也是同理, 当前进程继承了 shell 的 stdin, 所以当你从 stdin 中读取数据时, 其实就获取到你在 shell 上输入的数据. (PS: shell 可以是 windows 下的 cmd, powershell, 也可以是 linux 下 bash 或者 zsh 等)

当你使用 ssh 在远程服务器上运行一个命令的时候, 在服务器上的命令输出虽然也是写入到服务器上 shell 的 stdout, 但是这个远程的 shell 是从 sshd 服务上 fork 出来的, 其 stdout 是继承自 sshd 的一个 fd, 这个 fd 其实是个 socket, 所以最终其实是写入到了一个 socket 中, 通过这个 socket 传输你本地的计算机上的 shell 的 stdout.

如果你理解了上述情况, 那么你也就能理解为什么守护进程需要关闭 stdio, 如果切到后台的守护进程没有关闭 stdio 的话, 那么你在用 shell 操作的过程中, 屏幕上会莫名其妙的多出来一些输出. 此处对应守护进程的 C 实现中的这一段:

for (; i < getdtablesize(); ++i) {
   close(i);  // 关闭打开的 fd
}

Linux/unix 的 fd 都被设计为整型数字, 从 0 开始. 你可以尝试运行如下代码查看.

console.log(process.stdin.fd); // 0
console.log(process.stdout.fd); // 1
console.log(process.stderr.fd); // 2

在上一节中的 在 IPC 通道建立之前, 父进程与子进程是怎么通信的? 如果没有通信, 那 IPC 是怎么建立的? 中使用环境变量传递 fd 的方法, 这么看起来就很直白了, 因为传递 fd 其实是直接传递了一个整型数字.

如何同步的获取用户的输入?

如果你理解了上述的内容, 那么放到 Node.js 中来看, 获取用户的输入其实就是读取 Node.js 进程中的输入流 (即 process.stdin 这个 stream) 的数据.

而要同步读取, 则是不用异步的 read 接口, 而是用同步的 readSync 接口去读取 stdin 的数据即可实现. 以下来自万能的 stackoverflow:

/*
 * http://stackoverflow.com/questions/3430939/node-js-readsync-from-stdin
 * @mklement0
 */
var fs = require('fs');

var BUFSIZE = 256;
var buf = new Buffer(BUFSIZE);
var bytesRead;

module.exports = function() {
  var fd = ('win32' === process.platform) ? process.stdin.fd : fs.openSync('/dev/stdin', 'rs');
  bytesRead = 0;

  try {
    bytesRead = fs.readSync(fd, buf, 0, BUFSIZE);
  } catch (e) {
    if (e.code === 'EAGAIN') { // 'resource temporarily unavailable'
      // Happens on OS X 10.8.3 (not Windows 7!), if there's no
      // stdin input - typically when invoking a script without any
      // input (for interactive stdin input).
      // If you were to just continue, you'd create a tight loop.
      console.error('ERROR: interactive stdin input not supported.');
      process.exit(1);
    } else if (e.code === 'EOF') {
      // Happens on Windows 7, but not OS X 10.8.3:
      // simply signals the end of *piped* stdin input.
      return '';
    }
    throw e; // unexpected exception
  }

  if (bytesRead === 0) {
    // No more stdin input available.
    // OS X 10.8.3: regardless of input method, this is how the end 
    //   of input is signaled.
    // Windows 7: this is how the end of input is signaled for
    //   *interactive* stdin input.
    return '';
  }
  // Process the chunk read.

  var content = buf.toString(null, 0, bytesRead - 1);

  return content;
};

Readline

readline 模块提供了一个用于从 Readble 的 stream (例如 process.stdin) 中一次读取一行的接口. 当然你也可以用来读取文件或者 net, http 的 stream, 比如:

const readline = require('readline');
const fs = require('fs');

const rl = readline.createInterface({
  input: fs.createReadStream('sample.txt')
});

rl.on('line', (line) => {
  console.log(`Line from file: ${line}`);
});

实现上, realine 在读取 TTY 的数据时, 是通过 input.on('keypress', onkeypress) 时发现用户按下了回车键来判断是新的 line 的, 而读取一般的 stream 时, 则是通过缓存数据然后用正则 .test 来判断是否为 new line 的.

PS: 打个广告, 如果在编写脚本时, 不习惯这样异步获取输入, 想要同步获取同步的用户输入可以看一看这个 Node.js 版本类 C语言使用的 scanf 模块 (支持 ts).

REPL

Read-Eval-Print-Loop (REPL)

整理中

Node.js 基础

Network

Network

Net

目前互联化的核心是建立在 TCP/IP 协议的基础上的, 这些协议将数据分割成小的数据包进行传输, 并且解决传输过程中各种各样复杂的问题. 关于协议的具体细节推荐阅读 W.Richard Stevens 的《TCP/IP 详解 卷1:协议》, 本文不做赘述, 只是列举一些常见的知识点, 新人推荐看《图解TCP/IP》, 抓包工具推荐看《Wireshark网络分析就这么简单》.

粘包

默认情况下, TCP 连接会启用延迟传送算法 (Nagle 算法), 在数据发送之前缓存他们. 如果短时间有多个数据发送, 会缓冲到一起作一次发送 (缓冲大小见 socket.bufferSize), 这样可以减少 IO 消耗提高性能.

如果是传输文件的话, 那么根本不用处理粘包的问题, 来一个包拼一个包就好了. 但是如果是多条消息, 或者是别的用途的数据那么就需要处理粘包.

可以参见网上流传比较广的一个例子, 连续调用两次 send 分别发送两段数据 data1 和 data2, 在接收端有以下几种常见的情况:

其中的 BCD 就是我们常见的粘包的情况. 而对于处理粘包的问题, 常见的解决方案有:

方案1

只需要等上一段时间再进行下一次 send 就好, 适用于交互频率特别低的场景. 缺点也很明显, 对于比较频繁的场景而言传输效率实在太低. 不过几乎不用做什么处理.

方案2

关闭 Nagle 算法, 在 Node.js 中你可以通过 socket.setNoDelay() 方法来关闭 Nagle 算法, 让每一次 send 都不缓冲直接发送.

该方法比较适用于每次发送的数据都比较大 (但不是文件那么大), 并且频率不是特别高的场景. 如果是每次发送的数据量比较小, 并且频率特别高的, 关闭 Nagle 纯属自废武功.

另外, 该方法不适用于网络较差的情况, 因为 Nagle 算法是在服务端进行的包合并情况, 但是如果短时间内客户端的网络情况不好, 或者应用层由于某些原因不能及时将 TCP 的数据 recv, 就会造成多个包在客户端缓冲从而粘包的情况. (如果是在稳定的机房内部通信那么这个概率是比较小可以选择忽略的)

方案3

封包/拆包是目前业内常见的解决方案了. 即给每个数据包在发送之前, 于其前/后放一些有特征的数据, 然后收到数据的时候根据特征数据分割出来各个数据包.

可靠传输

为每一个发送的数据包分配一个序列号(SYN, Synchronize packet), 每一个包在对方收到后要返回一个对应的应答数据包(ACK, Acknowledgement),. 发送方如果发现某个包没有被对方 ACK, 则会选择重发. 接收方通过 SYN 序号来保证数据的不会乱序(reordering), 发送方通过 ACK 来保证数据不缺漏, 以此参考决定是否重传. 关于具体的序号计算, 丢包时的重传机制等可以参见阅读陈皓的 《TCP的那些事儿(上)》 此处不做赘述.

window

TCP 头里有一个 Window 字段, 是接收端告诉发送端自己还有多少缓冲区可以接收数据的. 发送端就可以根据接收端的处理能力来发送数据, 从而避免接收端处理不过来. 详细参见陈皓的 《TCP的那些事儿(下)》

window 是否设置的越大越好?

类似木桶理论, 一个木桶能装多少水, 是由最短的那块木板决定的. 一个 TCP 连接的 window 是由该连接中间一连串设备中 window 最小的那一个设备决定的.

backlog

图片出处 http://www.cnxct.com/something-about-phpfpm-s-backlog/

关于该 backlog 的定义参见 man 手册:

The behavior of the backlog argument on TCP sockets changed with Linux 2.2. Now it specifies the queue length for completely established sockets waiting to be accepted, instead of the number of incomplete connection requests.

backlog 用于设置客户端与服务端 ESTABLISHED 之后等待 accept 的队列长图 (如上图中的 accept queue). 如果 backlog 过小, 在并发连接大的情况下容易导致 accept queue 装满之后断开连接. 但是如果将这个队列设置的特别大, 那么假定连接数并发量是 65525, 以 php-fpm 的 qps 5000 为例, 处理完约耗时 13s, 而这段时间中连接可能早已被 nginx 或者客户端断开, 那么我们去 accept 这个 socket 时只会拿到一个 broken pipe (该例子出处见 PHP 源码 Set FPM_BACKLOG_DEFAULT to 511). 经过我也不懂的计算 backlog 的长度默认是 511.

另外提一句, 这个 backlog 是通过系统指定时是通过 somaxconn 参数来指定 accept queue 的. 而 tcp_max_syn_backlog 参数指定的是 SYN queue 的长度.

状态机

tcpfsm.png

关于网络连接的建立以及断开, 存在着一个复杂的状态转换机制, 完整的状态表参见 《The TCP/IP Guide》

state 简述
CLOSED 连接关闭, 所有连接的初始状态
LISTEN 监听状态, 等待客户端发送 SYN
SYN-SENT 客户端发送了 SYN, 等待服务端回复
SYN-RECEIVED 双方都收到了 SYN, 等待 ACK
ESTABLISHED SYN-RECEIVED 收到 ACK 之后, 状态切换为连接已建立.
CLOSE-WAIT 被动方收到了关闭请求(FIN)后, 发送 ACK, 如果有数据要发送, 则发送数据, 无数据发送则回复 FIN. 状态切换到 LAST-ACK
LAST-ACK 等待对方 ACK 当前设备的 CLOSE-WAIT 时发送的 FIN, 等到则切换 CLOSED
FIN-WAIT-1 主动方发送 FIN, 等待 ACK
FIN-WAIT-2 主动方收到被动方的 ACK, 等待 FIN
CLOSING 主动方收到了FIN, 却没收到 FIN-WAIT-1 时发的 ACK, 此时等待那个 ACK
TIME-WAIT 主动方收到 FIN, 返回收到对方 FIN 的 ACK, 等待对方是否真的收到了 ACK, 如果过一会又来一个 FIN, 表示对方没收到, 这时要再 ACK 一次

TIME_WAIT 是什么情况? 出现过多的 TIME_WAIT 可能是什么原因?

TIME_WAIT 是连接的某一方 (可能是服务端也可能是客户端) 主动断开连接时, 四次挥手等待被断开的一方是否收到最后一次挥手 (ACK) 的状态. 如果在等待时间中, 再次收到第三次挥手 (FIN) 表示对方没收到最后一次挥手, 这时要再 ACK 一次. 这个等待的作用是避免出现连接混用的情况 (prevent potential overlap with new connections see TCP Connection Termination for more).

出现大量的 TIME_WAIT 比较常见的情况是, 并发量大, 服务器在短时间断开了大量连接. 对应 HTTP server 的情况可能是没开启 keepAlive. 如果有开 keepAlive, 一般是等待客户端自己主动断开, 那么TIME_WAIT 就只存在客户端, 而服务端则是 CLOSE_WAIT 的状态, 如果服务端出现大量 CLOSE_WAIT, 意味着当前服务端建立的连接大面积的被断开, 可能是目标服务集群重启之类.

UDP

TCP/UDP 的区别? UDP 有粘包吗?

协议 连接性 双工性 可靠性 有序性 有界性 拥塞控制 传输速度 量级 头部大小
TCP 面向连接
(Connection oriented)
全双工(1:1) 可靠
(重传机制)
有序
(通过SYN排序)
无, 有粘包情况 20~60字节
UDP 无连接
(Connection less)
n:m 不可靠
(丢包后数据丢失)
无序 有消息边界, 无粘包 8字节

UDP socket 支持 n 对 m 的连接状态, 在官方文档中有写到在 dgram.createSocket(options[, callback]) 中的 option 可以指定 reuseAddrSO_REUSEADDR标志. 通过 SO_REUSEADDR 可以简单的实现 n 对 m 的多播特性 (不过仅在支持多播的系统上才有).

常见的应用场景

传输层协议应用应用层协议
TCP电子邮件SMTP
终端连接TELNET
终端连接SSH
万维网HTTP
文件传输FTP
UDP域名解析DNS
简单文件传输TFTP
网络时间校对NTP
网络文件系统NFS
路由选择RIP
IP电话-
流式多媒体通信-

简单的说, UDP 速度快, 开销低, 不用封包/拆包允许丢一部分数据, 监控统计/日志数据上报/流媒体通信等场景都可以用 UDP. 目前 Node.js 的项目中使用 UDP 比较流行的是 StatsD 监控服务.

HTTP

目前世界上运行最良好的分布式集群, 莫过于当前的万维网 (http servers) 了. 目前前端工程师也都是靠 HTTP 协议吃饭的, 所以 2-3 年的前端同学都应该对 HTTP 有比较深的理解了, 所以这里不做太多的赘述. 推荐书籍《图解HTTP》, 博客HTTP 协议入门.

另外最近几年开始大家对 HTTP 的面试的考察也渐渐偏向理解 RESTful 架构. 简单的说, RESTful 是把每个 URI 当做资源 (Resources), 通过 method 作为动词来对资源做不同的动作, 然后服务器返回 status 来得知资源状态的变化 (State Transfer);

method/status

因为 HTTP 的方法 (method) 与状态码 (status) 讲解太常见, 你可以使用如下代码打印出来自己看 Node.js 官方定义的, 完整的就不列举了.

const http = require('http');

console.log(http.METHODS);
console.log(http.STATUS_CODES);

一个常见的 method 列表, 关于这些 method 在 RESTful 中的一些应用的详细可以参见Using HTTP Methods for RESTful Services

methods CRUD 幂等 缓存
GET Read
POST Create
PUT Update/Replace
PATCH Update/Modify
DELETE Delete

GET 和 POST 有什么区别?

网上有很多讲这个的, 比如从书签, url 等前端的角度去看他们的区别这里不赘述. 而从后端的角度看, 前两年出来一个 《GET 和 POST 没有区别》(出处不好考究, 就没贴了) 的文章比较有名, 早在我刚学 PHP 的时候也有过这种疑惑, 刚学 Node 的时候发现不能像 PHP 那样同时处理 GET 和 POST 的时候还很不适应. 后来接触 RESTful 才意识到, 这两个东西最根本的差别是语义, 引申了看, 协议 (protocol) 这种东西就是人与人之间协商的约定, 什么行为是什么作用都是"约定"好的, 而不是强制使用的, 非要把 GET 当 POST 这样不遵守约定的做法我们也爱莫能助.

跑题了, 简而言之, 讨论这二者的区别最好从 RESTful 提倡的语义角度来讲比较符合当代程序员的逼格比较合理.

POST 和 PUT 有什么区别?

POST 是新建 (create) 资源, 非幂等, 同一个请求如果重复 POST 会新建多个资源. PUT 是 Update/Replace, 幂等, 同一个 PUT 请求重复操作会得到同样的结果.

headers

HTTP headers 是在进行 HTTP 请求的交互过程中互相支会对方一些信息的主要字段. 比如请求 (Request) 的时候告诉服务端自己能接受的各项参数, 以及之前就存在本地的一些数据等. 详细各位可以参见 wikipedia:

主要区别在于, session 存在服务端, cookie 存在客户端. session 比 cookie 更安全. 而且 cookie 不一定一直能用 (可能被浏览器关掉). 服务端可以通过设置 cookie 的值为空并设置一个及时的 expires 来清除存在客户端上的 cookie.

什么是跨域请求? 如何允许跨域?

出于安全考虑, 默认情况下使用 XMLHttpRequest 和 Fetch 发起 HTTP 请求必须遵守同源策略, 即只能向相同 host 请求 (host = hostname : port) 注[1]. 向不同 host 的请求被称作跨域请求 (cross-origin HTTP request). 可以通过设置 CORS headersAccess-Control-Allow- 系列来允许跨域. 例如:

location ~* ^/(?:v1|_) {
  if ($request_method = OPTIONS) { return 200 ''; }
  header_filter_by_lua '
    ngx.header["Access-Control-Allow-Origin"] = ngx.var.http_origin; # 这样相当于允许所有来源了
    ngx.header["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, PATCH, OPTIONS";
    ngx.header["Access-Control-Allow-Credentials"] = "true";
    ngx.header["Access-Control-Allow-Headers"] = "Content-Type";
  ';
  proxy_pass http://localhost:3001;
}

注[1]:同源除了相同 host 也包括相同协议. 所以即使 host 相同, 从 HTTP 到 HTTPS 也属于跨域, 见讨论.

Script error. 是什么错误? 如何拿到更详细的信息?

接上题, 由于同源性策略 (CORS), 如果你引用的 js 脚本所在的域与当前域不同, 那么浏览器会把 onError 中的 msg 替换为 Script error. 要拿到详细错误的方法, 处理配好 Access-Control-Allow-Origin 还有在引用脚本的时候指定 crossorigin 例如:

<script src="http://another-domain.com/app.js" crossorigin="anonymous"></script>

详见 JavaScript Script Error.

Agent

Node.js 中的 http.Agent 用于池化 HTTP 客户端请求的 socket (pooling sockets used in HTTP client requests). 也就是复用 HTTP 请求时候的 socket. 如果你没有指定 Agent 的话, 默认用的是 http.globalAgent.

另外, 目前在 Node.js 的 6.8.1(包括)到 6.10(不包括)版本中发现一个问题:

1 和 2 这两种情况下, 一旦设置了 request timeout, 由于 socket 一直未销毁, 如果你在请求完成以后没有注意清除该事件, 会导致事件重复监听, 且该事件闭包引用了 req, 会导致内存泄漏.

如果有疑虑的话可以参见 Node 官方讨论的 issue 以及引入此 bug 的 commit, 如果此处描述有疑问可以在本 repo 的 issue 中指出.

socket hang up

hang up 有挂断的意思, socket hang up 也可以理解为 socket 被挂断. 在 Node.js 中当你要 response 一个请求的时候, 发现该这个 socket 已经被 "挂断", 就会就会报 socket hang up 错误.

Node.js 中源码的情况:

function socketCloseListener() {
  var socket = this;
  var req = socket._httpMessage;

  // Pull through final chunk, if anything is buffered.
  // the ondata function will handle it properly, and this
  // is a no-op if no final chunk remains.
  socket.read();

  // NOTE: It's important to get parser here, because it could be freed by
  // the `socketOnData`.
  var parser = socket.parser;
  req.emit('close');
  if (req.res && req.res.readable) {
    // Socket closed before we emitted 'end' below.
    req.res.emit('aborted');
    var res = req.res;
    res.on('end', function() {
      res.emit('close');
    });
    res.push(null);
  } else if (!req.res && !req.socket._hadError) {
    // This socket error fired before we started to
    // receive a response. The error needs to
    // fire on the request.
    req.emit('error', createHangUpError());  // <------------------- socket hang up
    req.socket._hadError = true;
  }

  // Too bad.  That output wasn't getting written.
  // This is pretty terrible that it doesn't raise an error.
  // Fixed better in v0.10
  if (req.output)
    req.output.length = 0;
  if (req.outputEncodings)
    req.outputEncodings.length = 0;

  if (parser) {
    parser.finish();
    freeParser(parser, req, socket);
  }
}

典型的情况是用户使用浏览器, 请求的时间有点长, 然后用户简单的按了一下 F5 刷新页面. 这个操作会让浏览器取消之前的请求, 然后导致服务端 throw 了一个 socket hang up.

详见万能的 stackoverflow: NodeJS - What does “socket hang up” actually mean?

DNS

早期可以用 TCP/IP 通信之后, 有一个比较蛋疼的问题, 就是 ip 都是一串比较长的数字, 比较难记, 于是大家想了个办法, 给每个 ip 取个好记一点的名字比如 Alan -> 192.168.0.11 这样只需要记住好记的名字即可, 随着这个名字的规范化最终变成了今天的域名 (Domain name), 而帮助别人记录这个名字的服务就叫域名解析服务 (Domain Name Service).

DNS 服务主要基于 UDP, 这里简单介绍 Node.js 实现的接口中的两个方法:

方法 功能 同步 网络请求 速度
.lookup(hostname[, options], cb) 通过系统自带的 DNS 缓存 (如 /etc/hosts) 同步
.resolve(hostname[, rrtype], cb) 通过系统配置的 DNS 服务器指定的记录 (rrtype指定) 异步

DNS 模块中 .lookup 与 .resolve 的区别?

当你要解析一个域名的 ip 时, 通过 .lookup 查询直接调用 getaddrinfo 来拿取地址, 速度很快, 但是如果本地的 hosts 文件被修改了, .lookup 就会拿 hosts 文件中的地方, 而 .resolve 依旧是外部正常的地址.

由于 .lookup 是同步的, 所以如果由于什么不可控的原因导致 getaddrinfo 缓慢或者阻塞是会影响整个 Node 进程的, 参见文档.

hosts 文件是什么? 什么叫 DNS 本地解析?

hosts 文件是个没有扩展名的系统文件, 其作用就是将网址域名与其对应的 IP 地址建立一个关联“数据库”, 当用户在浏览器中输入一个需要登录的网址时, 系统会首先自动从 hosts 文件中寻找对应的IP地址.

当我们访问一个域名时, 实际上需要的是访问对应的 IP 地址. 这时候, 获取 IP 地址的方式, 先是读取浏览器缓存, 如果未命中 => 接着读取本地 hosts 文件, 如果还是未命中 => 则向 DNS 服务器发送请求获取. 在向 DNS 服务器获取 IP 地址之前的行为, 叫做 DNS 本地解析.

ZLIB

在网络传输过程中, 如果网速稳定的情况下, 对数据进行压缩, 压缩比率越大, 那么传输的效率就越高等同于速度越快了. zlib 模块提供了 Gzip/Gunzip, Deflate/Inflate 和 DeflateRaw/InflateRaw 等压缩方法的类, 这些类接收相同的参数, 都属于可读写的 Stream 实例.

TODO

RPC

RPC (Remote Procedure Call Protocol) 基于 TCP/IP 来实现调用远程服务器的方法, 与 http 同属应用层. 常用于构建集群, 以及微服务 (推荐一本《Node.js 微服务》虽然我还没看完)

常见的 RPC 方式:

Thrift

Thrift是一种接口描述语言和二进制通讯协议,它被用来定义和创建跨语言的服务。它被当作一个远程过程调用(RPC)框架来使用,是由Facebook为“大规模跨语言服务开发”而开发的。它通过一个代码生成引擎联合了一个软件栈,来创建不同程度的、无缝的跨平台高效服务,可以使用C#C++(基于POSIX兼容系统)、Cappuccino、CocoaDelphiErlangGoHaskellJavaNode.jsOCamlPerlPHPPythonRubySmalltalk。虽然它以前是由Facebook开发的,但它现在是Apache软件基金会开源项目了。该实现被描述在2007年4月的一篇由Facebook发表的技术论文中,该论文现由Apache掌管。

HTTP

gRPC is an open source remote procedure call (RPC) system initially developed at Google. It uses HTTP/2 for transport, Protocol Buffers as the interface description language, and provides features such as authentication, bidirectional streaming and flow control, blocking or nonblocking bindings, and cancellation and timeouts. It generates cross-platform client and server bindings for many languages.

MQ

使用消息队列 (Message Queue) 来进行 RPC 调用 (RPC over mq) 在业内有不少例子, 比较适合业务解耦/广播/限流等场景.

TODO

Node.js 基础

OS

OS

TTY

"tty" 原意是指 "teletype" 即打字机, "pty" 则是 "pseudo-teletype" 即伪打字机. 在 Unix 中, /dev/tty* 是指任何表现的像打字机的设备, 例如终端 (terminal).

你可以通过 w 命令查看当前登录的用户情况, 你会发现每登录了一个窗口就会有一个新的 tty.

$ w
 11:49:43 up 482 days, 19:38,  3 users,  load average: 0.03, 0.08, 0.07
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
dev      pts/0    10.0.128.252     10:44    1:01m  0.09s  0.07s -bash
dev      pts/2    10.0.128.252     11:08    2:07   0.17s  0.14s top
root     pts/3    10.0.240.2       11:43    7.00s  0.04s  0.00s w

使用 ps 命令查看进程信息中也有 tty 的信息:

$ ps -x
  PID TTY      STAT   TIME COMMAND
 5530 ?        S      0:00 sshd: dev@pts/3
 5531 pts/3    Ss+    0:00 -bash
11296 ?        S      0:00 sshd: dev@pts/4
11297 pts/4    Ss     0:00 -bash
13318 pts/4    R+     0:00 ps -x
23733 ?        Ssl    2:53 PM2 v1.1.2: God Daemon

其中为 ? 的是没有依赖 TTY 的进程, 即守护进程.

在 Node.js 中你可以通过 stdio 的 isTTY 来判断当前进程是否处于 TTY (如终端) 的环境.

$ node -p -e "Boolean(process.stdout.isTTY)"
true
$ node -p -e "Boolean(process.stdout.isTTY)" | cat
false

OS

通过 OS 模块可以获取到当前系统一些基础信息的辅助函数.

属性 描述
os.EOL 根据当前系统, 返回当前系统的 End Of Line
os.arch() 返回当前系统的 CPU 架构, 如 'x86''x64'
os.constants 返回系统常量
os.cpus() 返回 CPU 每个核的信息
os.endianness() 返回 CPU 字节序, 如果是大端字节序返回 BE, 小端字节序则 LE
os.freemem() 返回系统空闲内存的大小, 单位是字节
os.homedir() 返回当前用户的根目录
os.hostname() 返回当前系统的主机名
os.loadavg() 返回负载信息
os.networkInterfaces() 返回网卡信息 (类似 ifconfig)
os.platform() 返回编译时指定的平台信息, 如 win32, linux, 同 process.platform()
os.release() 返回操作系统的分发版本号
os.tmpdir() 返回系统默认的临时文件夹
os.totalmem() 返回总内存大小(同内存条大小)
os.type() 根据 [uname](https://en.wikipedia.org/wiki/Uname#Examples) 返回系统的名称
os.uptime() 返回系统的运行时间,单位是秒
os.userInfo([options]) 返回当前用户信息

不同操作系统的换行符 (EOL) 有什么区别?

end of line (EOL) 同 newline, line ending, 以及 line break.

通常由 line feed (LF, \n) 和 carriage return (CR, \r) 组成. 常见的情况:

符号 系统
LF 在 Unix 或 Unix 相容系统 (GNU/Linux, AIX, Xenix, Mac OS X, ...)、BeOS、Amiga、RISC OS
CR+LF MS-DOS、微软视窗操作系统 (Microsoft Windows)、大部分非 Unix 的系统
CR Apple II 家族, Mac OS 至版本9

如果不了解 EOL 跨系统的兼容情况, 那么在处理文件的行分割/行统计等情况时可能会被坑.

OS 常量

Path

Node.js 内置的 path 是用于处理路径问题的模块. 不过众所周知, 路径在不同操作系统下又不可调和的差异.

Windows vs. POSIX

POSIX Windows
path.posix.sep '/' path.win32.sep '\\'
path.posix.normalize('/foo/bar//baz/asdf/quux/..') '/foo/bar/baz/asdf' path.win32.normalize('C:\temp\\foo\bar\..\') 'C:\\temp\\foo\\'
path.posix.basename('/tmp/myfile.html') 'myfile.html' path.win32.basename('C:\temp\myfile.html') 'myfile.html'
path.posix.join('/asdf', '/test.html') '/asdf/test.html' path.win32.join('/asdf', '/test.html') '\\asdf\\test.html'
path.posix.relative('/root/a', '/root/b') '../b' path.win32.relative('C:\a', 'c:\b') '..\\b'
path.posix.isAbsolute('/baz/..') true path.win32.isAbsolute('C:\foo\..') true
path.posix.delimiter ':' path.win32.delimiter ','
process.env.PATH '/usr/bin:/bin' process.env.PATH C:\Windows\system32;C:\Program Files\node\'
PATH.split(path.posix.delimiter) ['/usr/bin', '/bin'] PATH.split(path.win32.delimiter) ['C:\\Windows\\system32', 'C:\\Program Files\\node\\']

看了上表之后, 你应该了解到当你处于某个平台之下的时候, 所使用的 path 模块的方法其实就是对应的平台的方法, 例如笔者这里用的是 mac, 所以:

const path = require('path');
console.log(path.basename === path.posix.basename); // true

如果你处于其中某一个平台, 但是要处理另外一个平台的路径, 需要注意这个跨平台的问题.

path 对象

on POSIX:

path.parse('/home/user/dir/file.txt')
// Returns:
// {
//    root : "/",
//    dir : "/home/user/dir",
//    base : "file.txt",
//    ext : ".txt",
//    name : "file"
// }
┌─────────────────────┬────────────┐
│          dir        │    base    │
├──────┬              ├──────┬─────┤
│ root │              │ name │ ext │
"  /    home/user/dir / file  .txt "
└──────┴──────────────┴──────┴─────┘

on Windows:

path.parse('C:\\path\\dir\\file.txt')
// Returns:
// {
//    root : "C:\\",
//    dir : "C:\\path\\dir",
//    base : "file.txt",
//    ext : ".txt",
//    name : "file"
// }
┌─────────────────────┬────────────┐
│          dir        │    base    │
├──────┬              ├──────┬─────┤
│ root │              │ name │ ext │
" C:\      path\dir   \ file  .txt "
└──────┴──────────────┴──────┴─────┘

path.extname(path)

case return
path.extname('index.html') '.html'
path.extname('index.coffee.md') '.md'
path.extname('index.') '.'
path.extname('index') ''
path.extname('.index') ''

命令行参数

命令行参数 (Command Line Options), 即对 CLI 使用上的一些文档. 关于 CLI 主要有 4 种使用方式:

Options

参数 简介
-v, --version 查看当前 node 版本
-h, --help 查看帮助文档
-e, --eval "script" 将参数字符串当做代码执行
-p, --print "script" 打印 -e 的返回值
-c, --check 检查语法并不执行
-i, --interactive 即使 stdin 不是终端也打开 REPL 模式
-r, --require module 在启动前预先 require 指定模块
--no-deprecation 关闭废弃模块警告
--trace-deprecation 打印废弃模块的堆栈跟踪信息
--throw-deprecation 执行废弃模块时抛出错误
--no-warnings 无视报警(包括废弃警告)
--trace-warnings 打印警告的 stack (包括废弃模块)
--trace-sync-io 只要检测到异步 I/O 出于 Event loop 的开头就打印 stack trace
--zero-fill-buffers 自动初始化(zero-fill) BufferSlowBuffer
--preserve-symlinks 在解析和缓存模块时指示模块加载程序保存符号链接
--track-heap-objects 为堆快照跟踪堆对象的分配情况
--prof-process 使用 v8 选项 --prof 生成 Profilling 报告
--v8-options 显示 v8 命令行选项
--tls-cipher-list=list 指明替代的默认 TLS 加密器列表
--enable-fips 在启动时开启 FIPS-compliant crypto
--force-fips 在启动时强制实施 FIPS-compliant
--openssl-config=file 启动时加载 OpenSSL 配置文件
--icu-data-dir=file 指定ICU数据加载路径

环境变量

环境变量 简介
NODE_DEBUG=module[,…] 指定要打印调试信息的核心模块列表
NODE_PATH=path[:…] 指定搜索目录模块路径的前缀列表
NODE_DISABLE_COLORS=1 关闭 REPL 的颜色显示
NODE_ICU_DATA=file ICU (Intl object) 数据路径
NODE_REPL_HISTORY=file 持久化存储REPL历史文件的路径
NODE_TTY_UNSAFE_ASYNC=1 设置为1时, 将同步操作 stdio (如 console.log 变成同步)
NODE_EXTRA_CA_CERTS=file 指定 CA (如 VeriSign) 的额外证书路径

负载

负载是衡量服务器运行状态的一个重要概念. 通过负载情况, 我们可以知道服务器目前状态是清闲, 良好, 繁忙还是即将 crash.

通常我们要查看的负载是 CPU 负载, 详细一点的情况你可以通过阅读这篇博客: Understanding Linux CPU Load 来了解.

命令行上可以通过 uptime, top 命令, Node.js 中可以通过 os.loadavg() 来获取当前系统的负载情况:

load average: 0.09, 0.05, 0.01

其中分别是最近 1 分钟, 5 分钟, 15 分钟内系统 CPU 的平均负载. 当 CPU 的一个核工作饱和的时候负载为 1, 有几核 CPU 那么饱和负载就是几.

在 Node.js 中单个进程的 CPU 负载查看可以使用 pidusage 模块.

除了 CPU 负载, 对于服务端 (偏维护) 还需要了解网络负载, 磁盘负载等.

CheckList

有一个醉汉半夜在路灯下徘徊,路过的人奇怪地问他:“你在路灯下找什么?”醉汉回答:“我在找我的KEY”,路人更奇怪了:“找钥匙为什么在路灯下?”,醉汉说:“因为这里最亮!”。

很多服务端的同学在说到检查服务器状态时只知道使用 top 命令, 其实情况就和上面的笑话一样, 因为对于他们而言 top 是最亮的那盏路灯.

对于服务端程序员而言, 完整的服务器 checklist 首推 《性能之巅》 第二章中讲述的 USE 方法.

The USE Method provides a strategy for performing a complete check of system health, identifying common bottlenecks and errors. For each system resource, metrics for utilization, saturation and errors are identified and checked. Any issues discovered are then investigated using further strategies.

This is an example USE-based metric list for Linux operating systems (eg, Ubuntu, CentOS, Fedora). This is primarily intended for system administrators of the physical systems, who are using command line tools. Some of these metrics can be found in remote monitoring tools.

Physical Resources

componenttypemetric
CPUutilizationsystem-wide: vmstat 1, "us" + "sy" + "st"; sar -u, sum fields except "%idle" and "%iowait"; dstat -c, sum fields except "idl" and "wai"; per-cpu: mpstat -P ALL 1, sum fields except "%idle" and "%iowait"; sar -P ALL, same as mpstat; per-process: top, "%CPU"; htop, "CPU%"; ps -o pcpu; pidstat 1, "%CPU"; per-kernel-thread: top/htop ("K" to toggle), where VIRT == 0 (heuristic). [1]
CPUsaturationsystem-wide: vmstat 1, "r" > CPU count [2]; sar -q, "runq-sz" > CPU count; dstat -p, "run" > CPU count; per-process: /proc/PID/schedstat 2nd field (sched_info.run_delay); perf sched latency (shows "Average" and "Maximum" delay per-schedule); dynamic tracing, eg, SystemTap schedtimes.stp "queued(us)" [3]
CPUerrorsperf (LPE) if processor specific error events (CPC) are available; eg, AMD64's "04Ah Single-bit ECC Errors Recorded by Scrubber" [4]
Memory capacityutilizationsystem-wide: free -m, "Mem:" (main memory), "Swap:" (virtual memory); vmstat 1, "free" (main memory), "swap" (virtual memory); sar -r, "%memused"; dstat -m, "free"; slabtop -s c for kmem slab usage; per-process: top/htop, "RES" (resident main memory), "VIRT" (virtual memory), "Mem" for system-wide summary
Memory capacitysaturationsystem-wide: vmstat 1, "si"/"so" (swapping); sar -B, "pgscank" + "pgscand" (scanning); sar -W; per-process: 10th field (min_flt) from /proc/PID/stat for minor-fault rate, or dynamic tracing [5]; OOM killer: dmesg | grep killed
Memory capacityerrorsdmesg for physical failures; dynamic tracing, eg, SystemTap uprobes for failed malloc()s
Network Interfacesutilizationsar -n DEV 1, "rxKB/s"/max "txKB/s"/max; ip -s link, RX/TX tput / max bandwidth; /proc/net/dev, "bytes" RX/TX tput/max; nicstat "%Util" [6]
Network Interfacessaturationifconfig, "overruns", "dropped"; netstat -s, "segments retransmited"; sar -n EDEV, *drop and *fifo metrics; /proc/net/dev, RX/TX "drop"; nicstat "Sat" [6]; dynamic tracing for other TCP/IP stack queueing [7]
Network Interfaceserrorsifconfig, "errors", "dropped"; netstat -i, "RX-ERR"/"TX-ERR"; ip -s link, "errors"; sar -n EDEV, "rxerr/s" "txerr/s"; /proc/net/dev, "errs", "drop"; extra counters may be under /sys/class/net/...; dynamic tracing of driver function returns 76]
Storage device I/Outilizationsystem-wide: iostat -xz 1, "%util"; sar -d, "%util"; per-process: iotop; pidstat -d; /proc/PID/sched "se.statistics.iowait_sum"
Storage device I/Osaturationiostat -xnz 1, "avgqu-sz" > 1, or high "await"; sar -d same; LPE block probes for queue length/latency; dynamic/static tracing of I/O subsystem (incl. LPE block probes)
Storage device I/Oerrors/sys/devices/.../ioerr_cnt; smartctl; dynamic/static tracing of I/O subsystem response codes [8]
Storage capacityutilizationswap: swapon -s; free; /proc/meminfo "SwapFree"/"SwapTotal"; file systems: "df -h"
Storage capacitysaturationnot sure this one makes sense - once it's full, ENOSPC
Storage capacityerrorsstrace for ENOSPC; dynamic tracing for ENOSPC; /var/log/messages errs, depending on FS
Storage controllerutilizationiostat -xz 1, sum devices and compare to known IOPS/tput limits per-card
Storage controllersaturationsee storage device saturation, ...
Storage controllererrorssee storage device errors, ...
Network controllerutilizationinfer from ip -s link (or /proc/net/dev) and known controller max tput for its interfaces
Network controllersaturationsee network interface saturation, ...
Network controllererrorssee network interface errors, ...
CPU interconnectutilizationLPE (CPC) for CPU interconnect ports, tput / max
CPU interconnectsaturationLPE (CPC) for stall cycles
CPU interconnecterrorsLPE (CPC) for whatever is available
Memory interconnectutilizationLPE (CPC) for memory busses, tput / max; or CPI greater than, say, 5; CPC may also have local vs remote counters
Memory interconnectsaturationLPE (CPC) for stall cycles
Memory interconnecterrorsLPE (CPC) for whatever is available
I/O interconnectutilizationLPE (CPC) for tput / max if available; inference via known tput from iostat/ip/...
I/O interconnectsaturationLPE (CPC) for stall cycles
I/O interconnecterrorsLPE (CPC) for whatever is available

Software Resources

componenttypemetric
Kernel mutexutilizationWith CONFIG_LOCK_STATS=y, /proc/lock_stat "holdtime-totat" / "acquisitions" (also see "holdtime-min", "holdtime-max") [8]; dynamic tracing of lock functions or instructions (maybe)
Kernel mutexsaturationWith CONFIG_LOCK_STATS=y, /proc/lock_stat "waittime-total" / "contentions" (also see "waittime-min", "waittime-max"); dynamic tracing of lock functions or instructions (maybe); spinning shows up with profiling (perf record -a -g -F 997 ..., oprofile, dynamic tracing)
Kernel mutexerrorsdynamic tracing (eg, recusive mutex enter); other errors can cause kernel lockup/panic, debug with kdump/crash
User mutexutilizationvalgrind --tool=drd --exclusive-threshold=... (held time); dynamic tracing of lock to unlock function time
User mutexsaturationvalgrind --tool=drd to infer contention from held time; dynamic tracing of synchronization functions for wait time; profiling (oprofile, PEL, ...) user stacks for spins
User mutexerrorsvalgrind --tool=drd various errors; dynamic tracing of pthread_mutex_lock() for EAGAIN, EINVAL, EPERM, EDEADLK, ENOMEM, EOWNERDEAD, ...
Task capacityutilizationtop/htop, "Tasks" (current); sysctl kernel.threads-max, /proc/sys/kernel/threads-max (max)
Task capacitysaturationthreads blocking on memory allocation; at this point the page scanner should be running (sar -B "pgscan*"), else examine using dynamic tracing
Task capacityerrors"can't fork()" errors; user-level threads: pthread_create() failures with EAGAIN, EINVAL, ...; kernel: dynamic tracing of kernel_thread() ENOMEM
File descriptorsutilizationsystem-wide: sar -v, "file-nr" vs /proc/sys/fs/file-max; dstat --fs, "files"; or just /proc/sys/fs/file-nr; per-process: ls /proc/PID/fd | wc -l vs ulimit -n
File descriptorssaturationdoes this make sense? I don't think there is any queueing or blocking, other than on memory allocation.
File descriptorserrorsstrace errno == EMFILE on syscalls returning fds (eg, open(), accept(), ...).

ulimit

ulimit 用于管理用户对系统资源的访问.

-a   显示目前全部限制情况
-c   设定 core 文件的最大值, 单位为区块
-d   <数据节区大小> 程序数据节区的最大值, 单位为KB
-f   <文件大小> shell 所能建立的最大文件, 单位为区块
-H   设定资源的硬性限制, 也就是管理员所设下的限制
-m   <内存大小> 指定可使用内存的上限, 单位为 KB
-n   <文件描述符数目> 指定同一时间最多可开启的 fd 数
-p   <缓冲区大小> 指定管道缓冲区的大小, 单位512字节
-s   <堆叠大小> 指定堆叠的上限, 单位为 KB
-S   设定资源的弹性限制
-t   指定CPU使用时间的上限, 单位为秒
-u   <进程数目> 用户最多可开启的进程数目
-v   <虚拟内存大小> 指定可使用的虚拟内存上限, 单位为 KB

例如:

$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 127988
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 655360
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 4096
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

注意, open socket 等资源拿到的也是 fd, 所以 ulimit -n 比较小除了文件打不开, 还可能建立不了 socket 链接.

Node.js 基础

错误处理/调试

错误处理/调试

简述

异步还是不异步? 这是一个问题.

Promise

callback-hell

相信很多同学在面试的时候都碰到过这样一个问题, 如何处理 Callback Hell. 在早些年的时候, 大家会看到有很多的解决方案例如 Q, async, EventProxy 等等. 最后从流行程度来看 Promise 当之无愧的独领风骚, 并且是在 ES6 的 JavaScript 标准上赢得了支持.

关于它的基础知识/概念推荐看阮一峰的 Promise 对象 这里就不多不赘述.

Promise 中 .then 的第二参数与 .catch 有什么区别?

参见 We have a problem with promises

另外关于同步与异步, 有个问题希望大家看一下, 这是很简单的 Promise 的使用例子:

let doSth = new Promise((resolve, reject) => {
  console.log('hello');
  resolve();
});

doSth.then(() => {
  console.log('over');
});

毫无疑问的可以得到以下输出结果:

hello
over

但是首先的问题是, 该 Promise 封装的代码肯定是同步的, 那么这个 then 的执行是异步的吗?

其次的问题是, 如下代码, setTimeout 到 10s 之后再 .then 调用, 那么 hello 是会在 10s 之后在打印吗, 还是一开始就打印?

let doSth = new Promise((resolve, reject) => {
  console.log('hello');
  resolve();
});

setTimeout(() => {
  doSth.then(() => {
    console.log('over');
  })
}, 10000);

以及理解如下代码的执行顺序 (出处):

setTimeout(function() {
  console.log(1)
}, 0);
new Promise(function executor(resolve) {
  console.log(2);
  for( var i=0 ; i<10000 ; i++ ) {
    i == 9999 && resolve();
  }
  console.log(3);
}).then(function() {
  console.log(4);
});
console.log(5);

如果你不了解这些问题, 可以自己在本地尝试研究一下打印的结果. 这里希望你掌握的是 Promise 的状态转换, 包括异步与 Promise 的关系, 以及 Promise 如何帮助你处理异步, 如果你研究过 Promise 的实现那就更好了.

Events

Events 是 Node.js 中一个非常重要的 core 模块, 在 node 中有许多重要的 core API 都是依赖其建立的. 比如 Stream 是基于 Events 实现的, 而 fs, net, http 等模块都依赖 Stream, 所以 Events 模块的重要性可见一斑.

通过继承 EventEmitter 来使得一个类具有 node 提供的基本的 event 方法, 这样的对象可以称作 emitter, 而触发(emit)事件的 cb 则称作 listener. 与前端 DOM 树上的事件并不相同, emitter 的触发不存在冒泡, 逐层捕获等事件行为, 也没有处理事件传递的方法.

Eventemitter 的 emit 是同步还是异步?

Node.js 中 Eventemitter 的 emit 是同步的. 在官方文档中有说明:

The EventListener calls all listeners synchronously in the order in which they were registered. This is important to ensure the proper sequencing of events and to avoid race conditions or logic errors.

另外, 可以讨论如下的执行结果是输出 hi 1 还是 hi 2?

const EventEmitter = require('events');

let emitter = new EventEmitter();

emitter.on('myEvent', () => {
  console.log('hi 1');
});

emitter.on('myEvent', () => {
  console.log('hi 2');
});

emitter.emit('myEvent');

或者如下情况是否会死循环?

const EventEmitter = require('events');

let emitter = new EventEmitter();

emitter.on('myEvent', () => {
  console.log('hi');
  emitter.emit('myEvent');
});

emitter.emit('myEvent');

以及这样会不会死循环?

const EventEmitter = require('events');

let emitter = new EventEmitter();

emitter.on('myEvent', function sth () {
  emitter.on('myEvent', sth);
  console.log('hi');
});

emitter.emit('myEvent');

使用 emitter 处理问题可以处理比较复杂的状态场景, 比如 TCP 的复杂状态机, 做多项异步操作的时候每一步都可能报错, 这个时候 .emit 错误并且执行某些 .once 的操作可以将你从泥沼中拯救出来.

另外可以注意一下的是, 有些同学喜欢用 emitter 来监控某些类的状态, 但是在这些类释放的时候可能会忘记释放 emitter, 而这些类的内部可能持有该 emitter 的 listener 的引用从而导致内存泄漏.

阻塞/异步

如何判断接口是否异步? 是否只要有回调函数就是异步?

开放性问题, 每个写 node 的人都有一套自己的判断方式.

单纯使用回调函数并不会异步, IO 操作才可能会异步, 除此之外还有使用 setTimeout 等方式实现异步.

有这样一个场景, 你在线上使用 koa 搭建了一个网站, 这个网站项目中有一个你同事写的接口 A, 而 A 接口中在特殊情况下会变成死循环. 那么首先问题是, 如果触发了这个死循环, 会对网站造成什么影响?

Node.js 中执行 js 代码的过程是单线程的. 只有当前代码都执行完, 才会切入事件循环, 然后从事件队列中 pop 出下一个回调函数开始执行代码. 所以 ① 实现一个 sleep 函数, 只要通过一个死循环就可以阻塞整个 js 的执行流程. (关于如何避免坑爹的同事写出死循环, 在后面的测试环节有写到.)

如何实现一个 sleep 函数? ①

function sleep(ms) {
  var start = Date.now(), expire = start + ms;
  while (Date.now() < expire) ;
  return;
}

而异步, 是使用 libuv 来实现的 (C/C++的同学可以参见 libev 和 libevent) 另一个线程里的事件队列.

如果在线上的网站中出现了死循环的逻辑被触发, 整个进程就会一直卡在死循环中, 如果没有多进程部署的话, 之后的网站请求全部会超时, js 代码没有结束那么事件队列就会停下等待不会执行异步, 整个网站无法响应.

如何实现一个异步的 reduce? (注:不是异步完了之后同步 reduce)

需要了解 reduce 的情况, 是第 n 个与 n+1 的结果异步处理完之后, 在用新的结果与第 n+2 个元素继续依次异步下去. 不贴答案, 期待诸君的版本.

Timers

在笔者这里将 Node.js 中的异步简单的划分为两种, 硬异步和软异步.

硬异步是指由于 IO 操作或者外部调用走 libuv 而需要异步的情况. 当然, 也存在 readFileSync, execSync 等例外情况, 不过 node 由于是单线程的, 所以如果常规业务在普通时段执行可能比较耗时同步的 IO 操作会使得其执行过程中其他的所有操作都不能响应, 有点作死的感觉. 不过在启动/初始化以及一些工具脚本的应用场景下是完全没问题的. 而一般的场景下 IO 操作都是需要异步的.

软异步是指, 通过 setTimeout 等方式来实现的异步. 关于 nextTick, setTimeout 以及 setImmediate 三者的区别参见该帖

Event loop 示例

   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘

关于事件循环, Timers 以及 nextTick 的关系详见官方文档 The Node.js Event Loop, Timers, and process.nextTick(): 英文论坛中文讨论 以及 Tasks, microtasks, queues and schedules

并行/并发

并行 (Parallel) 与并发 (Concurrent) 是两个很常见的概念.

可以看 Erlang 作者 Joe Armstrong 的博客 (Concurrent and Parallel)

con_and_par

并发 (Concurrent) = 2 队列对应 1 咖啡机.

并行 (Parallel) = 2 队列对应 2 咖啡机.

Node.js 通过事件循环来挨个抽取事件队列中的一个个 Task 执行, 从而避免了传统的多线程情况下 2个队列对应 1个咖啡机 的时候上下文切换以及资源争抢/同步的问题, 所以获得了高并发的成就.

至于在 node 中并行, 你可以通过 cluster 来再添加一个咖啡机.

Node.js 基础

测试

测试

简述

为什么要写测试? 写测试是否会拖累开发进度?

项目在多人合作的时候, 为了某个功能修改了某个模块的某部分代码, 实际的情况中修改一个地方可能会影响到别人开发的多个功能, 在自己不知情的情况下想要保证自己修改的代码不影响到其他功能, 最简单的办法是通过测试来保证.

A
  \
    E
  /   \
B       H
  \   /
    F
  / 
C
  \
    G
  / 
D

如上述情况, ABCD 是逻辑层, EFGH 等是更低一次层 (比如工具层等), 当你为了功能 A 的 BUG 修改了 H 的代码, 那么实际受影响的功能除了 A 之外还有 BC, 如果你有针对每一个逻辑的测试, 那么修改了 H 的代码之后, 跑一遍测试即可保证对 H 的修改不会影响到 BC (如果有影响, 那么相应的测试会报错). 利用这种特性, 你还可以基于测试去做重构, 在通过原有测试的情况下, 即表明新的重构版本可以替代原有的版本.

而这样的效果, 只有当覆盖率达到了一定程度 (通常是 80% 以上, 90% 以上为最理想) 才能实现, 如果测试的覆盖率低, 无法覆盖到多种情况, 那么测试对你的项目可能是没有用甚至起到反作用的 (让你误以为你的修改没问题而发布等).

写测试是否会拖累开发进度要视具体情况而定. 需要考虑到, 开发进度包含功能和品质两个方面, 单纯写代码的速度不能完全代表开发进度. 测试在适当的情况下可以保证项目的品质从而得到更好的开发进度.

如上述的例子, 在修改功能 A 的 BUG 的时候, 如果你不知道 H 会影响到 BC 又没有测试的话, 那么开发 BC 的同学可能会出现十分经典的 "昨天还好好的, 今天怎么就不能用了?" 的情况.

当然写测试拖累开发进度的情况也是客观存在的, 通常是有以下几种情况:

测试是如何保证业务逻辑中不会出现死循环的?

你可以通过测试来避免坑爹的同事在某些逻辑中写出死循环, 在通常的测试中加上超时的时间, 在覆盖率足够的情况下, 就可以通过跑出超时的测试来排查出现死循环以及低性能的情况.

测试方法

黑盒测试

黑盒测试 (Black-box Testing), 测试应用程序的功能, 而不是其内部结构或运作. 测试者不需了解代码、内部结构等, 只需知道什么是应用应该做的事, 即当键入特定的输入, 可得到一定的输出. 测试者通过选择有效输入无效输入来验证是否正确的输出. 此测试方法可适合大部分的软件测试, 例如集成测试 (Integration Testing) 以及系统测试 (System Testing).

白盒测试

白盒测试 (White-box Testing) 测试应用程序的内部结构或运作, 而不是测试应用程序的功能 (即黑盒测试). 在白盒测试时, 以编程语言的角度来设计测试案例. 白盒测试可以应用于单元测试 (Unit Testing)、集成测试 (Integration Testing) 和系统的软件测试流程, 可测试在集成过程中每一单元之间的路径, 或者主系统跟子系统中的测试.

单元测试

单元测试 (Unit Testing) 是白盒测试的一种, 用于针对程序模块进行正确性检验的测试工作. 单元 (Unit) 是指最小可测试的部件. 在过程化编程中, 一个单元就是单个程序、函数、过程等; 对于面向对象编程, 最小单元就是方法, 包括基类、抽象类、或者子类中的方法.

另外, 每次修改代码之后, 通过单元测试来验证比把整个应用启动/重启验证要更快/更简单.

覆盖率

测试覆盖率 (Test Coverage) 是指代码中各项逻辑被测试覆盖到的比率, 比如 90% 的覆盖率, 是指代码中 90% 的情况都被测试覆盖到了.

覆盖率通常由四个维度贡献:

常用的测试覆盖率框架 istanbul.

当然覆盖率并不完全是由单元测试贡献, 在单元测试之上还有集成测试等. 更多关于覆盖率的内容可以参见测试覆盖(率)到底有什么用?

Mock

Mock 主要用于单元测试中. 当一个测试的对象可能依赖其他 (也许复杂/多个) 的对象. 为了确保其行为不受其他对象的影响, 你可以通过模拟其他对象的行为来隔离你要测试的对象.

当你要测试的单元依赖了一些很难纳入单元测试的情况时 (例如要测试的单元依赖数据库/文件操作/第三方服务 等情况的返回时), 使用 mock 是非常有用的. 简而言之, Mock 是模拟其他依赖的 behaviour.

Mock 与 Stub 的区别参见: Mocks Aren't Stubs

常见测试工具

集成测试

集成测试也称综合测试、组装测试、联合测试, 将程序模块采用适当的集成策略组装起来, 对系统的接口及集成后的功能进行正确性检测的测试工作. 集成测试可以是黑盒的, 也可以是白盒的, 其主要目的是检查软件单位之间的接口是否正确, 而集成测试的对象是已经经过单元测试的模块.

例如你可以在本地将项目中的 web app 启动, 并模拟接口调用:

describe('Path API', () => {
  // ...

  describe('GET /v2/path/:_id', () => {
    it('should return 200 GET /v2/path/:_id', () => {
      return request
        .get('/v2/path/' + pathId)
        .set('Cookie', 'common_user=xxx')
        .expect(200);
    });
  });

  describe('POST /v2/path', () => {
    it('should return 412 POST /v2/path lost params path', () => {
      return request
        .post('/v2/path')
        .set('Cookie', 'common_user=xxx')
        .expect(412);
    });

    it('should return 409 POST /v2/path when path exist', () => {
      return request
        .post('/v2/path')
        .send({path: '/'})
        .set('Cookie', 'common_user=xxx')
        .expect(409);
    });

    it('should return 200 POST /v2/path successfully', () => {
      return request
        .post('/v2/path')
        .send({path: '/comment'})
        .set('Cookie', 'common_user=xxx')
        .expect(200);
    });
  });

  // ...
});

基准测试

目前 Node.js 中流行的白盒级基准测试工具是 benchmark.

const Benchmark = require('benchmark');
const suite = new Benchmark.Suite;

suite.add('RegExp#test', function() {
    /o/.test('Hello World!');
})
.add('String#indexOf', function() {
    'Hello World!'.indexOf('o') > -1;
})
.on('cycle', function(event) {
    console.log(String(event.target));
})
.on('complete', function() {
    console.log('Fastest is ' + this.filter('fastest').map('name'));
})
// run async
.run({ 'async': true });

你可以将同一个功能的不同实现基于同一个标准来比较不同实现的速度, 从而得到最优解.

黑盒级别的基准测试, 则推荐 Apache ab 以及 wrk 等, 例如执行:

ab -n 100 -c 10 https://ele.me/

可以得到如下的详细数据:

Server Software:        Tengine/2.1.1
Server Hostname:        ele.me
Server Port:            443
SSL/TLS Protocol:       TLSv1.2,ECDHE-RSA-AES256-GCM-SHA384,2048,256

Document Path:          /
Document Length:        284 bytes

Concurrency Level:      10
Time taken for tests:   1.775 seconds
Complete requests:      100
Failed requests:        0
Non-2xx responses:      100
Total transferred:      62400 bytes
HTML transferred:       28400 bytes
Requests per second:    56.33 [#/sec] (mean)
Time per request:       177.511 [ms] (mean)
Time per request:       17.751 [ms] (mean, across all concurrent requests)
Transfer rate:          34.33 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       88  116  26.0    104     234
Processing:    33   55  39.6     47     394
Waiting:       33   54  39.0     46     394
Total:        124  171  48.1    152     491

Percentage of the requests served within a certain time (ms)
  50%    152
  66%    184
  75%    193
  80%    199
  90%    224
  95%    242
  98%    288
  99%    491
 100%    491 (longest request)

与前者相比, ab 等工具可以设置规模以及并发情况. 在比规模不大/需求不复杂的情况下, ab 以及 wrk 也可以用于做压力测试.

压力测试

压力测试 (Stress testing), 是保证系统稳定性的一种测试方法. 通过预估系统所需要承载的 QPS, TPS 等指标, 然后通过如 Jmeter 等压测工具模拟相应的请求情况, 来验证当前应能能否达到目标.

对于比较重要, 流量较高或者后期业务量会持续增长的系统, 进行压力测试是保证项目品质的重要环节. 常见的如负载是否均衡, 带宽是否合理, 以及磁盘 IO 网络 IO 等问题都可以通过比较极限的压力测试暴露出来.

Assert

断言 (Assert) 是快速判断并对不符合预期的情况进行报错的模块. 是将:

if (condition) {
  throw new Error('Sth wrong');
}

写成:

assert(!condition, 'Sth wrong');

等等情况的一种简化. 并且提供了丰富了 equal 判断, 对于对象类型也有深度/严格判断等情况支持.

Node.js 中内置的 assert 模块也是属于断言模块的一种, 但是官方在文档中有注明, 该内置模块主要是用于内置代码编写时的基本断言需求, 并不是一个通用的断言库 (not intended to be used as a general purpose assertion library)

常见断言工具

Node.js 基础

util

util

URL

┌─────────────────────────────────────────────────────────────────────────────┐
│                                    href                                     │
├──────────┬┬───────────┬─────────────────┬───────────────────────────┬───────┤
│ protocol ││   auth    │      host       │           path            │ hash  │
│          ││           ├──────────┬──────┼──────────┬────────────────┤       │
│          ││           │ hostname │ port │ pathname │     search     │       │
│          ││           │          │      │          ├─┬──────────────┤       │
│          ││           │          │      │          │ │    query     │       │
"  http:   // user:pass @ host.com : 8080   /p/a/t/h  ?  query=string   #hash "
│          ││           │          │      │          │ │              │       │
└──────────┴┴───────────┴──────────┴──────┴──────────┴─┴──────────────┴───────┘

转义字符

常见的需要转义的字符列表:

字符 encodeURI
' ' '%20'
< '%3C'
> '%3E'
" '%22'
``` '%60'
\r '%0D'
\n '%0A'
\t '%09'
{ '%7B'
} '%7D'
` `
\\ '%5C'
^ '%5E'
' '%27'

想了解更多? 你可以这样:

Array(range).fill(0)
  .map((_, i) => String.fromCharCode(i))
  .map(encodeURI)

range 先来个 255 试试 (doge

Query Strings

query string 属于 URL 的一部分, 见上方 URL 的表. 在 Node.js 中有内置提供一个 querystring 的模块.

方法 描述
.parse(str[, sep[, eq[, options]]]) 将一个 query string 解析为 json 对象
.unescape(str) 供 .parse 调用的内置解转义方法, 暴露出来以供用户自行替代
.stringify(obj[, sep[, eq[, options]]]) 将一个 json 对象转换成 query string
.escape(str) 供 .stringify 调用的内置转义方法, 暴露出来以供用户自行替代

Node.js 内置的 querystring 目前对于有深度的结构尚不支持. 见如下:

const qs = require('qs'); // 第三方
const querystring = require('querystring'); // Node.js 内置

let obj = { a: { b: { c: 1 } } };

console.log(qs.stringify(obj)); // 'a%5Bb%5D%5Bc%5D=1'
console.log(querystring.stringify(obj)); // 'a='

let str = 'a%5Bb%5D%5Bc%5D=1';

console.log(qs.parse(str)); // { a: { b: { c: '1' } } }
console.log(querystring.parse(str)); // { 'a[b][c]': '1' }

HTTP 如何通过 GET 方法 (URL) 传递 let arr = [1,2,3,4] 给服务器?

const qs = require('qs');

let arr = [1,2,3,4];
let str = qs.stringify({arr});

console.log(str); // arr%5B0%5D=1&arr%5B1%5D=2&arr%5B2%5D=3&arr%5B3%5D=4
console.log(decodeURI(str)); // 'arr[0]=1&arr[1]=2&arr[2]=3&arr[3]=4'
console.log(qs.parse(str)); // { arr: [ '1', '2', '3', '4' ] }

通过 https://your.host/api/?arr[0]=1&arr[1]=2&arr[2]=3&arr[3]=4 即可传递把 arr 数组传递给服务器

util

util.is*() 从 v4.0.0 开始被不建议使用即将废弃 (deprecated). 大概的废弃原因, 笔者个人认为是维护这些功能吃力不讨好, 而且现在流行的轮子那么多. 那么一下是具体列表:

其中大部分都可以作为面试题来问如何实现.

util.inherits

Node.js 中继承 (util.inherits) 的实现?

https://github.com/nodejs/node/blob/v7.6.0/lib/util.js#L960

/**
 * Inherit the prototype methods from one constructor into another.
 *
 * The Function.prototype.inherits from lang.js rewritten as a standalone
 * function (not on Function.prototype). NOTE: If this file is to be loaded
 * during bootstrapping this function needs to be rewritten using some native
 * functions as prototype setup using normal JavaScript does not work as
 * expected during bootstrapping (see mirror.js in r114903).
 *
 * @param {function} ctor Constructor function which needs to inherit the
 *     prototype.
 * @param {function} superCtor Constructor function to inherit prototype from.
 * @throws {TypeError} Will error if either constructor is null, or if
 *     the super constructor lacks a prototype.
 */
exports.inherits = function(ctor, superCtor) {

  if (ctor === undefined || ctor === null)
    throw new TypeError('The constructor to "inherits" must not be ' +
                        'null or undefined');

  if (superCtor === undefined || superCtor === null)
    throw new TypeError('The super constructor to "inherits" must not ' +
                        'be null or undefined');

  if (superCtor.prototype === undefined)
    throw new TypeError('The super constructor to "inherits" must ' +
                        'have a prototype');

  ctor.super_ = superCtor;
  Object.setPrototypeOf(ctor.prototype, superCtor.prototype);
};

正则表达式

正则表达式最早生物学上用来描述大脑神经元的一种表达式, 被 GNU 的大胡子拿来做字符串匹配之后在原本的道路上渐行渐远.

整理中..

常用模块

Awesome Node.js Most depended-upon packages

如何获取某个文件夹下所有的文件名?

一个简单的例子:

const fs = require('fs');
const path = require('path');

function traversal(dir) {
  let res = []
  for (let item of fs.readdirSync(dir)) {
    let filepath = path.join(dir, item);
    try {
      let fd = fs.openSync(filepath, 'r');
      let flag = fs.fstatSync(fd).isDirectory();
      fs.close(fd); // TODO
      if (flag) {
        res.push(...traversal(filepath));
      } else {
        res.push(filepath);
      }
    } catch(err) {
      if (err.code === 'ENOENT' && // link 文件打不开
          !!fs.readlinkSync(filepath)) { // 判断是否 link 文件
        res.push(filepath);
      } else {
        console.error('err', err);
      }
    } 
  }
  return res.map((file) => path.basename(file));
}

console.log(traversal('.'));


当然也可以 Oh my glob:

const glob = require("glob");

glob("**/*.js", (err, files) => {
  if (err) {
    throw new Error(err);
  }
  files.map((filename) => {
    console.log('Here you are:', filename);
  });
});
Node.js 基础

存储

存储

简介

科班的同学可以了解一下数据库范式, 在 ElemeFe 面试不会问, 但是其他地方可能会问 (比如阿里).

Mysql

SQL (Structured Query Language) 是关系式数据库管理系统的标准语言, 关于关系型数据库这里主要带大家看一下 Mysql 的几个问题

存储引擎

attr MyISAM InnoDB
Locking Table-level Row-level
designed for need of speed high volume of data
foreign keys × (DBMS) ✓ (RDBMS)
transaction ×
fulltext search ×
scene lots of select lots of insert/update
count rows fast slow
auto_increment fast slow

参见 MYSQL: INNODB 还是 MYISAM?

索引

索引是用空间换时间的一种优化策略. 推荐阅读: mysql索引类型 以及 主键与唯一索引的区别

Mongodb

Monogdb 连接问题(超时/断开等)有可能是什么问题导致的?

other

populate

aggregate

pipeline

Cursor

整理中

Replication

备份数据库与 M/S, M/M 等部署方式的区别?

关于数据库基于各种模式的特点全部可以通过以下图片分清:

storage

图片出处:Google App Engine 的 co-founder Ryan Barrett 在 2009 年的 google i/o 上的演讲 《Transaction Across DataCenter》(视频: http://www.youtube.com/watch?v=srOgpXECblk)

根据上图, 我们可以知道 Master/Slave 与 Master/Master 的关系.

attrMaster/SlaveMaster/Master
一致性Eventually:当你写入一个新值后,有可能读不出来,但在某个时间窗口之后保证最终能读出来。比如:DNS,电子邮件、Amazon S3,Google搜索引擎这样的系统。
事务完整本地
延迟低延迟
吞吐高吞吐
数据丢失部分丢失
熔断只读读/写

读写分离

读写分离是在 query 量大的情况下减轻单个 DB 节点压力, 优化数据库读/写速度的一种策略. 不论是 MySQL 还是 MongoDB 都可以进行读写分离.

读写分离的配置方式直接搜索一下 数据库名 + 读写分离 即可找到. 通常是 M/S 的情况, 使用 Master 专门写, 用 Slave 节点专门读. 使用读写分离时, 请确认读的请求对一致性要求不高, 因为从写库同步读库是有延迟的.

数据一致性

关于数据一致性推荐看陈皓的分布式系统的事务处理

什么情况下数据会出现脏数据? 如何避免?

为了数据的一致性, 这6件事, 要么都成功做完, 要么都不成功, 而且这个操作的过程中, 对A、B帐号的其它访问必需锁死, 所谓锁死就是要排除其它的读写操作, 否则就会出现脏数据 ---- 即数据一致性的问题.

这个问题并不仅仅出现在数据库操作中, 普通的并发以及并行操作都可能导致出现脏数据. 避免出现脏数据通常是从架构上避免或者采用事务的思想处理.

矛盾

强一致性必然导致性能短板, 而弱一致性则有很好的性能但是存在数据安全(灾备数据丢失)/一致性(脏读/脏写等)的问题.

目前 Node.js 业内流行的主要是与 Mongodb 配合, 在数据一致性方面属于短板.

事务

事务并不仅仅是 sql 数据库中的一个功能, 也是分布式系统开发中的一个思想, 事务在分布式的问题中可以称为 "两阶段提交" (以下引用陈皓原文)

第一阶段:

第二阶段:

异常:

缓存

redis 与 memcached 的区别?

attr memcached redis
struct key/value key/value + list, set, hash etc.
backup ×
Persistence ×
transcations ×
consistency strong (by cas) weak
thread multi single
memory physical physical & swap

其他

Node.js 基础

安全

安全

Crypto

Node.js 的 crypto 模块封装了诸多的加密功能, 包括 OpenSSL 的哈希、HMAC、加密、解密、签名和验证函数等.

Node.js 的加密貌似有点问题, 某些算法算出来跟别的语言 (比如 Python) 不一样. 具体情况还在整理中 (时间不定), 欢迎补充.

加密是如何保证用户密码的安全性?

在客户端加密, 是增加传输的过程中被第三方嗅探到密码后破解的成本. 对于游戏, 在客户端加密是防止外挂/破解等. 在服务端加密 (如 md5) 是避免管理数据库的 DBA 或者攻击者攻击数据库之后直接拿到明文密码, 从而提高安全性.

TLS/SSL

早期的网络传输协议由于只在大学内使用, 所以是默认互相信任的. 所以传统的网络通信可以说是没有考虑网络安全的. 早年的浏览器大厂网景公司为了应对这个情况设计了 SSL (Secure Socket Layer), SSL 的主要用途是:

  1. 认证用户和服务器, 确保数据发送到正确的客户机和服务器;
  2. 加密数据以防止数据中途被窃取;
  3. 维护数据的完整性, 确保数据在传输过程中不被改变.

存在三个特性:

1999年, SSL 因为应用广泛, 已经成为互联网上的事实标准. IETF 就在那年把 SSL 标准化/强化. 标准化之后的名称改为传输层安全协议 (Transport Layer Security, TLS). 很多相关的文章都把这两者并列称呼 (TLS/SSL), 因为这两者可以视作同一个东西的不同阶段.

HTTPS

在网络上, 每个网站都在各自的服务器上, 想要确保你访问的是一个正确的网站, 并且访问到这个网站正确的数据 (没有被劫持/篡改), 除了需要传输安全之外, 还需要安全的认证, 认证不能由目标网站进行, 否则恶意/钓鱼网站也可以自己说自己是对的, 所以为了能在网络上维护网络之间的基本信任, 早期的大厂们合力推动了一项名为 PKI 的基础设施, 通过第三方来认证网站.

公钥基础设施 (Public Key Infrastructure, PKI) 是一种遵循标准的, 利用公钥加密技术为电子商务的开展提供一套安全基础平台的技术和规范. 其基础建置包含认证中心 (Certification Authority, CA) 、注册中心 (Register Authority, RA) 、目录服务 (Directory Service, DS) 服务器.

由 RA 统筹、审核用户的证书申请, 将证书申请送至 CA 处理后发出证书, 并将证书公告至 DS 中. 在使用证书的过程中, 除了对证书的信任关系与证书本身的正确性做检查外, 并透过产生和发布证书废止列表 (Certificate Revocation List, CRL) 对证书的状态做确认检查, 了解证书是否因某种原因而遭废弃. 证书就像是个人的身分证, 其内容包括证书序号、用户名称、公开金钥 (Public Key) 、证书有效期限等.

在 TLS/SSL 中你可以使用 OpenSSL 来生成 TLS/SSL 传输时用来认证的 public/private key. 不过这个 public/private key 是自己生成的, 而通过 PKI 基础设施可以获得权威的第三方证书 (key) 从而加密 HTTP 传输安全. 目前博客圈子里比较流行的是 Let's Encrypt 签发免费的 HTTPS 证书.

需要注意的是, 如果 PKI 受到攻击, 那么 HTTPS 也一样不安全. 可以参见 HTTPS 劫持 - 知乎讨论 中的情况, 证书由 CA 机构签发, 一般浏览器遇到非权威的 CA 机构是会告警的 (参见 12306), 但是如果你在某些特殊的情况下信任了某个未知机构/证书, 那么也可能被劫持.

此外有的 CA 机构以邮件方式认证, 那么当某个网站的邮件服务收到攻击/渗透, 那么攻击者也可能以此从 CA 机构获取权威的正确的证书.

XSS

跨站脚本 (Cross-Site Scripting, XSS) 是一种代码注入方式, 为了与 CSS 区分所以被称作 XSS. 早期常见于网络论坛, 起因是网站没有对用户的输入进行严格的限制, 使得攻击者可以将脚本上传到帖子让其他人浏览到有恶意脚本的页面, 其注入方式很简单包括但不限于 JavaScript / VBScript / CSS / Flash 等.

当其他用户浏览到这些网页时, 就会执行这些恶意脚本, 对用户进行 Cookie 窃取/会话劫持/钓鱼欺骗等各种攻击. 其原理, 如使用 js 脚本收集当前用户环境的信息 (Cookie 等), 然后通过 img.src, Ajax, onclick/onload/onerror 事件等方式将用户数据传递到攻击者的服务器上. 钓鱼欺骗则常见于使用脚本进行视觉欺骗, 构建假的恶意的 Button 覆盖/替换真实的场景等情况 (该情况在用户上传 CSS 的时候也可能出现, 如早期淘宝网店装修, 使用 CSS 拼接假的评分数据等覆盖在真的评分数据上误导用户).

过滤 Html 标签能否防止 XSS? 请列举不能的情况?

用户除了上传

<script>alert('xss');</script>

还可以使用图片 url 等方式来上传脚本进行攻击

<table background="javascript:alert(/xss/)"></table>
<img src="javascript:alert('xss')">

还可以使用各种方式来回避检查, 例如空格, 回车, Tab

<img src="javas cript:
alert('xss')">

还可以通过各种编码转换 (URL 编码, Unicode 编码, HTML 编码, ESCAPE 等) 来绕过检查

<img%20src=%22javascript:alert('xss');%22>
<img src="javascrip&#116&#58alert(/xss/)">

CSP 策略

在百般无奈, 没有统一解决方案的情况下, 厂商们推出了 CSP 策略.

以 Node.js 为例, 计算脚本的 hashes 值:

const crypto = require('crypto');

function getHashByCode(code, algorithm = 'sha256') {
  return algorithm + '-' + crypto.createHash(algorithm).update(code, 'utf8').digest("base64");
}

getHashByCode('console.log("hello world");'); // 'sha256-wxWy1+9LmiuOeDwtQyZNmWpT0jqCUikqaqVlJdtdh/0='

设置 CSP 头:

content-security-policy: script-src 'sha256-wxWy1+9LmiuOeDwtQyZNmWpT0jqCUikqaqVlJdtdh/0='
<script>console.log('hello geemo')</script> <!-- 不执行 -->
<script>console.log('hello world');</script> <!-- 执行 -->

策略指令可以参见 CSP Policy Directives以及阮一峰的博文, 屈大神的博文

CSRF

跨站请求伪造 (Cross-Site Request Forgery, CSRF, https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet) 是一种伪造跨站请求的攻击方式. 例如利用你在 A 站 (攻击目标) 的 cookie / 权限等, 在 B 站 (恶意/钓鱼网站) 拼装 A 站的请求.

比如 Q 君是某论坛管理员. 已知这个论坛 A 删除的接口是 post 到某个地址, 并指定一个帖子的 id. 那么我可以在自己的博客 B 上组织一个 CSRF 请求. 然后诱使 Q 君来访问我的博客. 就可以在 Q 君不知情的情况下删除掉我想删的某个帖子.

钓鱼方式包括但不限于公开网站 (xss), 攻击者的恶意网站, email 邮件, 微博, 微信, 短信等及时消息.

同源策略是最早用于防止 CSRF 的一种方式, 即关于跨站请求 (Cross-Site Request) 只有在同源/信任的情况下才可以请求. 但是如果一个网站群, 在互相信任的情况下, 某个网站出现了问题:

a.public.com
b.public.com
c.public.com
...

以上情况下, 如果 c.public.com 上没有预防 xss 等情况, 使得攻击者可以基于此站对其他信任的网站发起 CSRF 攻击.

另外同源策略主要是浏览器来进行验证的, 并且不同浏览器的实现又各自不同, 所以在某些浏览器上可以直接绕过, 而且也可以直接通过短信等方式直接绕过浏览器.

预防:

  1. A 站 (预防站) 检查 http 请求的 header 确认其 origin
  2. 检查 CSRF token

1.同源检查

通过检查来过滤简单的 CSRF 攻击, 主要检查一下两个 header:

2.CSRF token

简单来说, 对需要预防的请求, 通过特别的算法生成 token 存在 session 中, 然后将 token 隐藏在正确的界面表单中, 正式请求时带上该 token 在服务端验证, 避免跨站请求.

中间人攻击

中间人 (Man-in-the-middle attack, MITM) 是指攻击者与通讯的两端分别创建独立的联系, 并交换其所收到的数据, 使通讯的两端认为他们正在通过一个私密的连接与对方直接对话, 但事实上整个会话都被攻击者完全控制. 在中间人攻击中, 攻击者可以拦截通讯双方的通话并插入新的内容.

目前比较常见的是在公共场所放置精心准备的免费 wifi, 劫持/监控通过该 wifi 的流量. 或者攻击路由器, 连上你家 wifi 攻破你家 wifi 之后在上面劫持流量等.

对于通信过程中的 MITM, 常见的方案是通过 PKI / TLS 预防, 及时是通过存在第三方中间人的 wifi 你通过 HTTPS 访问的页面依旧是安全的. 而 HTTP 协议是明文传输, 则没有任何防护可言.

不常见的还有强力的互相认证, 你确认他之后, 他也确认你一下; 延迟测试, 统计传输时间, 如果通讯延迟过高则认为可能存在第三方中间人; 等等.

SQL/NoSQL 注入

注入攻击是指当所执行的一些操作中有部分由用户传入时, 用户可以将其恶意逻辑注入到操作中. 当你使用 eval, new Function 等方式执行的字符串中有用户输入的部分时, 就可能被注入攻击. 上文中的 XSS 就属于一种注入攻击. 前面的章节中也提到过 Node.js 的 child_process.exec 由于调用 bash 解析, 如果执行的命令中有部分属于用户输入, 也可能被注入攻击.

SQL

Sql 注入是网站常见的一种注入攻击方式. 其原因主要是由于登录时需要验证用户名/密码, 其执行 sql 类似:

SELECT * FROM users WHERE usernae = 'myName' AND password = 'mySecret';

其中的用户名和密码属于用户输入的部分, 那么在未做检查的情况下, 用户可能拼接恶意的字符串来达到其某种目的, 例如上传密码为 '; DROP TABLE users; -- 使得最终执行的内容为:

SELECT * FROM users WHERE usernae = 'myName' AND password = ''; DROP TABLE users; --';

其能实现的功能, 包括但不限于删除数据 (经济损失), 篡改数据 (密码等), 窃取数据 (网站管理权限, 用户数据) 等. 防治手段常见于:

NoSQL

看个简单的情况:

let {user, pass, age} = ctx.query;

db.collection.find({
  user, pass,
  $where: `this.age >= ${age}`
})

那么这里的 age 就可以注入了. 另外 GET/POST 还可以传递深层结构 (比如 ?name[0]=alan 传递上来), 通过 qs 之类的模块解析后导致注入, 如 cnodejs 遭遇 mongodb 注入.

Node.js API及常用第三方模块

一、http模块: http.createServer(function(req,res){}) 二、NodeJS的模块: 1. exports.方法 = 方法 2. module.exports = {} 3. const 本地变量 = require('js地址') 三、 fs模块 1. fs.readFile("文件路径",function(err,data){}) 四、全局变量 1. __dirname 2. __filename 五、path模块: 1. path.join() 2. path.resolve() 3. path.extname() 六、 mime模块(第三方模块) 1. mime.getType() : 文档类型 res.writeHead(200, { "Content-type": mime.getType('.jpg') }); 七、 url模块 // http://www.yts.com/api/index.html?username=rose&type=flower 1. const result = url.parse(url地址) 2. result.pathname ---> api/index.html 3. result.query ----> "username=rose&type=flower"

Node.js API及常用第三方模块

HTTP 模块

所有后端动态语言要想运行起来,都得先搭建服务器。Node.js 搭建服务器需要用到一个原生的模块 http。

  1. 加载 http 模块
  2. 调用 http.createServer() 方法创建服务,方法接受一个回调函数,回调函数中有两个参数,第一个是请求体,第二个是响应体。
  3. 在回调函数中一定要使用 response.end() 方法,用于结束当前请求,不然当前请求会一直处在等待的状态。
  4. 调用 listen 监听一个端口。
//原生模块
var http = require('http');

http.createServer(function(reqeust, response){
	response.end('Hello Node');
}).listen(8080);

参数接受 -- GET

当以 GET 请求服务器的时候,服务器可以通过 request.mothod 来判断当前的请求方式并通过 request.url 来获取当前请求的参数。

var http = require('http');
var url = require('url');
 
http.createServer(function(req, res){
    var params = url.parse(req.url, true).query;
    res.end(params);
 
}).listen(3000);

参数接受 -- POST

不同于 GET 请求,POST 请求不能通协 url 进行获取,这个时候就需要用到请求体的事件进行监听获取

var http = require('http');
var util = require('util');
var querystring = require('querystring');
 
http.createServer(function(req, res){
    // 定义了一个post变量,用于暂存请求体的信息
    var post = '';     
 
    // 通过req的data事件监听函数,每当接受到请求体的数据,就累加到post变量中
    req.on('data', function(chunk){    
        post += chunk;
    });
 
    // 在end事件触发后,通过querystring.parse将post解析为真正的POST请求格式,然后向客户端返回。
    req.on('end', function(){    
        post = querystring.parse(post);
        res.end(util.inspect(post));
    });
}).listen(3000);
Node.js API及常用第三方模块

NET 模块

网络模型

net、dgram、http、https

Node内置的模块,对应的网络通信方式

模块 服务
net TCP
dgram UDP
http HTTP
https HTTPS

传输层

网络层

链路层

驱动

物理层

将二进制的0和1和电压高低,光的闪灭和电波的强弱信号进行转换

输入url到页面加载都发生了什么事情?

输入url到页面加载都发生了什么事情?

3次握手

4次挥手

输入地址
浏览器查找域名的 IP 地址
这一步包括 DNS 具体的查找过程,包括:浏览器缓存->系统缓存->路由器缓存...
浏览器向 web 服务器发送一个 HTTP 请求
服务器的永久重定向响应(从 http://example.com 到 http://www.example.com)
浏览器跟踪重定向地址
服务器处理请求
服务器返回一个 HTTP 响应
浏览器显示 HTML
浏览器发送请求获取嵌入在 HTML 中的资源(如图片、音频、视频、CSS、JS等等)
浏览器发送异步请求

状态码

1XX 2XX 3XX 4XX 5XX
信息性状态码 成功状态码 重定向 客户端错误状态码 服务端错误状态码
少见 200 OK 301 永久性重定向 400 请求报文语法错误 500服务器请求错误
204 响应报文不含实体的主体部分 302 临时性重定向(负载均衡) 401发送的请求需要有通过 HTTP 认证的认证信息 307 和302含义相同 503 服务器暂时处于超负载或正在停机维护,无法处理请求
206 范围请求 303 资源存在着另一个 URL,应使用 GET 方法定向获取资源 403 对请求资源的访问被服务器拒绝
304 客户端已经执行了GET,但文件未变化。 404 服务器上没有找到请求的资源

http/https

http是应用层协议,建立在TCP/IP之上,https则建立在TLS、SSL加密层协议之上,现代web基本都是http/https应用。TCP在建立连接要发送报文,http也是,http报文分为请求报文和响应报文,报文格式如下:

对应的代码如下,注意换行和空格

HTTP/1.0 200 OK    //起始行

Content-type:text/plain    //头部
Content-length:19            //头部  

Hi I'm a message!    //主体

Node中http模块提供创建基于http协议的网络通信应用的接口,继承于net模块,采用事件驱动机制,能与多个客户端保持连接,并不为每个连接开启新的进程或线程,低内存、高并发,性能优良。

API

创建TCP、UDP客户端和服务端

在Node中,net模块提供创建基于TCP协议的网络通信的API。

首先引入内置net模块

var net = require("net")
方法 作用
net.createServer([options][, connectionListener]) 创建一个 TCP 服务器。 参数 connectionListener 自动给 'connection' 事件创建监听器。
net.connect(options[, connectionListener]) 返回一个新的 'net.Socket',并连接到指定的地址和端口。 当 socket 建立的时候,将会触发 'connect' 事件。
net.createConnection(options[, connectionListener]) 创建一个到端口 port 和 主机 host的 TCP 连接。 host 默认为 'localhost'。
net.connect(port[, host][, connectListener]) 创建一个端口为 port 和主机为 host的 TCP 连接 。host 默认为 'localhost'。 参数 connectListener 将会作为监听器添加到 'connect' 事件。返回 'net.Socket'。
net.createConnection(port[, host][, connectListener]) 创建一个端口为 port 和主机为 host的 TCP 连接 。host 默认为 'localhost'。 参数 connectListener 将会作为监听器添加到 'connect' 事件。返回 'net.Socket'。
net.connect(path[, connectListener]) 创建连接到 path 的 unix socket 。参数 connectListener 将会作为监听器添加到 'connect' 事件上。返回 'net.Socket'。
net.createConnection(path[, connectListener]) 创建连接到 path 的 unix socket 。参数 connectListener 将会作为监听器添加到 'connect' 事件。返回 'net.Socket'。
net.isIP(input) 检测输入的是否为 IP 地址。 IPV4 返回 4, IPV6 返回 6,其他情况返回 0。
net.isIPv4(input) 如果输入的地址为 IPV4, 返回 true,否则返回 false。
net.isIPv6(input) 如果输入的地址为 IPV6, 返回 true,否则返回 false。

net.Server

net.Server通常用于创建一个TCP或本地服务器。

方法 作用
server.listen(port[, host][, backlog][, callback]) 监听指定端口 port 和 主机 host ac连接。 默认情况下 host 接受任何 IPv4 地址(INADDR_ANY)的直接连接。端口 port 为 0 时,则会分配一个随机端口。
server.listen(path[, callback]) 通过指定 path 的连接,启动一个本地 socket 服务器。
server.listen(handle[, callback]) 通过指定句柄连接。
server.listen(options[, callback]) options 的属性:端口 port, 主机 host, 和 backlog, 以及可选参数 callback 函数, 他们在一起调用server.listen(port, [host], [backlog], [callback])。还有,参数 path 可以用来指定 UNIX socket。
server.close([callback]) 服务器停止接收新的连接,保持现有连接。这是异步函数,当所有连接结束的时候服务器会关闭,并会触发 'close' 事件。
server.address() 操作系统返回绑定的地址,协议族名和服务器端口。
server.unref() 如果这是事件系统中唯一一个活动的服务器,调用 unref 将允许程序退出。
server.ref() 与 unref 相反,如果这是唯一的服务器,在之前被 unref 了的服务器上调用 ref 将不会让程序退出(默认行为)。如果服务器已经被 ref,则再次调用 ref 并不会产生影响。
server.getConnections(callback) 异步获取服务器当前活跃连接的数量。当 socket 发送给子进程后才有效;回调函数有 2 个参数 err 和 count。
let server = net.createServer((socket) => {});
server.listen(3000, () => {});

net.Socket事件

net.Socket对象是 TCP 或 UNIX Socket 的抽象。net.Socket 实例实现了一个双工流接口。 他们可以在用户创建客户端(使用 connect())时使用, 或者由 Node 创建它们,并通过 connection 服务器事件传递给用户。

方法 作用
lookup 在解析域名后,但在连接前,触发这个事件。对 UNIX sokcet 不适用。
connect 成功建立 socket 连接时触发。
data 当接收到数据时触发。
end 当 socket 另一端发送 FIN 包时,触发该事件。
timeout 当 socket 空闲超时时触发,仅是表明 socket 已经空闲。用户必须手动关闭连接。
drain 当写缓存为空得时候触发。可用来控制上传。
error 错误发生时触发。
close 当 socket 完全关闭时触发。参数 had_error 是布尔值,它表示是否因为传输错误导致 socket 关闭。
let server = net.createServer((socket) => {
    socket.on('data', (data) => {});
    socket.on('end', () => {});
    socket.on('error', (err) => {});
    socket.on('close', () => {});
});
server.on('close', (socket) => {});
server.on('error', (e) => {});

net.Sockets属性

net.Socket提供了很多有用的属性,便于控制socket交互:

socket.connect(path[, connectListener]) 打开指定路径的 unix socket。通常情况不需要使用 net.createConnection 打开 socket。只有你实现了自己的 socket 时才会用到。
socket.setEncoding([encoding]) 设置编码
socket.write(data[, encoding][, callback]) 在 socket 上发送数据。第二个参数指定了字符串的编码,默认是 UTF8 编码。
socket.end([data][, encoding]) 半关闭 socket。例如,它发送一个 FIN 包。可能服务器仍在发送数据。
socket.destroy() 确保没有 I/O 活动在这个套接字上。只有在错误发生情况下才需要。(处理错误等等)。
socket.pause() 暂停读取数据。就是说,不会再触发 data 事件。对于控制上传非常有用。
socket.resume() 调用 pause() 后想恢复读取数据。
socket.setTimeout(timeout[, callback]) socket 闲置时间超过 timeout 毫秒后 ,将 socket 设置为超时。
socket.setNoDelay([noDelay]) 禁用纳格(Nagle)算法。默认情况下 TCP 连接使用纳格算法,在发送前他们会缓冲数据。将 noDelay 设置为 true 将会在调用 socket.write() 时立即发送数据。noDelay 默认值为 true。
socket.setKeepAlive([enable][, initialDelay]) 禁用/启用长连接功能,并在发送第一个在闲置 socket 上的长连接 probe 之前,可选地设定初始延时。默认为 false。 设定 initialDelay (毫秒),来设定收到的最后一个数据包和第一个长连接probe之间的延时。将 initialDelay 设为0,将会保留默认(或者之前)的值。默认值为0。
socket.address() 操作系统返回绑定的地址,协议族名和服务器端口。返回的对象有 3 个属性,比如{ port: 12346, family: 'IPv4', address: '127.0.0.1' }。
socket.unref() 如果这是事件系统中唯一一个活动的服务器,调用 unref 将允许程序退出。如果服务器已被 unref,则再次调用 unref 并不会产生影响。
socket.ref() 与 unref 相反,如果这是唯一的服务器,在之前被 unref 了的服务器上调用 ref 将不会让程序退出(默认行为)。如果服务器已经被 ref,则再次调用 ref 并不会产生影响。

new net.Socket([options])构造一个新的socket对象。

let server = net.createServer((socket) => {
    socket.setEncoding('utf8');
    socket.write();
    socket.end();
});

TCP模拟HTTP

可以用net模块模拟出http模块的功能

我们还可以在这里可以借助socket.pipe配合fs.createWriteStream来吧浏览器的数据保存到本地message.txt文件中,

在浏览器输入http://localhost:3000会返回hello

let net = require('net');
let server = net.createServer({
    // 如果 pauseOnConnect 被设置为 true, 那么与连接相关的套接字都会暂停,也不会从套接字句柄读取数据。 这样就允许连接在进程之间传递,避免数据被最初的进程读取。 如果想从一个暂停的套接字开始读数据
    pauseOnConnect: true
}, (socket) => {
    socket.setEncoding('utf8');
    socket.on('data', (data) => {
        console.log(data);
    });
    socket.on('end', () => {
        console.log('client disconnected');
    });
    //接收到客户端发送的错误就会调用   
    socket.on('error', (err) => {
        console.log("error");
    });
    socket.on('close', () => {
        console.log("close socket");
    });
    socket.end(`
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 5

hello`)
    console.log('request');
});

server.listen(3000, () => {
    console.log('opened server on', server.address({}));
});
server.on('connection', (socket) => {
    console.log('connection');
});

//server.unref();//停止node对server的监听事件
//服务器关闭事件
server.on('close', (socket) => {
    console.log('close server');
});
server.on('error', (e) => {
    if (e.code === 'EADDRINUSE') {
        console.log('Address in use, retrying...');
        setTimeout(() => {
            server.close();
            server.listen(PORT, HOST);
        }, 1000);
    }
});

postman测试,记录在message.txt的格式为

POST /abc HTTP/1.1
Content-Type: multipart/form-data; boundary=--------------------------879095998142409176007484
abc: 123
bbb: ccc
ddd: eee
token: eyJkYXRhIjp7ImlucHV0RW1haWwiOiJsZW1vbiIsImlucHV0UGFzc3dvcmQiOiIxMjMifSwiY3JlYXRlZCI6MTU0NzA0MTEyMCwiZXhwIjo2MH0=.jP/Nm3RMzy6MGnH5uHWuvTkFZp94Bm5tMfDhhdRxlaM=
cache-control: no-cache
Postman-Token: 97b4950a-1169-407b-8787-ab238d3954d4
User-Agent: PostmanRuntime/7.6.0
Accept: */*
Host: localhost:3000
cookie: csrfToken=58RWUaRa3ZuA2uIp7cxn34pC
accept-encoding: gzip, deflate
content-length: 157
Connection: keep-alive

----------------------------879095998142409176007484
Content-Disposition: form-data; name="x"

x
----------------------------879095998142409176007484--

net源码中我们也可以看到这些设置,从中我们得知http模块是真的基于net模块实现的

var statusLine = `HTTP/1.1 ${statusCode} ${this.statusMessage}${CRLF}`; // line 252

function Server(options, requestListener) {
  net.Server.call(this, { allowHalfOpen: true });
  if (requestListener) {
    this.on('request', requestListener);
  }
} // line 283

net.Server.call(this, { allowHalfOpen: true }); //line 298

参考文档

解决 npm 安装 node-sass 速度慢的问题

解决 npm 安装 node-sass 速度慢的问题

常见问题

解决方案:

# 1. 单独安装:
npm install --unsafe-perm node-sass
# 2. 直接使用:
npm install --unsafe-perm

解决方案:

可通过配置淘宝的镜像源解决,首先配置淘宝的镜像源

npm config set registry https://registry.npm.taobao.org

然后在 ~/.npmrc 加入下面内容

sass_binary_site=https://npm.taobao.org/mirrors/node-sass/

.npmrc 文件位于

win:

C:\Users\[你的账户名称]\.npmrc

linux:直接使用

vi ~/.npmrc

完整配置如图

安装node-gpy

安装node-gpy

背景介绍:node-gyp

GYP

GYP是一种构建自动化工具。 GYP由Google创建,用于生成用于构建Chromium Web浏览器的本机IDE项目文件,并使用BSD软件许可证作为开源软件获得许可。 GYP的功能类似于CMake构建工具。 GYP处理包含JSON字典的文件,以生成一个或多个目标项目make文件。

操作系统: macOS, Linux, Solaris, FreeBSD, OpenBSD, Windows

编写时间: Python

许可协议: BSD license

原著者: Mark Mentovai

长久以来 linux 的二进制分发一直是巨坑,npm 为了方便干脆就直接源码分发,用户装的时候再现场编译。

Google使用过很多处理平台无关的项目构建系统,比如Scons,CMake。在实际使用中这些并不能满足需求。开发复杂的应用程序时,在Mac上Xcode更加适合,而Windows上Visual Studio更是无二之选。gyp是为Chromium项目创建的项目生成工具,生成项目文件后就可以调用GCC, vsbuild, xcode等编译平台来编译。从平台无关的配置生成平台相关的Visual Studio、Xcode、Makefile的项目文件。这样一来我们就不需要花额外的时间处理每个平台不同的项目配置以及项目之间的依赖关系。

node下的gyp

至于为什么要有node-gyp,是由于node程序中需要调用一些其他语言编写的工具甚至是dll,需要先编译一下,否则就会有跨平台的问题,例如在windows上运行的软件copy到mac上就不能用了,但是如果源码支持,编译一下,在mac上还是可以用的。node-gyp在较新的Node版本中都是自带的(平台相关),用来编译原生C++模块。

node-gyp是用Node.js编写的跨平台命令行工具,用于为Node.js编译本机附加模块。它包含gyp-next项目的供应商副本,该副本 以前由Chromium团队使用,已扩展以支持Node.js本机插件的开发。

在一个新的vue项目中安装:

先在控制台输入:

#(此命令为一键安装) 如果是干净的环境可以用下面命令一键安装
npm install --global --production windows-build-tools

为啥要一键安装呢,安装的是啥呢?

解释:安装前提条件

  1. python(v2.7 ,3.x不支持);

  2. visual C++ Build Tools,或者 (vs2015以上(包含15))

  3. .net framework 4.5.1

就是安装的这三个东西,安装时间有点长,别着急

安装命令

npm install -g node-gyp

安装完成后查看

控制台输入:

node-gyp list

遇到问题

python找不到或者环境不对

npm config set python D:\Library\Python\Python27\python.exe

如果不行执行下面的

node-gyp configure --python v2.7.3 --verbose

node-gyp rebuild 出错

npm uninstall node-gyp -g
npm i -g windows-build-tools# 环境已用不用执行
npm install -g node-gyp
npm iconfig set python python
npm i microtime --save-dev

提示找不到系统SDK

node-gyp 配置错误

--proxy=http://myproxyurl/
或者
npm config set registry http://registry.npmjs.org/

提示https超时或者异常

npm config set registry http://registry.npmjs.org/
npm config get registry # 确定是不是http,有时设置可能失败,原因未知