Node.js复习

前阵子学习了Node,已经了解了Node的基本知识,并安装了node,npm,了解了怎么去运行,现在这篇是复习一下node。

Node(复习)

Node.js:基于JavaScript语言和V8引擎的开源Web服务器项目

在Node上运行的JavaScript相比其他后端开发语言有何优势?

最大的优势是借助JavaScript天生的事件驱动机制V8高性能引擎,使编写高性能Web服务轻而易举。
其次,JavaScript语言本身是完善的函数式语言,在Node环境下,通过模块化的JavaScript代码,加上函数式编程,并且无需考虑浏览器兼容性问题,直接使用最新的ECMAScript 6标准,可以完全满足工程上的需求。

io.js,这又是什么鬼?

io.js是“尝鲜版”,而Node.js是线上稳定版,相当于Fedora Linux和RHEL的关系。

#npm

npm其实是Node.js的包管理工具(package manager)。

  • 为啥我们需要一个包管理工具呢?因为我们在Node.js上开发时,会用到很多别人写的JavaScript代码。如果我们要使用别人写的某个包,每次都根据名称搜索一下官方网站,下载代码,解压,再使用,非常繁琐。于是一个集中管理的工具应运而生:大家都把自己开发的模块打包后放到npm官网上,如果要使用,直接通过npm安装就可以直接用,不用管代码存在哪,应该从哪下载。

  • 更重要的是,如果我们要使用模块A,而模块A又依赖于模块B,模块B又依赖于模块X和模块Y,npm可以根据依赖关系,把所有依赖的包都下载下来并管理起来。否则,靠我们自己手动管理,肯定既麻烦又容易出错。

第一个Node程序

选择一个目录,例如F:\Node,把文件保存为hello.js,就可以打开命令行窗口,把当前目录切换到hello.js所在目录,然后输入以下命令运行这个程序了:

1
2
F:\Node>node hello.js
Hello, world.

也可以保存为别的名字,比如first.js,但是必须要以.js结尾。此外,文件名只能是英文字母数字下划线的组合。

命令行模式和Node交互模式

请注意区分命令行模式和Node交互模式。

看到类似C:>是在Windows提供的命令行模式

看到>是在Node交互式环境下:

在Node交互式环境下,我们可以输入JavaScript代码并立刻执行。

使用严格模式

如果在JavaScript文件开头写上'use strict';,那么Node在执行该JavaScript时将使用严格模式。但是,在服务器环境下,如果有很多JavaScript文件,每个文件都写上’use strict’;很麻烦。我们可以给Nodejs传递一个参数,让Node直接为所有js文件开启严格模式:

1
node --use_strict

给指定文件开启严格模式:

1
node --use_strict 文件名(例如:hello.js)

模块

在Node环境中,一个.js文件就称之为一个模块(module)。

使用模块有什么好处?

  • 最大的好处是大大提高了代码的可维护性。其次,编写代码不必从零开始。当一个模块编写完毕,就可以被其他地方引用。我们在编写程序的时候,也经常引用其他模块,包括Node内置的模块和来自第三方的模块

  • 使用模块还可以避免函数名和变量名冲突。相同名字的函数和变量完全可以分别存在不同的模块中,因此,我们自己在编写模块时,不必考虑名字会与其他模块冲突。

比如,我们编写了一个hello.js文件,这个hello.js文件就是一个模块,模块的名字就是文件名(去掉.js后缀),所以hello.js文件就是名为hello的模块。

1
2
3
4
5
6
7
8
9
'use strict';

var s = 'Hello';

function greet(name) {
console.log(s + ', ' + name + '!');
}

module.exports = greet;

函数greet()是我们在hello模块中定义的,你可能注意到最后一行是一个奇怪的赋值语句,它的意思是,把函数greet作为模块的输出暴露出去,这样其他模块就可以使用greet函数了

问题是其他模块怎么使用hello模块的这个greet函数呢?我们再编写一个main.js文件,调用hello模块的greet函数:

1
2
3
4
5
6
7
8
'use strict';

// 引入hello模块:
var greet = require('./hello');

var s = 'Michael';

greet(s); // Hello, Michael!

注意到引入hello模块用Node提供的require函数:

1
var greet = require('./hello');

引入的模块作为变量保存在greet变量中,那greet变量到底是什么东西?其实变量greet就是在hello.js中我们用module.exports = greet;输出的greet函数。所以,main.js就成功地引用了hello.js模块中定义的greet()函数,接下来就可以直接使用它了。

