fs模块
Node.js内置的fs模块就是文件系统模块,负责读写文件。
和所有其它JavaScript模块不同的是,fs模块同时提供了异步和同步的方法。
** 异步读文件 **
按照JavaScript的标准,异步读取一个文本文件的代码如下:
1 | var fs = require("fs"); |
异步读取时,传入的回调函数接收两个参数,当正常读取时,err参数为null,data参数为读取到的String。当读取发生错误时,err参数代表一个错误对象,data为undefined。这也是Node.js标准的回调函数:第一个参数代表错误信息,第二个参数代表结果。
1 | if (err) { |
** 读取一个图片文件 **
1 | var fs = require("fs"); |
当读取二进制文件时,不传入文件编码时,回调函数的data参数将返回一个Buffer对象。在Node.js中,Buffer对象就是一个包含零个或任意个字节的数组(和Array不同)。
Buffer对象可以和String作转换,例如,把一个Buffer对象转换成String:
1 | var fs = require("fs"); |
同步读文件
除了标准的异步读取模式外,fs也提供相应的同步读取函数。同步读取的函数和异步函数相比,多了一个Sync后缀,并且不接收回调函数,函数直接返回结果。
用fs模块同步读取一个文本文件的代码如下:
1 | var fs = require('fs'); |
如果同步读取文件发生错误,则需要用try…catch捕获该错误:
1 | try { |
** 写文件 **
将数据写入文件是通过fs.writeFile()实现的:
1 | var fs = require('fs'); |
writeFile()的参数依次为文件名、数据和回调函数。如果传入的数据是String,默认按UTF-8编码写入文本文件,如果传入的参数是Buffer,则写入的是二进制文件。回调函数由于只关心成功与否,因此只需要一个err参数。
和readFile()类似,writeFile()也有一个同步方法,叫writeFileSync():
1 | var fs = require('fs'); |
如果我们要获取文件大小,创建时间等信息,可以使用fs.stat(),它返回一个Stat对象,能告诉我们文件或目录的详细信息:
1 | var fs = require('fs'); |
异步还是同步
在fs模块中,提供同步方法是为了方便使用。那我们到底是应该用异步方法还是同步方法呢?
由于Node环境执行的JavaScript代码是服务器端代码,所以,绝大部分需要在服务器运行期反复执行业务逻辑的代码,必须使用异步代码,否则,同步代码在执行时期,服务器将停止响应,因为JavaScript只有一个执行线程。
服务器启动时如果需要读取配置文件,或者结束时需要写入到状态文件时,可以使用同步代码,因为这些代码只在启动和结束时执行一次,不影响服务器正常运行时的异步执行。
stream
是Node.js提供的又一个仅在服务区端可用的模块,目的是支持“流”这种数据结构。
流用来读取数据,比如从文件读取数据时,可以打开一个文件流,然后从文件流中不断地读取数据。有些流用来写入数据,比如向文件写入数据时,只需要把数据不断地往文件流中写进去就可以了。
流也是一个对象,我们只需要响应流的事件就可以了:data事件表示流的数据已经可以读取了,end事件表示这个流已经到末尾了,没有数据可以读取了,error事件表示出错了。
1 | var fs = require('fs'); |
要以流的形式写入文件,只需要不断调用write()方法,最后以end()结束:
1 | var fs = require('fs'); |
读取数据的流都继承自stream.Readable,写入的流都继承自stream.Writable。
** pipe **
管道:一个Readable流和一个Writable流串起来后,所有的数据自动从Readable流进入Writable流,这种操作叫pipe。
在Node.js中,Readable流有一个pipe()方法,把一个文件流和另一个文件流串起来,这样源文件的所有数据就自动写入到目标文件里了,实际上是一个复制文件的程序:
1 | var fs = require('fs'); |
默认情况下,当Readable流的数据读取完毕,end事件触发后,将自动关闭Writable流。如果我们不希望自动关闭Writable流,需要传入参数:
readable.pipe(writable, { end: false });
http
HTTP服务器
HTTP服务器程序,从头处理TCP连接,解析HTTP是不现实的。http模块提供的request和response对象。request对象封装了HTTP请求,我们调用request对象的属性和方法就可以拿到所有HTTP请求的信息;response对象封装了HTTP响应,我们操作response对象的方法,就可以把HTTP响应返回给浏览器。
用Node.js实现一个HTTP服务器程序:
1 | var http = require('http'); |
文件服务器
设定一个目录,然后让Web程序变成一个文件服务器。只需要解析request.url中的路径,然后在本地找到对应的文件,把文件内容发送出去就可以了。
解析URL需要用到Node.js提供的url模块,通过parse()将一个字符串解析为一个Url对象:
1 | var url = require('url'); |
处理本地文件目录需要使用Node.js提供的path模块
1 | var path = require('path'); |
文件服务器Demo:
1 | var |
crypto
crypto模块的目的是为了提供通用的加密和哈希算法。用纯JavaScript代码实现这些功能速度会非常慢。Nodejs用C/C++实现这些算法后,通过cypto这个模块暴露为JavaScript接口,这样用起来方便,运行速度也快。
MD5和SHA1
1 | const crypto = require('crypto'); |
Hmac
Hmac算法也是一种哈希算法,它可以利用MD5或SHA1等哈希算法。不同的是,Hmac还需要一个密钥:
1 | const crypto = require('crypto'); |
只要密钥发生了变化,那么同样的输入数据也会得到不同的签名,因此,可以把Hmac理解为用随机数“增强”的哈希算法。
AES
AES是一种常用的对称加密算法,加解密都用同一个密钥。crypto模块提供了AES支持,但是需要自己封装好函数,便于使用:
1 | const crypto = require('crypto'); |
结果:
1 | Plain text: Hello, this is a secret message! |
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 | const crypto = require('crypto'); |
RSA
RSA算法是一种非对称加密算法,即由一个私钥和一个公钥构成的密钥对,通过私钥加密,公钥解密,或者通过公钥加密,私钥解密。其中,公钥可以公开,私钥必须保密。
在使用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模块提供的方法,即可实现非对称加解密。
私钥加密,公钥解密:
1 | const |
公钥加密,私钥解密:
1 | // 使用公钥加密: |