JS 学习之Node(二)

基本模块

global

Node.js 中唯一的全局对象 `global`。

process

`process` 代表 Node.js 的进程,通过它我们可以获取一些有用的信息。

> process === global.process;
true
> process.version;
'v5.2.0'
> process.platform;
'darwin'
> process.arch;
'x64'
> process.cwd(); //返回当前工作目录
'/Users/michael'
> process.chdir('/private/tmp'); // 切换当前工作目录
undefined
> process.cwd();
'/private/tmp'

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

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

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

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

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

判断 Javascript 执行环境

if (typeof(window) === 'undefined') {
    console.log('node.js');
} else {
    console.log('browser');
}

fs

异步读文件

'use strict';


var fs = require('fs');


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

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

读取二进制文件

fs.readFile('favicon.ico', function (err, data) {
  if (err) {
    console.log('出错了');
  } else {
    console.log(data);
    console.log(data[6]);
    console.log(data.length + 'bytes');
  }
});

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

Buffer 对象可以和 String 作转换。

// Buffer -> String
var text = data.toString('utf-8');
console.log(text);
// String -> Buffer
var buf = Buffer.from(text, 'utf-8');
console.log(buf);

同步读文件

同步读文件时,文件内容直接返回。

'use strict';


var fs = require('fs');


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

写文件

'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 参数。

同步写文件

'use strict';


var fs = require('fs');


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

stat

获取文件相关信息。

'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);
        }
    }
})

异步还是同步

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

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

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

stream

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

读取流

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

var stream = fs.createReadStream(file, 'UTF-8');
stream.on('data', function (chunk) {
    console.log('DATA:');
    console.log(chunk);
    
})
stream.on('end', function () {
    console.log('END');
    
})
stream.on('error', function () {
    console.log('ERROR');
    
})

写入流

var stream = fs.createWriteStream('output.txt', 'utf-8');
stream.write('使用stream写入数据\n');
stream.write('END')
stream.end();


var ws2 = fs.createWriteStream('output2.txt', 'utf-8');
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() 把一个文件流和另一个文件流串起来,这样源文件的所有数据就自动写入到目标文件里了,所以,这实际上是一个复制文件的程序:

'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 流,需要传入参数:

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

http

简单的 http 服务器,对于所有请求,返回 Hello world!

'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/

文件服务器

'use strict';


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


var root = path.resolve(process.argv[2] || '.');


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


var server = http.createServer(function (request, response) {
    var pathname = url.parse(request.url).pathname;
    var filepath = path.join(root, pathname);
    fs.stat(filepath, function(err, stats) {
        if (!err && stats.isFile()) {
            console.log('200 ' + request.url);
            response.writeHead(200);
            fs.createReadStream(filepath).pipe(response);
        } else {
            console.log('404 ' + request.url);
            response.writeHead(404);
            response.end('404 Not Found');
        }
    });
});


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

然后执行 node .\main.js http/static/

crypto

MD5和SHA1

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

const crypto = require('crypto');


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


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


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

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

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

还可以使用更安全的sha256sha512

Hmac

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

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支持,但是需要自己封装好函数,便于使用:

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: ' + decrypte

运行结果如下:

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算法基于数学原理,比如小明和小红想要协商一个密钥,可以这么做:

  1. 小明先选一个素数和一个底数,例如,素数p=23,底数g=5(底数可以任选),再选择一个秘密整数a=6,计算A=g^a mod p=8,然后大声告诉小红:p=23,g=5,A=8
  2. 小红收到小明发来的pgA后,也选一个秘密整数b=15,然后计算B=g^b mod p=19,并大声告诉小明:B=19
  3. 小明自己计算出s=B^a mod p=2,小红也自己计算出s=A^b mod p=2,因此,最终协商的密钥s2

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

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

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('he

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

$ node dh.js 
Prime: a8224c...deead3
Generator: 02
Secret of Xiao Ming: 695308...d519be
Secret of Xiao Hong: 695308...d519be

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

RSA

RSA算法是一种非对称加密算法,即由一个私钥和一个公钥构成的密钥对,通过私钥加密,公钥解密,或者通过公钥加密,私钥解密。其中,公钥可以公开,私钥必须保密。

RSA算法是1977年由Ron Rivest、Adi Shamir和Leonard Adleman共同提出的,所以以他们三人的姓氏的头字母命名。

当小明给小红发送信息时,可以用小明自己的私钥加密,小红用小明的公钥解密,也可以用小红的公钥加密,小红用她自己的私钥解密,这就是非对称加密。相比对称加密,非对称加密只需要每个人各自持有自己的私钥,同时公开自己的公钥,不需要像AES那样由两个人共享同一个密钥。

在使用Node进行RSA加密前,我们先要准备好私钥和公钥。

首先,在命令行执行以下命令以生成一个RSA密钥对:

openssl genrsa -aes256 -out rsa-key.pem 2048

根据提示输入密码,这个密码是用来加密RSA密钥的,加密方式指定为AES256,生成的RSA的密钥长度是2048位。执行成功后,我们获得了加密的rsa-key.pem文件。

第二步,通过上面的rsa-key.pem加密文件,我们可以导出原始的私钥,命令如下:

openssl rsa -in rsa-key.pem -outform PEM -out rsa-prv.pem

输入第一步的密码,我们获得了解密后的私钥。

类似的,我们用下面的命令导出原始的公钥:

openssl rsa -in rsa-key.pem -outform PEM -pubout -out rsa-pub.pem

这样,我们就准备好了原始私钥文件rsa-prv.pem和原始公钥文件rsa-pub.pem,编码格式均为PEM。

下面,使用crypto模块提供的方法,即可实现非对称加解密。

首先,我们用私钥加密,公钥解密:

const
    fs = require('fs'),
    crypto = require('crypto');


// 从文件加载key:
function loadKey(file) {
    // key实际上就是PEM编码的字符串:
    return fs.readFileSync(file, 'utf8');
}


let
    prvKey = loadKey('./rsa-prv.pem'),
    pubKey = loadKey('./rsa-pub.pem'),
    message = 'Hello, world!';


// 使用私钥加密:
let enc_by_prv = crypto.privateEncrypt(prvKey, Buffer.from(message, 'utf8'));
console.log('encrypted by private key: ' + enc_by_prv.toString('hex'));




let dec_by_pub = crypto.publicDecrypt(pubKey, enc_by_prv);
console.log('decrypted by public key: ' + dec_by_pub.toString('utf8

执行后,可以得到解密后的消息,与原始消息相同。

接下来我们使用公钥加密,私钥解密:

// 使用公钥加密:
let enc_by_pub = crypto.publicEncrypt(pubKey, Buffer.from(message, 'utf8'));
console.log('encrypted by public key: ' + enc_by_pub.toString('hex'));


// 使用私钥解密:
let dec_by_prv = crypto.privateDecrypt(prvKey, enc_by_pub);
console.log('decrypted by private key: ' + dec_by_prv.toString('utf8'));

执行得到的解密后的消息仍与原始消息相同。

如果我们把message字符串的长度增加到很长,例如1M,这时,执行RSA加密会得到一个类似这样的错误:data too large for key size,这是因为RSA加密的原始信息必须小于Key的长度。那如何用RSA加密一个很长的消息呢?实际上,RSA并不适合加密大数据,而是先生成一个随机的AES密码,用AES加密原始信息,然后用RSA加密AES口令,这样,实际使用RSA时,给对方传的密文分两部分,一部分是AES加密的密文,另一部分是RSA加密的AES口令。对方用RSA先解密出AES口令,再用AES解密密文,即可获得明文。