在使用require()引入模块的时候,请注意模块的相对路径。因为main.js和hello.js位于同一个目录,所以我们用了当前目录.:

1
var greet = require('./hello'); // 不要忘了写相对目录!

如果只写模块名:

1
var greet = require('hello');

则Node会依次在内置模块全局模块当前模块下查找hello.js,你很可能会得到一个错误:

module.js
throw err;
  ^
Error: Cannot find module 'hello'
at Function.Module._resolveFilename
at Function.Module._load
...
at Function.Module._load
at Function.Module.runMain

遇到这个错误,你要检查:

  • 模块名是否写对了;
  • 模块文件是否存在;
  • 相对路径是否写对了。

CommonJS规范

这种模块加载机制被称为CommonJS规范。在这个规范下,每个.js文件都是一个模块,它们内部各自使用的变量名和函数名都互不冲突,例如,hello.js和main.js都申明了全局变量var s = ‘xxx’,但互不影响。

小结:
要在模块中对外输出变量,用

module.exports = variable;

输出的变量可以是任意对象、函数、数组等等。

要引入其他模块输出的对象,用:

var foo = require('other_module');

深入了解模块原理

JavaScript语言本身并没有一种模块机制来保证不同模块可以使用相同的变量名。

其实要实现“模块”这个功能,并不需要语法层面的支持。Node.js也并不会增加任何JavaScript语法。实现“模块”功能的奥妙就在于JavaScript是一种函数式编程语言,它支持闭包。如果我们把一段JavaScript代码用一个函数包装起来,这段代码的所有“全局”变量就变成了函数内部的局部变量。

请注意我们编写的hello.js代码是这样的:

var s = 'Hello';
var name = 'world';

console.log(s + ' ' + name + '!');

Node.js加载了hello.js后,它可以把代码包装一下,变成这样执行:

(function () {
// 读取的hello.js代码:
var s = 'Hello';
var name = 'world';

console.log(s + ' ' + name + '!');
// hello.js代码结束
})();

所以,Node利用JavaScript的函数式编程的特性,轻而易举地实现了模块的隔离。

但是,模块的输出module.exports怎么实现?

这个也很容易实现,Node可以先准备一个对象module:

// 准备module对象:
var module = {
id: 'hello',
exports: {}
};
var load = function (module) {
// 读取的hello.js代码:
function greet(name) {
console.log('Hello, ' + name + '!');
}

module.exports = greet;
// hello.js代码结束
return module.exports;
};
var exported = load(module);
// 保存module:
save(module, exported);

可见,变量module是Node在加载js文件前准备的一个变量,并将其传入加载函数,我们在hello.js中可以直接使用变量module原因就在于它实际上是函数的一个参数:

module.exports = greet;

通过把参数module传递给load()函数,hello.js就顺利地把一个变量传递给了Node执行环境,Node会把module变量保存到某个地方。

由于Node保存了所有导入的module,当我们用require()获取module时,Node找到对应的module,把这个moduleexports变量返回,这样,另一个模块就顺利拿到了模块的输出:

var greet = require('./hello');

以上是Node实现JavaScript模块的一个简单的原理介绍。

module.exports vs exports

很多时候,你会看到,在Node环境中,有两种方法可以在一个模块中输出变量:

方法一:对module.exports赋值:

// hello.js

function hello() {
console.log('Hello, world!');
}

function greet(name) {
console.log('Hello, ' + name + '!');
}

module.exports = {
hello: hello,
greet: greet
};

方法二:直接使用exports:

// hello.js

function hello() {
console.log('Hello, world!');
}

function greet(name) {
console.log('Hello, ' + name + '!');
}

function hello() {
console.log('Hello, world!');
}

exports.hello = hello;
exports.greet = greet;

但是你不可以直接对exports赋值:

// 代码可以执行,但是模块并没有输出任何变量:

exports = {
hello: hello,
greet: greet
};

结论:
如果要输出一个函数或数组,必须直接对module.exports对象赋值。

直接对module.exports赋值,可以应对任何情况

module.exports = {
foo: function () { return 'foo'; }
};

或者:

module.exports = function () { return 'foo'; };

基本模块

因为Node.js是运行在服务区端的JavaScript环境,服务器程序和浏览器程序相比,最大的特点是没有浏览器的安全限制了,而且,服务器程序必须能接收网络请求,读写文件,处理二进制内容,所以,Node.js内置的常用模块就是为了实现基本的服务器功能。这些模块在浏览器环境中是无法被执行的,因为它们的底层代码是用C/C++在Node.js运行环境中实现的。

global

在前面的JavaScript课程中,我们已经知道,JavaScript有且仅有一个全局对象,在浏览器中,叫window对象。而在Node.js环境中,也有唯一的全局对象,但不叫window,而叫global,这个对象的属性和方法也和浏览器环境的window不同。进入Node.js交互环境,可以直接输入:

E:\Node>node
> global.console
Console {
  log: [Function: bound ],
  info: [Function: bound ],
  warn: [Function: bound ],
  error: [Function: bound ],
  dir: [Function: bound ],
  time: [Function: bound ],
  timeEnd: [Function: bound ],
  trace: [Function: bound trace],
  assert: [Function: bound ],
  Console: [Function: Console] }
>

process

·process·也是Node.js提供的一个对象,它代表当前Node.js进程。通过process对象可以拿到许多有用信息:

> process==global.process
true
> process.version
'v4.6.0'
> process.platform
'win32'
> process.arch
'x64'
>
> process.cwd()
'E:\\Node'

JavaScript程序是由事件驱动执行的单线程模型,Node.js也不例外。Node.js不断执行响应事件的JavaScript函数,直到没有任何响应事件的函数可以执行时,Node.js就退出了。

如果我们想要在下一次事件响应中执行代码,可以调用process.nextTick()

// test.js

// process.nextTick()将在下一轮事件循环中调用:
process.nextTick(function () {
console.log('nextTick callback!');
});
console.log('nextTick was set!');

用Node执行上面的代码node test.js,你会看到,打印输出是:

1
2
nextTick was set!
nextTick callback!

这说明传入process.nextTick()的函数不是立刻执行,而是要等到下一次事件循环。

Node.js进程本身的事件就由process对象来处理。如果我们响应exit事件,就可以在程序即将退出时执行某个回调函数:

1
2
3
4
// 程序即将退出时的回调函数:
process.on('exit', function (code) {
console.log('about to exit with code: ' + code);
});

判断JavaScript执行环境

有很多JavaScript代码既能在浏览器中执行,也能在Node环境执行,但有些时候,程序本身需要判断自己到底是在什么环境下执行的,常用的方式就是根据浏览器Node环境提供的全局变量名称来判断:

1
2
3
4
5
if (typeof(window) === 'undefined') {
console.log('node.js');
} else {
console.log('browser');
}

fs

Node.js内置的fs模块就是 文件系统模块 ,负责读写文件。

和所有其它JavaScript模块不同的是,fs模块同时提供了异步和同步的方法。

回顾一下什么是异步方法。因为JavaScript的单线程模型,执行IO操作时,JavaScript代码无需等待,而是传入回调函数后,继续执行后续JavaScript代码。比如jQuery提供的getJSON()操作:

1
2
3
4
$.getJSON('http://example.com/ajax', function (data) {
console.log('IO结果返回后执行...');
});
console.log('不等待IO结果直接执行后续代码...');

而同步的IO操作则需要等待函数返回:

1
2
// 根据网络耗时,函数将执行几十毫秒~几秒不等:
var data = getJSONSync('http://example.com/ajax');

同步操作的好处是代码简单,缺点是程序将等待IO操作,在等待时间内,无法响应其它任何事件。而异步读取不用等待IO操作,但代码较麻烦。

异步读文件
按照JavaScript的标准,异步读取一个文本文件的代码如下:

1
2
3
4
5
6
7
8
9
10
11
'use strict';

var fs = require('fs');

fs.readFile('sample.txt', 'utf-8', function (err, data) {
if (err) {
console.log(err);
} else {
console.log(data);
}
});

请注意,sample.txt文件必须在当前目录下,且文件编码为utf-8

异步读取时,传入的回调函数接收两个参数,当正常读取时,err参数为nulldata参数为读取到的String。当读取发生错误时,err参数代表一个错误对象,dataundefined。这也是Node.js标准的回调函数:第一个参数代表错误信息,第二个参数代表结果。后面我们还会经常编写这种回调函数。

由于err是否为null就是判断是否出错的标志,所以通常的判断逻辑总是:

1
2
3
4
5
if (err) {
// 出错了
} else {
// 正常
}

如果我们要读取的文件不是文本文件,而是二进制文件,怎么办?

下面的例子演示了如何读取一个图片文件:

1
2
3
4
5
6
7
8
9
10
11
12
'use strict';

var fs = require('fs');

fs.readFile('sample.png', function (err, data) {
if (err) {
console.log(err);
} else {
console.log(data);
console.log(data.length + ' bytes');
}
});

当读取二进制文件时,不传入文件编码时,回调函数的data参数将返回一个Buffer对象。在Node.js中,Buffer对象就是一个包含零个或任意个字节的数组(注意和Array不同)。

Buffer对象可以和String作转换,例如,把一个Buffer对象转换成String:

1
2
3
// Buffer -> String
var text = data.toString('utf-8');
console.log(text);

或者把一个String转换成Buffer:

1
2
3
// String -> Buffer
var buf = new Buffer(text, 'utf-8');
console.log(buf);

同步读文件

除了标准的异步读取模式外,fs也提供相应的同步读取函数。同步读取的函数和异步函数相比,多了一个Sync后缀,并且不接收回调函数,函数直接返回结果。

fs模块同步读取一个文本文件的代码如下:

1
2
3
4
5
6
'use strict';

var fs = require('fs');

var data = fs.readFileSync('sample.txt', 'utf-8');
console.log(data);

可见,原异步调用的回调函数的data被函数直接返回,函数名需要改为readFileSync,其它参数不变。

如果同步读取文件发生错误,则需要用try…catch捕获该错误:

1
2
3
4
5
6
try {
var data = fs.readFileSync('sample.txt', 'utf-8');
console.log(data);
} catch (err) {
// 出错了
}

写文件

将数据写入文件是通过fs.writeFile()实现的:

1
2
3
4
5
6
7
8
9
10
11
12
'use strict';

var fs = require('fs');

var data = 'Hello, Node.js';
fs.writeFile('output.txt', data, function (err) {
if (err) {
console.log(err);
} else {
console.log('ok.');
}
});

writeFile()的参数依次为文件名数据回调函数。如果传入的数据是String,默认按UTF-8编码写入文本文件,如果传入的参数是Buffer,则写入的是二进制文件。回调函数由于只关心成功与否,因此只需要一个err参数。
和readFile()类似,writeFile()也有一个同步方法,叫writeFileSync()

1
2
3
4
5
6
'use strict';

var fs = require('fs');

var data = 'Hello, Node.js';
fs.writeFileSync('output.txt', data);

stat
如果我们要获取文件大小创建时间等信息,可以使用fs.stat(),它返回一个Stat对象,能告诉我们文件或目录的详细信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
'use strict';

var fs = require('fs');

fs.stat('sample.txt', function (err, stat) {
if (err) {
console.log(err);
} else {
// 是否是文件:
console.log('isFile: ' + stat.isFile());
// 是否是目录:
console.log('isDirectory: ' + stat.isDirectory());
if (stat.isFile()) {
// 文件大小:
console.log('size: ' + stat.size);
// 创建时间, Date对象:
console.log('birth time: ' + stat.birthtime);
// 修改时间, Date对象:
console.log('modified time: ' + stat.mtime);
}
}
});

运行结果如下:

1
2
3
4
5
isFile: true
isDirectory: false
size: 181
birth time: Fri Dec 11 2015 09:43:41 GMT+0800 (CST)
modified time: Fri Dec 11 2015 12:09:00 GMT+0800 (CST)

stat()也有一个对应的同步函数statSync()

异步还是同步

fs模块中,提供同步方法是为了方便使用。那我们到底是应该用异步方法还是同步方法呢?

由于Node环境执行的JavaScript代码是服务器端代码,所以,绝大部分需要在服务器运行期反复执行业务逻辑的代码,必须使用异步代码,否则,同步代码在执行时期,服务器将停止响应,因为JavaScript只有一个执行线程。

服务器启动时如果需要读取配置文件,或者结束时需要写入到状态文件时,可以使用同步代码,因为这些代码只在启动和结束时执行一次,不影响服务器正常运行时的异步执行。

stream

stream是Node.js提供的又一个仅在服务区端可用的模块,目的是支持“流”这种数据结构

什么是流?流是一种抽象的数据结构。
流的特点是数据是有序的,而且必须依次读取,或者依次写入,不能像Array那样随机定位。

在Node.js中,流也是一个对象,我们只需要响应流的事件就可以了:data事件表示流的数据已经可以读取了,end事件表示这个流已经到末尾了,没有数据可以读取了,error事件表示出错了。

下面是一个从文件流读取文本内容的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
'use strict';

var fs = require('fs');

// 打开一个流:
var rs = fs.createReadStream('sample.txt', 'utf-8');

rs.on('data', function (chunk) {
console.log('DATA:')
console.log(chunk);
});

rs.on('end', function () {
console.log('END');
});

rs.on('error', function (err) {
console.log('ERROR: ' + err);
});

要注意,data事件可能会有多次,每次传递的chunk是流的一部分数据。

要以流的形式写入文件,只需要不断调用write()方法,最后以end()结束:

1
2
3
4
5
6
7
8
9
10
11
12
13
'use strict';

var fs = require('fs');

var ws1 = fs.createWriteStream('output1.txt', 'utf-8');
ws1.write('使用Stream写入文本数据...\n');
ws1.write('END.');
ws1.end();

var ws2 = fs.createWriteStream('output2.txt');
ws2.write(new Buffer('使用Stream写入二进制数据...\n', 'utf-8'));
ws2.write(new Buffer('END.', 'utf-8'));
ws2.end();

所有可以读取数据的流都继承自stream.Readable,所有可以写入的流都继承自stream.Writable

pipe

就像可以把两个水管串成一个更长的水管一样,两个流也可以串起来。一个Readable流和一个Writable流串起来后,所有的数据自动从Readable流进入Writable流,这种操作叫pipe。

在Node.js中,Readable流有一个pipe()方法,就是用来干这件事的。

让我们用pipe()把一个文件流和另一个文件流串起来,这样源文件的所有数据就自动写入到目标文件里了,所以,这实际上是一个复制文件的程序:

1
2
3
4
5
6
7
8
'use strict';

var fs = require('fs');

var rs = fs.createReadStream('sample.txt');
var ws = fs.createWriteStream('copied.txt');

rs.pipe(ws);

默认情况下,当Readable流的数据读取完毕,end事件触发后,将自动关闭Writable流。如果我们不希望自动关闭Writable流,需要传入参数:

1
readable.pipe(writable, { end: false });

http

HTTP协议
要理解Web服务器程序的工作原理,首先,我们要对HTTP协议有基本的了解。

HTTP服务器
要开发HTTP服务器程序,从头处理TCP连接,解析HTTP是不现实的。这些工作实际上已经由Node.js自带的http模块完成了。应用程序并不直接和HTTP协议打交道,而是操作http模块提供的requestresponse对象。

request对象封装了HTTP请求,我们调用request对象的属性和方法就可以拿到所有HTTP请求的信息;

response对象封装了HTTP响应,我们操作response对象的方法,就可以把HTTP响应返回给浏览器。

用Node.js实现一个HTTP服务器程序非常简单。我们来实现一个最简单的Web程序hello.js,它对于所有请求,都返回Hello world!:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

'use strict';

// 导入http模块:
var http = require('http');

// 创建http server,并传入回调函数:
var server = http.createServer(function (request, response) {
// 回调函数接收request和response对象,
// 获得HTTP请求的method和url:
console.log(request.method + ': ' + request.url);
// 将HTTP响应200写入response, 同时设置Content-Type: text/html:
response.writeHead(200, {'Content-Type': 'text/html'});
// 将HTTP响应的HTML内容写入response:
response.end('<h1>Hello world!</h1>');
});

// 让服务器监听8080端口:
server.listen(8080);

console.log('Server is running at http://127.0.0.1:8080/');

在命令提示符下运行该程序,可以看到以下输出:

1
2
$ node hello.js 
Server is running at http://127.0.0.1:8080/

不要关闭命令提示符,直接打开浏览器输入http://localhost:8080,即可看到服务器响应的内容。
同时,在命令提示符窗口,可以看到程序打印的请求信息:

1
2
GET: /
GET: /favicon.ico

文件服务器
让我们继续扩展一下上面的Web程序。我们可以设定一个目录,然后让Web程序变成一个文件服务器。要实现这一点,我们只需要解析request.url中的路径,然后在本地找到对应的文件把文件内容发送出去就可以了。

解析URL需要用到Node.js提供的url模块,它使用起来非常简单,通过parse()将一个字符串解析为一个Url对象:

1
2
3
4
5
'use strict';

var url = require('url');

console.log(url.parse('http://user:pass@host.com:8080/path/to/file?query=string#hash'));

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Url {
protocol: 'http:',
slashes: true,
auth: 'user:pass',
host: 'host.com:8080',
port: '8080',
hostname: 'host.com',
hash: '#hash',
search: '?query=string',
query: 'query=string',
pathname: '/path/to/file',
path: '/path/to/file?query=string',
href: 'http://user:pass@host.com:8080/path/to/file?query=string#hash' }

处理本地文件目录需要使用Node.js提供的path模块,它可以方便地构造目录:

1
2
3
4
5
6
7
8
9
10
'use strict';

var path = require('path');

// 解析当前目录:
var workDir = path.resolve('.'); //F:\Node

// 组合完整的文件路径:当前目录+'pub'+'index.html':
var filePath = path.join(workDir, 'pub', 'index.html');
// 'F:\Node\pub\index.html'

最后,我们实现一个文件服务器file_server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
'use strict';

var
fs = require('fs'),
url = require('url'),
path = require('path'),
http = require('http');

// 从命令行参数获取root目录,默认是当前目录:
var root = path.resolve(process.argv[2] || '.');

console.log('Static root dir: ' + root);

// 创建服务器:
var server = http.createServer(function (request, response) {
// 获得URL的path,类似 '/css/bootstrap.css':
var pathname = url.parse(request.url).pathname;
// 获得对应的本地文件路径,类似 '/srv/www/css/bootstrap.css':
var filepath = path.join(root, pathname);
// 获取文件状态:
fs.stat(filepath, function (err, stats) {
if (!err && stats.isFile()) {
// 没有出错并且文件存在:
console.log('200 ' + request.url);
// 发送200响应:
response.writeHead(200);
// 将文件流导向response:
fs.createReadStream(filepath).pipe(response);
} else {
// 出错了或者文件不存在:
console.log('404 ' + request.url);
// 发送404响应:
response.writeHead(404);
response.end('404 Not Found');
}
});
});

server.listen(8080);

console.log('Server is running at http://127.0.0.1:8080/');

没有必要手动读取文件内容。由于response对象本身是一个Writable Stream,直接用pipe()方法就实现了自动读取文件内容并输出到HTTP响应。

在命令行运行node file_server.js ./path/to/dir,把/path/to/dir改成你本地的file_server.js路径下一个有效的目录,然后在浏览器中输入http://localhost:8080/index.html

只要当前目录下存在文件index.html,服务器就可以把文件内容发送给浏览器。观察控制台输出:

1
2
3
4
F:\Node>node demo2.js ./path/to/dir
Static root dir: F:\Node\path\to\dir
Server is running at http://127.0.0.1:8080/
200 /index.html

第一个请求是浏览器请求index.html页面,后续请求是浏览器解析HTML后发送的其它资源请求。

crypto

crypto模块的目的是为了提供通用的加密和哈希算法。用纯JavaScript代码实现这些功能不是不可能,但速度会非常慢。Nodejs用C/C++实现这些算法后,通过cypto这个模块暴露为JavaScript接口,这样用起来方便,运行速度也快。

MD5和SHA1

MD5是一种常用的哈希算法用于给任意数据一个“签名”。这个签名通常用一个十六进制的字符串表示:

1
2
3
4
5
6
7
8
9
const crypto = require('crypto');

const hash = crypto.createHash('md5');

// 可任意多次调用update():
hash.update('Hello, world!');
hash.update('Hello, nodejs!');

console.log(hash.digest('hex')); // 7e1977739c748beac0c0fd14fd26a544

update()方法默认字符串编码为UTF-8,也可以传入Buffer

如果要计算SHA1,只需要把'md5'改成'sha1',就可以得到SHA1的结果1f32b9c9932c02227819a4151feed43e131aca40

还可以使用更安全的sha256sha512

Hmac
Hmac算法也是一种哈希算法,它可以利用MD5或SHA1等哈希算法。不同的是,Hmac还需要一个密钥

1
2
3
4
5
6
7
8
const crypto = require('crypto');

const hmac = crypto.createHmac('sha256', 'secret-key');

hmac.update('Hello, world!');
hmac.update('Hello, nodejs!');

console.log(hmac.digest('hex')); // 80f7e22570.

只要密钥发生了变化,那么同样的输入数据也会得到不同的签名,因此,可以把Hmac理解为用随机数“增强”的哈希算法。

AES

AES是一种常用的对称加密算法,加解密都用同一个密钥。crypto模块提供了AES支持,但是需要自己封装好函数,便于使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const crypto = require('crypto');

function aesEncrypt(data, key) {
const cipher = crypto.createCipher('aes192', key);
var crypted = cipher.update(data, 'utf8', 'hex');
crypted += cipher.final('hex');
return crypted;
}

function aesDecrypt(encrypted, key) {
const decipher = crypto.createDecipher('aes192', key);
var decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}

var data = 'Hello, this is a secret message!';
var key = 'Password!';
var encrypted = aesEncrypt(data, key);
var decrypted = aesDecrypt(encrypted, key);

console.log('Plain text: ' + data);
console.log('Encrypted text: ' + encrypted);
console.log('Decrypted text: ' + decrypted);

运行结果如下:

1
2
3
Plain text: Hello, this is a secret message!
Encrypted text: 8a944d97bdabc157a5b7a40cb180e7...
Decrypted text: Hello, this is a secret message!

可以看出,加密后的字符串通过解密又得到了原始内容。

注意到AES有很多不同的算法,如aes192aes-128-ecbaes-256-cbc等,AES除了密钥外还可以指定IV(Initial Vector)不同的系统只要IV不同,用相同的密钥加密相同的数据得到的加密结果也是不同的。加密结果通常有两种表示方法:hex和base64,这些功能Nodejs全部都支持,但是在应用中要注意,如果加解密双方一方用Nodejs,另一方用Java、PHP等其它语言,需要仔细测试。如果无法正确解密,要确认双方是否遵循同样的AES算法,字符串密钥和IV是否相同,加密后的数据是否统一为hex或base64格式。

Diffie-Hellman

DH算法是一种密钥交换协议,它可以让双方在不泄漏密钥的情况下协商出一个密钥来。DH算法基于数学原理,比如小明和小红想要协商一个密钥,可以这么做:

小明先选一个素数和一个底数,例如,素数p=23,底数g=5(底数可以任选),再选择一个秘密整数a=6,计算A=g^a mod p=8,然后大声告诉小红:p=23,g=5,A=8;

小红收到小明发来的p,g,A后,也选一个秘密整数b=15,然后计算B=g^b mod p=19,并大声告诉小明:B=19;

小明自己计算出s=B^a mod p=2,小红也自己计算出s=A^b mod p=2,因此,最终协商的密钥s为2。

在这个过程中,密钥2并不是小明告诉小红的,也不是小红告诉小明的,而是双方协商计算出来的。第三方只能知道p=23,g=5,A=8,B=19,由于不知道双方选的秘密整数a=6和b=15,因此无法计算出密钥2。

用crypto模块实现DH算法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const crypto = require('crypto');

// xiaoming's keys:
var ming = crypto.createDiffieHellman(512);
var ming_keys = ming.generateKeys();

var prime = ming.getPrime();
var generator = ming.getGenerator();

console.log('Prime: ' + prime.toString('hex'));
console.log('Generator: ' + generator.toString('hex'));

// xiaohong's keys:
var hong = crypto.createDiffieHellman(prime, generator);
var hong_keys = hong.generateKeys();

// exchange and generate secret:
var ming_secret = ming.computeSecret(hong_keys);
var hong_secret = hong.computeSecret(ming_keys);

// print secret:
console.log('Secret of Xiao Ming: ' + ming_secret.toString('hex'));
console.log('Secret of Xiao Hong: ' + hong_secret.toString('hex'));

运行后,可以得到如下输出:

1
2
3
4
5
$ node dh.js 
Prime: a8224c...deead3
Generator: 02
Secret of Xiao Ming: 695308...d519be
Secret of Xiao Hong: 695308...d519be

注意每次输出都不一样,因为素数的选择是随机的。

证书
crypto模块也可以处理数字证书。数字证书通常用在SSL连接,也就是Web的https连接。一般情况下,https连接只需要处理服务器端的单向认证,如无特殊需求(例如自己作为Root给客户发认证证书),建议用反向代理服务器如Nginx等Web服务器去处理证书。


本作品采用 知识共享署名 2.5 中国大陆许可协议 进行许可,欢迎转载,但转载请注明来自JayMo,并保持转载后文章内容的完整。本人保留所有版权相关权利。
本文永久链接:http://jaymo666.github.io/2017/06/29/node-document/