1

内容

5.内存控制
6.理解Buffer
7.网络编程

一、内存控制

1.V8的垃圾回收机制与内存限制

Node基于垃圾回收机制进行内存的自动管理。这种机制,在浏览器环境下,几乎是完美的,但是同java一样,在后端运行的node,如果想要更完美的运行,依然需要判断和管理内存,内存管理的好坏、垃圾回收状况是否优良,都会直接影响服务器的性能。

1)node 与 V8

Node在JavaScript的执行上直接受益于V8,可以随着V8的升级就能享受到更好的性能或新的语言特性(如ES5和ES6)等,
同时也受到V8的限制,尤其是内存显示。

2)V8的内存限制

一般的后台开发语言中,内存使用的大小几乎没有限制。但是,V8最初是为浏览器打造的,在V8下,64位系统可以操纵1.4GB内存,32位系统可以操纵0.7GB内存。在这样的限制下,node几乎不能直接操纵大内存。

3)V8的对象分配

在V8中所有的js对象都是通过堆来进行分配的,可以使用node提供的V8内存使用量的查看方式查看内存分配及使用状况:

// 1.rss:resident set size,进程的常驻内存
// 2.heapTotal: 已经申请到的堆内存。
// 3.heapUsed: 当前堆内存使用量。
$ node
> process.memoryUsage();
{ rss: 14958592,
heapTotal: 7195904,
heapUsed: 2821496 }

clipboard.png

调整内存限制大小:

node --max-old-space-size=1700 test.js // 设置老生代内存空间的最大值,单位为MB
// 或者
node --max-new-space-size=1024 test.js //  设置新生代内存空间的最大值,单位为KB

限制堆内存原因:v8垃圾回收机制的限制,以1.5G垃圾回收队内存为例,v8做一次小垃圾回收需要50毫秒以上,做一次非增量的垃圾回收甚至要1秒以上,这是垃圾回收引起JavaScipt线程暂停执行的时间,这使得应用的性能和响应能力都会直接下降。

4)V8的垃圾回收机制
v8的垃圾回收策略主要基于分代式垃圾回收机制。按照对象的存活时间将内存的垃圾回收进行不同的分代,然后,分别对不同的分代的内存再进行高效的垃圾回收算法。
1.v8的内存分代
在V8中,主要将内存分为新生代和老生代两代。新内存中的对象存活时间短,老内存中的对象存活时间长或常驻内存对象。
clipboard.png

2.新生代垃圾回收算法scavenge算法
新生代中的对象主要通过scavenge算法进行垃圾回收,其主要是采用cheney算法进行具体处理。

cheney算法采用一种复制方式的垃圾回收算法,将堆内存一分为二,只有一部分空间被使用称为From空间,另一个处于闲置称为To空间。当进行分配对象的时候先在from空间分配,当进行垃圾回收时,会检查from空间中的存活对象,将这些存活对象复制到to空间中,复制完成后From和to空间角色互换,清空to空间,在垃圾回收过程中就是通过将存活对象在两个空间中进行复制。

  • 缺点: 只能使用一半的内存
  • 优点: 只复制存活的对象,对于生命周期短的场景存活对象只占小部分,所以时间效率高

当一个对象经过多次复制依然存活时,就会被认为是生命周期较长的对象,会被移入老生代内存中。

对于移入老生代内存有两个条件:

  • 对象已经经过新生代内存回收机制的回收依然存活
  • 复制到To空间的对象超过25%(为什么是25%?这个To空间接下来会成为From空间并接受内存分配,如果占比过高影响后续分配)

老内存垃圾回收算法Mark-Sweep & Mark-Compact

采用标记清除,它分为标记清除两个阶段
在标记阶段遍历所有的对象并标记活着的对象,在清除阶段只清除死亡的对象,死亡对象在老生代内存只占一小部分。老生代内存进行一次清除后,内存空间会出现不连续的状态,所以清理完成需要进行一步标记整理。、
Incremental Marking
为了避免出现javaScript应用逻辑与垃圾回收器看到不一致的情况,垃圾回收都要将应用逻辑停下来,这种行为会造成停顿,在新生代垃圾回收过程中因为存活对象比较少,即使停顿基本影响不大。在老生代垃圾回收中,通常存活对象较多,全堆垃圾回收的标记、清除、整理影响较大。
解决办法:分批次进行,拆分成许多小步,每进行一小步就让逻辑运行一会

clipboard.png

5)查看垃圾回收日志
在启动时时加入 参数 --trace_gc。
还可以在启动时增加--prof参数,来得到v8执行时的性能分析数据,其中包含了垃圾回收执行时占用的时间等。

2.高效使用内存

1)作用域
只被局部变量引用的对象存活周期较短。将会分配到新生代中的From空间中,在作用域释放后,局部变量失效,其引用的对象将会在下次垃圾回收时被释放。
1.标识符查找

js在执行时会先查找该变量定义在哪里。它最先查找的是当前作用域,如果在当前作用域中无法找到该变量的声明,将会向上级的作用域里查找,直到查到为止。

2.作用域链
这个查找过程,就是一个作用域链的查找过程

var foo = function () {
    var local = 'local var';
    var bar = function () {
        var local = 'another var';
        var baz = function () {
            console.log(local);
        };
        baz();
    };
    bar();
};
foo();

local变量在baz()函数形成的作用域中查找不到,会到bar形成的作用域中寻找,以此类推,逐渐向上寻找,一直查到全局作用域。由于标识符查找的方向是自内而外的,也就是向上的,因此,变量只能向外访问,不能向内访问。

3.变量的主动释放
如果变量是全局变量,那么,全局作用域要等进程全部退出才会释放,此时将会导致引用的对象常驻内存。如果需要释放常驻内存的对象,可以使用delete来删除,或者将变量重新赋值让旧的对象脱离引用关系。我们来看一下主动清除和整理老内存的一段代码:

global.foo = "I am global object";
console.log(global.foo); // => "I am global object"
delete global.foo;
// 或者重新赋值
global.foo = undefined; // or null
console.log(global.foo); // => undefined

其他的变量主动释放都可以用这个方法,同时由于delete会干扰v8的优化,因此,采用赋空值的方式,比较稳妥。

2)闭包
在js中实现外部作用域访问内部作用域的方法叫做闭包。

var foo = function () {
    var bar = function () {
        var local = "局部变量";
        return function () {
            return local;
        };
    };
    var baz = bar();
    console.log(baz());
};

闭包是通过中间函数进行间接访问内部变量实现的一个功能,一旦变量引用这个中间函数,这个中间函数将不会释放,同时也会使原始的作用域不会得到释放,作用域中产生的内存占用也不会得到释放。除非不再有引用,才会逐步释放。

3)*小结
可以利用闭包和垃圾回收的机制,来存储一些需要存活时间长一些的对象,并将其作为公共访问的数据区域来使用。但是,闭包和全局变量的使用还是要小心,由于无法及时回收内存,这会增加常驻内存的产生,会导致老生代中的对象增多。

3.内存指标

1)查看内存使用情况

  • 查看进程的内存占用:process.memoryUsage()
  • 查看系统的内存占用:使用os模块的totalmem()[系统的总内存],freemen()[系统的闲置内存]

2)堆外内存

堆中的内存用量总是小于进程的常驻内存用量,这意味着Node中的内存使用并非都是通过V8进行分配的,这些不通过V8分配的内存,称为堆外内存。
例如:buffer对象不经过v8内存分配,因此,也不会有堆内存的大小限制。

3)小结
Node的内存主要由通过V8进行分配的部分和Node 自行分配的部分,受V8的垃圾回收限制的主要是V8的堆内存。

4.内存泄漏

内存泄漏的实质就是应当回收的对象因为意外没有被回收,变成了常驻在老生代中的对象。
造成内存泄漏的主要原因有:缓存、队列消费不及时、作用域未释放。

1)慎将内存当做缓存
缓存十分节省资源,因为它的访问比IO效率要高,一旦命中缓存,就可以节省一次IO时间。
但是在Node中,缓存并非物美价廉,一旦一个对象被当做缓存来使用,那它将会常驻在老生代中,这将导致垃圾回收在进行扫描和整理时,对这些对象做无用功。
v8内存是通过垃圾回收进行处理的,没有过期策略,而真正的缓存是存在过期策略的。

缓存限制策略
将结果记录在数组中,一旦超过数量,就以先进先出的方式进行淘汰。如果需要更高效的缓存,可以参与LRU算法,地址为:https://github.com/isaacs/nod...

另一个案例在于模块机制,由于模块的缓存机制,模块是常驻老生代的,需要添加清空队列的相应接口,以供调用者释放内存。

(function (exports, require, module, __filename, __dirname) {
var local = "局部变量";
exports.get = function () {
return local;
};
});

//每次调用时都会造成内存增长
var leakArray = [];
exports.leak = function () {
leakArray.push("leak" + Math.random());
};

缓存的解决方案

进程间是无法共享内存的,因此,使用内存作为缓存不是一个好的解决方案。最好的解决方案是使用外部缓存,例如redis等。这些缓存可以将缓存的压力从内存转移到进程的外部,减少常驻内存的对象数量,让垃圾回收更有效率,同时,还可以实现进程间共享缓存,节约宝贵的资源。

2)关注队列状态

因为一般情况下,消费的速度要远远高于生产的速度,因此,不容易产生内存泄漏,不过一旦发生内存泄漏,将会造成内存堆积。

例如,日志写入数据库的这种情况,因为数据库写入速度低于日志的生产速度,造成了数据库写入请求的堆积,进而造成内存溢出。

解决方案是监控队列的长度,一旦产生堆积,应当通过监控系统报警,同时,设置合理的超时机制,一旦超时,通过回调函数传递超时异常。
例如bagpipe的超时模式和拒绝模式。

5.内存泄漏排查

定位Node应用的内存泄漏常用工具如下:

工具 说明
v8-profiler 可以对v8堆内存抓取快照,并对cpu进行分析
node-heapdump 可以对v8堆内存抓取快照,用于事后分析
node-mtrace 使用gcc的mtrace工具来分析堆的使用
dtrace 在smartos上使用的内存分析工具
node-memwatch 采用wtfpl许可发布的内存分析工具

6.大内存应用

使用流的方式操作大内存,也就是使用stream模块。这个模块继承了eventemitter,具备基本的自定义事件功能,同时抽象出标准的事件和方法。它分可读和可写两种。node中大多数模块都有stream应用,例如fs的createReadStream()和createWriteStream(),process模块的stdin和stdout。
由于V8的内存限制无法通过fs.readFile()和fs.writeFile()直接读取大文件,而要使用fs的createReadStream()和createWriteStream()来读取,我们看个例子:

var reader = fs.createReadStream('in.txt');
var writer = fs.createWriteStream('out.txt');
reader.on('data', function (chunk) {
    writer.write(chunk);
});
reader.on('end', function () {
    writer.end();
});

//或者

var reader = fs.createReadStream('in.txt');
var writer = fs.createWriteStream('out.txt');
reader.pipe(writer);

因为流使用了buffer作为读写的编码方式,因此,不受v8内存的限制。但是,物理内存依然有限制。

二、理解Buffer

因为在node中需要处理网络协议、操作数据库、处理图片、接受上传文件等,在网络流和文件操作中,还要处理大量二进制数据,js自有的字符串远远不能满足这些需求,于是Buffer对象应运而生。

1.Buffer结构

Buffer是一个像Array的对象,但是它主要用于操作字节。

1)模块结构
buffer是一个典型的js与c++结合的模块,将性能相关的部分用c++实现,将非性能相关的部分用js实现。同时buffer也是node的核心模块,可以直接使用,并且,buffer属于堆外内存,可以通过自己管理其垃圾回收。当然,buffer对象的管理还是在堆内,再由这个对象去管理堆外的内存。
由于Buffer太常见,Node在进程启动时已经加载了它,并将其放在全局对象上,所以在使用Buffer时,无须通过require()即可直接使用。

clipboard.png

2)Buffer对象
buffer对象类似于数组,他的元素都是16进制的两位数,即0~255的数值。

//不同编码的字符串,占用的元素个数也不相同,中文字在UTF-8下占用3个元素,字母和半角标点符号占用1个元素。
var str = "深入浅出node.js";
var buf = new Buffer(str, 'utf-8');
console.log(buf);
// => <Buffer e6 b7 b1 e5 85 a5 e6 b5 85 e5 87 ba 6e 6f 64 65 2e 6a 73>

我们可以调用length属性,得到buffer对象的长度,还可以通过下标访问元素。

var buf = new Buffer(100);
console.log(buf.length); // => 100
console.log(buf[10]);//0

//我们给buffer元素赋值
buf[10] = 100;
console.log(buf[10]); // => 100  
buf[20] = -100;
console.log(buf[20]); // 156  -100+256=156
buf[21] = 300;
console.log(buf[21]); // 44    300-256=44
buf[22] = 3.1415;
console.log(buf[22]); // 3     舍弃小数部分

3)Buffer内存分配
Buffer对象的内存分配不是在V8的堆内存中,而是在Node的C++层面实现内存的申请的。
因为处理大量的字节数据不能采用需要一点内存就向操作系统申请一点内存的方式,这可能造成大量的内存申请的系统调用,对操作系统有一定压力。
Node在内存的使用上应用的是在C++层面申请内存,在js中分配内存的策略。

node采用了slab的分配机制,slab其实就是一块申请好的固定内存区域,它有3种状态:

  • full:完全分配状态
  • partial:部分分配状态
  • empty:没有被分配状态

分配指定大小的Buffer对象: new Buffer(size);
node以8KB为界限来区分Buffer是大对象还是小对象的:Buffer.poolSize = 8 * 1024;

1.分配小Buffer对象

如果指定的buffer的大小小于8kb,node会按照小对象的方式进行分配。
如果slab的剩余空间不够本次分配,则会构造一个新的slab,原slab中剩余的空间将会造成浪费。例如:

new Buffer(1);
new Buffer(8192);

2.分配大Buffer对象
大于8kb的buffer对象,会被分配一个SlowBuffer对象作为slab单元,这个slab单元将被这个大的Buffer对象独占。

// Big buffer, just alloc one
this.parent = new SlowBuffer(this.length);
this.offset = 0;

这里的SlowBuffer类是在C++中定义的,虽然引用buffer模块可以访问到它,但是不推荐直接操纵它,而是用buffer替代。上面提到的buffer对象都是js层面的,能够被v8标记回收,但是其内部的parent属性指向的SlowBuffer对象却来自Node的c++模块,是c++层面的buffer对象,所用的这部分内存不在v8的堆中。

3.小结

真正的buffer内存是在node的c++层面提供的,js层面只是使用它。当进行小而频繁的buffer操作时,采用slab的机制进行预先申请和事后分配,使得js到操作系统之间不必有过多的内存申请方面的系统调用。对于大块的buffer而言,直接使用c++层面提供的内存,无需频繁的分配操作。

2.Buffer的转换

Buffer对象可以和字符串进行相互转换,支持的编码类型有:ASCII、UTF-8、UTF-16LE/UCS-2、Base64、Binary、Hex
1)字符串转Buffer
new Buffer(str,[encoding]);encoding默认为utf-8类型的编码和存储。
写入的方法是:buf.write(string,[offset],[length],[encoding])

2)Buffer转字符串
buf.toString([encoding], [start], [end]) encoding默认为utf-8

3)Buffer不支持的编码类型
Buffer.isEncoding(encoding) 是否支持某种编码

对于不支持的编码格式,可以使用iconv和iconv-lite来解决。

3.Buffer的拼接

1)乱码是如何产生的

// data事件中获取的chunk对象其实就是buffer对象。
var fs = require('fs');
//我们限定可读流的每次读取的buffer长度限制为11
var rs = fs.createReadStream('test.js', {highWaterMark: 11});
var data = '';
rs.on("data", function (chunk){
    data += chunk;//data = data.toString() + chunk.toString();
});
rs.on("end", function () {
    console.log(data);//床前明��光,疑���地上霜。举头��明月,���头思故乡。
});


2)setEncoding() 与 string_decoder()

为了解决上文中的乱码问题,我们应该设置一些编解码格式:setEncoding()和string_decoder()
通过这个方法,我们传递的不再是buffer对象,而是编码后的字符串了

var fs = require('fs');
var rs = fs.createReadStream('test.js', {highWaterMark: 11});
rs.setEncoding('utf8');
var data = '';
rs.on("data", function (chunk){
data += chunk;
});
rs.on("end", function () {
console.log(data);//床前明月光,疑是地上霜。举头望明月,低头思故乡。
});

这个过程中,也就是调用setEncoding(),可读流在内部设置了decoder对象,这个对象来自于string_decoder模块的StringDecoder对象实例,
因为基于StringDecoder得到的编码,直到utf-8的宽字符是3个字节,因此会将前3个汉字先输出,也就是先输出9个字节,然后将月字的前两个字节保留在StringDecoder实例内部,再和后续的字节进行拼接。它目前支持utf-8、base64、ucs-2、utf-16le等,其他的没有支持的编解码格式,还是需要字节手工控制。

var StringDecoder = require('string_decoder').StringDecoder;
var decoder = new StringDecoder('utf8');
var buf1 = new Buffer([0xE5, 0xBA, 0x8A, 0xE5, 0x89, 0x8D, 0xE6, 0x98, 0x8E, 0xE6, 0x9C]);
console.log(decoder.write(buf1));
// =>床前明
var buf2 = new Buffer([0x88, 0xE5, 0x85, 0x89, 0xEF, 0xBC, 0x8C, 0xE7, 0x96, 0x91, 0xE6]);
console.log(decoder.write(buf2));
// => 月光,疑

3)正确拼接Buffer
正确的拼接方式,是用一个数组来存储接收到的所以buffer片段,然后调用buffer.concat()合成一个buffer对象。concat还实现了从小对象buffer向大对象buffer复制的过程

var chunks = [];
var size = 0;
res.on('data', function (chunk) {
    chunks.push(chunk);
    size += chunk.length;
});
res.on('end', function () {
    var buf = Buffer.concat(chunks, size);
    var str = iconv.decode(buf, 'utf8');
    console.log(str);
});

4.Buffer与性能

buffer在文件io和网络io中具有广泛应用,不管是什么对象,一旦进入到网络传输中,都需要转换为buffer,然后以二进制进行数据传输。因此,提供io效率,可以从buffer转换入手。
在构建web服务时,将页面的动态内容和静态内容进行分离,静态内容可以通过先转换为buffer的方式,提升传输性能。
文件读取
文件读取时需要设置好highWaterMark参数。也就是我们在fs.createReadStream(path,opts)时,可以传入一些参数:

{
flags: 'r',
encoding: null,
fd: null,
mode: 0666,
highWaterMark: 64 * 1024
}

还可以设置start和end来指定读取文件的位置范围:{start: 90, end: 99}

在理想状态下,每次读取的长度都是用户指定的highWaterMark,剩余的还可分配给下一次。pool是常驻内存的,只有当pool单元神域数量小于128(kMinPoolSpace)字节时,才会重新分配一个buffer对象,我们来看一下源代码:

highWaterMark的大小对性能的影响:

  • highWaterMark设置对buffer内存的分配和使用有一定影响(文件读取基于buffer分配,buffer基于Slowbuffer分配,如果文件过小,则可能造成slab的浪费。)
  • highWaterMark设置过小,可能导致系统调用次数过多

三、网络编程

Node提供了net、dgrm、http、https这四个模块,分别用于处理TCP、UDP、HTTP、HTTPS,适用于服务器端和客户端。

1.构建TCP服务

1)TCP
TCP全称为传输控制协议,在OSI模型上属于传输层协议。

七层协议示意图如下:

clipboard.png

TCP是面向连接的协议,其显著特征是在传输之前需要3次握手形成会话。
clipboard.png
只有会话形成后,服务器端和客户端才能相互发送数据。在创建会话的过程中,服务器端和客户端分别提供一个套接字,
这两个套接字共同形成一个连接。服务器端和客户端则通过套接字实现两者之间连接的操作。

2)创建TCP服务器端

//创建TCP服务器端来接受网络请求
var net = require('net');
var server = net.createServer(function (socket) {
    // 新的连接
    socket.on('data', function (data) {
        socket.write("hello") ;
    });
    socket.on('end', function () {
        console.log('连接断开');
    });
    socket.write("hello world\n");
});
server.listen(8124, function () {
    console.log('server bound');
});

//为了体现listener是连接事件connection的监听器,也可以采用另外一种方式进行监听
var server = net.createServer();
server.on('connection', function (socket) {
 // 新的连接
});
server.listen(8124);

可以使用telnet作为客户端,对服务进行会话交流

$ telnet 127.0.0.1 8124
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
hello world
hi
hello

除了端口外,我们还可以使用Domain Socket进行监听。

server.listen('/tmp/echo.sock');

通过net模块自己构建客户端进行会话

var net = require('net');
// console.log(net)
var client = net.connect({ port: 8124 }, function () { //'connect' listener
    console.log('client connected');
    client.write('world!\r\n');
});
client.on('data', function (data) {
    console.log(data.toString());
    client.end();
});
client.on('end', function () {
    console.log('client disconnected');
});

//如果是domain socket 可以这样写

var client = net.connect({path: '/tmp/echo.sock'});

3)TCP服务的事件
主要是服务器事件和连接事件。
1.服务器事件
通过net.createServer()创建的服务器,它是一个eventEmitter实例,也是stream实例,有如下事件:

  • listening,在调用server.listen()绑定端口或者domain socket后触发,可以写为:server.listen(port,listeningListener)
  • connection,每个客户端套接字连接到服务器端时触发,简介写法为net.createServer(),最后一个参数传递。
  • close,调用server.close()后会停止接收新的套接字连接,保持当前存在的连接,等待所以连接都断开后,触发该事件
  • error,服务器发生异常时,将会触发事件,比如侦听一个使用中的端口,将会触发一个异常,如果不侦听error事件,服务器将会抛出异常。

2.连接事件
服务器可以同时与多个客户端保持连接,对于每个连接而言是可写可读Stream对象。
Stream对象可以用于服务器端和客户端之间的通信,可以通过data事件从一端读取另一端发来的数据,也可以通过write()从一端向另一端发送数据。

  • data,当一端调用write()发送数据时,另一端会触发data事件,事件传递的数据即是write()发送的数据。
  • end,任意一端发送FIN数据,另一端将会触发该事件。
  • connect,客户端与服务器连接成功后,客户端触发该事件
  • drain,当任意一端调用write()时,当前这端会触发该事件
  • error,异常触发该事件
  • close,当套接字完全关闭时,触发该事件
  • timeout,当一定时间后,连接不活跃,将触发该事件,告知当前用户,该连接已经被闲置了。

3.管道操作
由于TCP套接字是可写可读的Stream对象,可以利用pipe()实现管道操作。

var net = require('net');
var server = net.createServer(function (socket) {
socket.write('Echo server\r\n');
socket.pipe(socket);
});
server.listen(1337, '127.0.0.1');

tcp针对网络中的小数据包有优化政策,nagle算法,nagle要求网络中缓冲区数据达到一定数量或一定时间后,才将其触发,小数据包会被nagle合并,来优化网络。这个方法会带来一定的传输延迟。
我们可以通过socket.setNoDelay(true)来去掉nagle算法,使得write()可以立即发送数据。但是,data事件还是要进行小包合并后触发的,这个需要注意。

2.构建UDP服务

udp,用户数据包协议,也是传输层协议。udp不是面向连接的,也就是说udp无需连接,它是面向事务的简单不可靠信息传输服务,在网络差的情况下存在丢包严重的问题,由于无需连接,资源消耗低,处理块速且灵活,常常用于那种偶尔丢几个包也不产生重大影响的场景,例如,音频、视频等,DNS就是基于udp实现的。另外,一个udp套接字可以与多个udp服务进行通信。

1)创建UDP套接字
UDP套接字创建成功后,可以作为客户端发送数据,也可以作为服务器端接收数据。

var dgram = require('dgram');
var socket = dgram.createSocket("udp4");

2)创建UDP服务器端
若想让UDP套接字接收网络消息,只要调用dgram.bind(port,[address])对网卡和端口进行绑定即可。

var dgram = require("dgram");
var server = dgram.createSocket("udp4");
server.on("message", function (msg, rinfo) {
    console.log("server got: " + msg + " from " +
        rinfo.address + ":" + rinfo.port);
});
server.on("listening", function () {
    var address = server.address();
    console.log("server listening " +
        address.address + ":" + address.port);
});
server.bind(41234);

3)创建UDP客户端
udp是无需建立连接的,因此,高效快速不可靠。

var dgram = require('dgram');
var message = new Buffer("hi");
var client = dgram.createSocket("udp4");

//socket.send(buf, offset, length, port, address, [callback])
//socket.send(要发送的buf, buf的偏移, buf长度, port, address, [callback])
client.send(message, 0, message.length, 41234, "localhost", function(err, bytes) {
client.close();
});

//输出如下:

$ node server.js
server listening 0.0.0.0:41234
server got: hi from 127.0.0.1:58682

4)UDP套接字事件
udp socket只是一个eventemitter实例,不是stream实例,事件如下:

  • message,当udp套接字侦听网卡端口后,接收到消息时触发该事件,触发携带的数据为消息Buffer对象和一个远程地址信息
  • listening,udp开始监听时,触发该事件
  • close,调用close()时触发该事件,并不再触发message事件,如需再次触发message事件,重新绑定即可
  • error,异常触发该事件,如果不监听,异常将直接抛出,使进程退出

3.构建HTTP服务

我们将会使用node的核心模块http和https进行构建,这两个模块分别对http和https协议进行了抽象和封装,最大限度的模拟http协议和https协议的行为。

var http = require('http');
http.createServer(function (req, res) {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('Hello World\n');
}).listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');

1)HTTP
1.初识HTTP
HTTP是超文本传输协议,英文写作HyperText Transfer Protocol,它是构建在TCP协议之上的。在http的两端分别是客户端和服务器,这就是经典的B/S模式。另外,这里的B,就是浏览器的意思,浏览器成为了http的代理,用户的行为将会通过浏览器转化为http请求报文,发送给服务器,服务器也就是S,会处理请求,然后发送响应报文给代理,也就是浏览器,浏览器解析响应报文后,将用户界面展示给用户。这里我们看到,基于http或者https的B/S模式中国,浏览器只负责发送报文、接收报文、解析报文、展示界面,服务器负责处理http请求和发送http响应。

2.HTTP报文

采用curl工具,查看这次网络通信的所有报文信息。报文分为四部分。

$ curl -v http://127.0.0.1:1337
//第一部分:是经典的TCP三次握手,这样就建立了连接
* About to connect() to 127.0.0.1 port 1337 (#0)
* Trying 127.0.0.1...
* connected
* Connected to 127.0.0.1 (127.0.0.1) port 1337 (#0)
//第二部分:在完成握手之后,客户端向服务器端发送请求报文。
> GET / HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5
> Host: 127.0.0.1:1337
> Accept: */*
>
//第三部分:服务器端完成处理后,向客户端发送的响应内容,包括响应头和响应体。
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Date: Sat, 06 Apr 2013 08:01:44 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
<
Hello World
第四部分:结束会话的信息
* Connection #0 to host 127.0.0.1 left intact
* Closing connection #0

注意:报文的内容主要是两部分,报文头和报文体,上一个例子中,使用的是get请求,报文头的部分是上边报文信息中>和<的部分。在响应报文中,有一个报文体,是Hello World。

2)http模块
Node 的http模块包含对http处理的封装。 在node中,http服务继承tcp服务器(net模块),它能够与多个客户端保持连接,由于采用事件驱动的方式,因此,并不为每一个连接创建额外的线程或进程,保持很低的内存占用,所以能实现高并发。

http服务与tcp服务模型有区别的地方在于,在开启keepalive之后,一个tcp会话可以用于多次请求和响应,tcp服务以connection为单位进行服务,http以request为单位进行服务。http模块也就是将connection到request的过程进行了封装。

clipboard.png

http模块将连接所用的套接字的读写抽象为ServerRequest和ServerResponse对象,在请求产生的过程中,http模块拿到连接中传来的数据,调用二进制模块http_parser进行解析,在解析完请求报文的报文头后,触发request事件,之后调用用户的业务逻辑。
clipboard.png

处理程序对应的代码就是响应Hello World这部分

function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Hello World\n');
}

1.HTTP请求

对于tcp连接的读操作,http模块将其封装为ServerRequest对象,我们再来看看报文头,此处报文头会被http_parser进行解析:

> GET / HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5
> Host: 127.0.0.1:1337
> Accept: */*
>

第一行报文头GET / HTTP/1.1会被解析为,如下属性:

属性 说明
req.method 值为GET,也就是req.method='GET',这个就是请求方法,我们常见的请求方法有GET、POST、DELETE、PUT、CONNECT等
req.url 值为/,也就是req.url='/'
req.httpVersion 值为1.1,也就是req.httpVersion='1.1'

其余的报文头都会被解析为很有规律的json,也就是key和value。这些值,被解析到req.headers属性上。

headers:
{ 'user-agent': 'curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5',
host: '127.0.0.1:1337',
accept: '*/*' }

报文体部分则被抽象为一个只读流对象,如果业务逻辑需要读取报文体中的数据,则要在这个数据流结束后才能进行操作:

function (req, res) {
    // console.log(req.headers);
    var buffers = [];
    req.on('data', function (trunk) {
        buffers.push(trunk);
    }).on('end', function () {
        var buffer = Buffer.concat(buffers);
        // TODO
        res.end('Hello world');
    });
}

2.HTTP响应

http响应,也就是对套接字的写操作进行了封装,可以将其看成一个可写的流对象。
影响响应报文头部信息的API是res.setHeader()和res.writeHead()。

我们可以多次调用setHeader进行多次设置,但是只能调用一次writeHead,并且也只有调用了writeHead后,才会将响应报文头写入到连接中,除此之外,http模块还会自动帮你设置一些头信息:

< Date: Sat, 06 Apr 2013 08:01:44 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
<

报文体部分则是通过调用res.write()和res.end()实现的,res.end()会先调用write()发送数据,然后,发送信号通知服务器这次响应结束,响应的结果就是我们之前发送的hello world。

响应结束后,http服务器可能会将当前的连接用于下一个请求,或者关闭连接。另外,报头是在报文体发送前发送的,一旦开始了数据的发送,再次调用writeHead和setHead将不再生效。

另外,服务器不管是完成业务,还是发生异常,都应该调用res.end()以结束请求,否则客户端将会一直处于等待的状态。当然,也可以通过延迟res.end()的方式,来实现与客户端的长连接,但是结束时,务必关闭连接。

3.HTTP服务的事件
http服务也继承了events模块,因此也是一个EventEmitter实例。

  • connection : 在http请求和响应前,客户端与服务器需要建立tcp连接,这个连接可能因为开启了keep-alive,可以在多次响应和请求之间使
    用,当建立连接时,服务器触发一次connection事件
  • request : 建立tcp连接后,http模块底层将在数据流中抽象http请求和响应,当请求数据发送到服务器,在解析出http请求头后,将会触发该
    事件,在res.end()后,tcp连接可能用于下一次请求响应
  • close : 与tcp服务器的行为一致,调用server.close()停止接受新的连接,当已有的连接都断开时,触发该事件,可以给server.close()传递
    一个回调函数,来快速注册该事件。
  • checkContinue : 客户端发送较大的数据时,不会将数据直接发送,而是先发送一个头部带Expect:100-continue的请求到服务器,服务器将会 触发checkContinue事件,如果没有为服务器监听这个事件,服务器将会自动响应客户端100 Continue的状态码,表示接受数据上传,如果不接 受的数据较多时,响应客户端400 Bad Request,拒绝客户端继续发送数据即可。需要注意的是,该事件发生时不会触发request事件,两个事件是 互斥的,当客户端收到100 Continue后,重新发起请求时,才会触发request事件
  • connect : 当客户端发起CONNECT请求时触发,而发起CONNECT请求,通常在HTTP代理出现,如果不监听该事件,发起该请求的连接将会关闭
  • upgrade : 当客户端要求升级连接协议时,需要和服务器端协商,客户端会在请求头中带上Upgrade字段,服务器端会在接收到这样的请求时触发
    该事件,这个会在websocket中详细介绍,同样,如果不监听该事件发起该请求的连接将会关闭。
  • clientError : 连接的客户端触发error事件,这个错误会传递到服务器端,此时触发该事件。

3)HTTP客户端
http客户端会产生请求报文头和报文体,接收响应报文头和报文体,并解析。除了浏览器,我们也可以通过http模块提供的http.request(options,connect)来构造http客户端。

var http = require('http');
var options = {
   host:'127.0.0.1',
    hostname: '127.0.0.1',
    port: 1334,
    path: '/',
    method: 'GET'
};
var req = http.request(options, function (res) {
    console.log('STATUS: ' + res.statusCode);
    console.log('HEADERS: ' + JSON.stringify(res.headers));
    res.setEncoding('utf8');
    res.on('data', function (chunk) {
        console.log(chunk);
    });
});
req.end();

//输出:

$ node client.js
STATUS: 200
HEADERS: {"date":"Sat, 06 Apr 2013 11:08:01
GMT","connection":"keep-alive","transfer-encoding":"chunked"}
Hello World

options决定了http请求头的内容,选项如下:

参数 说明
host 服务器的域名或IP地址,默认localhost
hostname 服务器名称
port 服务器端口,默认80
localAddress 建立网络连接的本地网卡
sockerPath Domain套接字路径
method http请求方法,默认GET
path 请求路径,默认为/
headers 请求头对象
auth Basic认证,这个值将被计算成请求头中的Authorization

报文体的内容则由请求对象的wirte()和end()方法实现,通过write写入数据,通过end告知报文结束。

1.HTTP响应
http客户端的响应对象与服务器端较为类似,在ClientRequest对象中,它的事件也被称为response,ClientRequest在解析响应报文时,解析完响应头就会触发response事件,同时传递一个响应对象以供操作ClientResponse,后续响应报文以只读流的方式提供。

function(res) {
    console.log('STATUS: ' + res.statusCode);
    console.log('HEADERS: ' + JSON.stringify(res.headers));
    res.setEncoding('utf8');
    res.on('data', function (chunk) {
        console.log(chunk);
    });
}

2.HTTP代理
如同服务器端的实现一样,http提供的ClientRequest对象也是基于tcp实现的,在keepalive的情况下,一个底层会话连接可以多次用于请求,为了重用tcp连接,http模块包含一个默认的客户端代理对象http.globalAgent,它对每个服务器端的host+port创建的连接进行了管理,默认情况下,通过ClientRequest对象对同一个服务器端发起的HTTP请求最多可以创建5个连接,它的实质是一个连接池。

clipboard.png

自行构造代理对象:

var agent = new http.Agent({
    maxSockets: 10
});
var options = {
    hostname: '127.0.0.1',
    port: 1334,
    path: '/',
    method: 'GET',
    agent: agent
};

也可以设置Agent选项为false,以脱离连接池的管理,使得请求不受并发的限制。

Agent对象的sockets和requests属性分别表示当前连接池中使用的连接数和处于等待状态的请求数,在业务中监视这两个值有助于发现业务状态的繁忙程度。

3.HTTP客户端事件

  • response : 处理服务器端返回的response,返回后,触发该事件
  • socket : 当底层连接池中建立的连接分配给当前请求对象时,触发该事件
  • connect : 当客户端向服务器端发起CONNECT请求时,如果服务器端响应了200状态码,客户端会触发该事件
  • upgrade : 客户端向服务器端发起Upgrade请求时,如果服务器端响应了101 Switching Protocols状态,客户端将会触发该事件
  • continue : 客户端向服务器端发起Expect: 100-continue头信息,以试图发送较大数据量,如果服务器端响应100 Continue状态,客户端将触发该事件。

4.构建WebSocket服务

websocket与传统HTTP有如下好处:

  • 客户端与服务器只建立一个tcp连接,可以使用更少的连接
  • websocket服务器端可以推送数据到客户端,这远比HTTP请求响应模式更灵活,更高效。
  • 有更轻量级的协议头,减少数据传送量

    //websocket客户端程序
    var socket = new WebSocket('ws://127.0.0.1:12010/updates');
    socket.onopen = function () {
    setInterval(function() {
    if (socket.bufferedAmount == 0)
    socket.send(getUpdateData());
    }, 50);
    };
    socket.onmessage = function (event) {
    // TODO: event.data
    };

websocket是通过tcp重新拟定的新的协议,不是在http协议的基础上的封装。websocket分为握手和数据传输两部分,其中握手使用了http进行。

1)WebSocket握手
客户端建立连接是,通过HTTP发起请求报文。如下所示:

GET /chat HTTP/1.1
Host: server.example.com
//请求服务端升级协议为WebSocket
Upgrade: websocket
Connection: Upgrade
//用于安全校验
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
//指定子协议和版本号
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

Sec-WebSocket-Key的值是随机生成的base64编码的字符串。服务器端接收到之后,将其与字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11相连,形成字符串dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11,然后通过sha1安全散列算法计算出结果后,再进行base64编码,最后,返回给客户端,我们看一下这个算法:

var crypto = require('crypto');
var val = crypto.createHash('sha1').update(key).digest('base64');

服务器端在处理完请求后,响应如下报文:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

这段报文将告诉客户端,正在更换协议,更新为应用层协议websocket,并在当前的套接字上应用新的协议。
剩余的字段分别表示服务器端基于Sec-WebSocket-Key生成的字符串和选中的子协议。客户端将会校验Sec-WebSocket-Accept的值,如果成功,将开始接下来的数据传输。
我们使用node来模拟浏览器发起协议切换的行为:

var WebSocket = function (url) {
    // 伪代码,解析ws://127.0.0.1:12010/updates,用于请求
    this.options = parseUrl(url);
    this.connect();
};
WebSocket.prototype.onopen = function () {
    // TODO
};
WebSocket.prototype.setSocket = function (socket) {
    this.socket = socket;
};
WebSocket.prototype.connect = function () {
    var this = that;
    var key = new Buffer(this.options.protocolVersion + '-' + Date.now()).toString('base64');
    var shasum = crypto.createHash('sha1');
    var expected = shasum.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64');
    var options = {
        port: this.options.port, // 12010
        host: this.options.hostname, // 127.0.0.1
        headers: {
            'Connection': 'Upgrade',
            'Upgrade': 'websocket',
            'Sec-WebSocket-Version': this.options.protocolVersion,
            'Sec-WebSocket-Key': key
        }
    };
    var req = http.request(options);
    req.end();
    req.on('upgrade', function (res, socket, upgradeHead) {
        // 连接成功
        that.setSocket(socket);
        //触发open事件
        that.onopen();
    });
};

服务器端的响应代码

var server = http.createServer(function (req, res) {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('Hello World\n');
});
server.listen(12010);
// 在收到upgrade请求后,告知客户端允许切换协议
server.on('upgrade', function (req, socket, upgradeHead) {
    var head = new Buffer(upgradeHead.length);
    upgradeHead.copy(head);
    var key = req.headers['sec-websocket-key'];
    var shasum = crypto.createHash('sha1');
    key = shasum.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest('base64');
    var headers = [
        'HTTP/1.1 101 Switching Protocols',
        'Upgrade: websocket',
        'Connection: Upgrade',
        'Sec-WebSocket-Accept: ' + key,
        'Sec-WebSocket-Protocol: ' + protocol
    ];
    // 让数据立即发送
    socket.setNoDelay(true);
    socket.write(headers.concat('', '').join('\r\n'));
    // 建立服务器端WebSocket连接
    var websocket = new WebSocket();
    websocket.setSocket(socket);
});

一旦websocket握手成功,服务器端与客户端就将会呈现对等的效果,都能接收和发送消息。

2)WebSocket数据传输

在顺利握手后,当前连接将不再进行http交互,而是开始websocket的数据帧协议,实现客户端与服务器的数据交换。
协议升级的过程如下:

clipboard.png

当客户端调用send()发送数据时,服务器端触发onmessage(),当服务器端调用send()发送数据时,客户端的onmessage()触发,当我们调用send()发送一条数据时,协议可能将这个数据封装为一帧或多帧数据,然后逐帧发送。
为了安全考虑,客户端需要发送的数据帧进行掩码处理,服务器一旦收到无掩码帧,比如中间拦截破坏,连接将会关闭。服务器发送到客户端的数据帧无需做掩码,如果客户端收到了带掩码的数据帧,连接也将关闭。
在websocket中的数据帧的定义,每8位为一列,也就是一个字节,其中每一位都有它的意义:

clipboard.png

  • fin,如果这一帧是最后一帧,这个fin为为1,其余情况为0.
  • rsv1、rsv2、rsv3,都是一位长,用于标识扩展,当有已协商的扩展时,这些值可能为1,其余情况为0。
  • opcode,4位长,可以用来表示0~15的值,用于解释当前数据帧,0表示附加数据帧,1表示文本数据帧,2表示二进制数据帧,8表示发送一个连接关闭的数据帧,9表示ping数据帧,10表示pong数据帧,其余值暂时没有定义。瓶数据帧和pong数据帧用于心跳检测,当一端发送一个ping数据帧时,另一端必须发送pong数据帧作为回应,告知对方这一端仍然处于响应状态。
  • masked,表示是否进行掩码处理,1位长度,客户端发送给服务器时为1,服务器发送回客户端时为0.
  • payload length:一个7、7+16或7+64位长的数据为,标识数据的长度,如果值在0~125之间那么该值就是数据的真实长度,如果是126,则后面16位的值是数据的真实长度,如果是127,则后面64位的值是数据的真实长度。
  • making key,当masked为1时,这里是一个32位长的数据位,用于解密数据。
  • payload data,我们的目标数据,位数为8的倍数。

客户端发送消息时,需要构造一个或多个数据帧协议报文,例如我们发送一个hello world,这个比较短,不存在分割多个数据帧的情况,并且以文本方式发送,他的payload length长度为96(12字节*8位/字节),二进制表示为110000。所以报文应该是:

fin(1) + res(000) + opcode(0001) + masked(1) + payload length(1100000) + masking key(32位) + payload
data(hello world!加密后的ܾ二进制)

服务器回复的是yakexi,这个无需掩码,形式如下:

fin(1) + res(000) + opcode(0001) + masked(0) + payload length(1100000) + payload data(yakexi的ܾ二进制)


5.网络服务与安全

SSL(secure socket layer)作为一种安全协议,它在传输层提供对网络连接的加密的功能,对于应用层它是透明的,数据在传递到应用层之前就已经完成了加密和解密的过程。
最开始使用这个协议的是网景的浏览器,然后,为了被更多的服务器核浏览器支持,IETF组织将其标准化,也就是TLS = transport layer security。
node在网络安全方面提供了crypto、tls、https三个模块,crypto用于加密解密,例如sha1、md5等加密算法,tls用于建立一个基于TLS/SSL的tcp链接,它可以看成是net模块的加密升级版本。https用于提供一个加密版本的http,也是http的加密升级版本,甚至提供的接口和事件也跟http模块一样。

1)TLS/SSL

1.密钥
TLS/SSL是一个公钥/私钥的结构,这也是一个非对称的结构,每个服务器和客户端都有自己的公钥和私钥。公钥用来加密要传输的数据,私钥用来解密接收到的数据。公钥和私钥是配对的,通过公钥加密的数据,只有通过私钥才能解密,所以在建立安全传输之前,客户端和服务器端之间需要互换公钥。客户端发送数据时要通过服务器端的公钥进行加密,服务器端发送数据时则需要客户端的公钥进行加密,如此才能完成加密解密的过程:

clipboard.png

node在底层采用openssl来实现TLS/SSL,为此要生成公钥和私钥需要通过openssl来完成,我们分别为服务器端和客户端生成私钥:

// 生成服务器端私钥
$ openssl genrsa -out server.key 1024
// 生成客户端私钥
$ openssl genrsa -out client.key 1024

上述命令生成了两个1024位长的RSA私钥文件,我们继续通过它生成公钥:

$ openssl rsa -in server.key -pubout -out server.pem
$ openssl rsa -in client.key -pubout -out client.pem

公钥和私钥的非对称性加密虽然很好,但是网络中依然可能存在窃听的情况,典型的例子就是中间人攻击。客户端和服务器端在交换公钥的过程中,中间人对客户端扮演服务器端的角色,对服务器端扮演客户端的角色,因此客户端和服务器端几乎感受不到中间人的存在,为了解决这个问题,数据传输过程中还需要对得到的公钥进行认证,以确认得到的公钥是出自目标服务器的,如果不能保证这种认证,中间人可能会将伪造的站点响应给用户,从而造成经济损失。

clipboard.png

为了解决中间人攻击的问题,TLS/SSL引入了数字证书来进行认证,与直接公钥不同,数字证书中包含了服务器的名称和主机名称、服务器的公钥、签名颁发机构的名称、来自签名颁发机构的签名。在建立连接前,会通过证书中的签名确认收到的公钥是来自目标服务器的,从而产生信任关系。

2.数字证书
CA (Certificate Authority,数字证书认证中心)是数字证书的颁发机构,这个证书具有ca通过自己的公钥和私钥实现的签名。
为了得到ca的签名证书,服务器端需要通过自己的私钥生成CSR = certificate signing request文件,ca机构将通过这个文件颁发属于该服务器的签名证书,只要通过ca机构就能验证证书是否合法。
通过ca机构颁发证书通常是一个繁琐的过程,需要付出一定的精力和费用,对于中小企业来说,可以采用自签名证书来构建安全的网络,也就是自己给自己的服务器扮演ca机构,给自己的服务器颁发自己的ca生成的签名证书。我们还是使用openssl来实现这一过程

//生成服务器私钥
$ openssl genrsa -out ca.key 1024
//生成csr文件
$ openssl req -new -key ca.key -out ca.csr
//通过私钥自签名生成证书,此时还没有业务服务器的签名
$ openssl x509 -req -in ca.csr -signkey ca.key -out ca.crt

clipboard.png

这样就生成了自己的签名证书,然后再次回到服务器端,服务器需要向ca申请签名,在申请签名之前,依然需要创建自己的csr,值得注意的是,这个过程中的common name需要匹配服务器域名,否则在后续的认证过程中会出错:

//生成自己的业务服务器csr
$ openssl req -new -key server.key -out server.csr
//向自己的ca申请签名证书,这个过程需要ca的证书和私钥参与,最终生成带签名的证书
$ openssl x509 -req -CA ca.crt -CAkey ca.key -CAcreateserial -in server.csr -out server.crt

之后,客户端发起安全连接前会去捕获服务器端的证书,并通过ca的证书验证服务器端证书的真伪。除了验证真伪外,通常还含有对服务器名称、IP地址等进行检验的过程:

clipboard.png

ca机构将证书颁发给服务器端后,证书在请求的过程中会被发送给客户端,客户端需要通过ca的证书验证真伪。如果是知名的ca机构,他们的证书一般都会预装在浏览器中,如果是自己扮演的ca,就需要让客户自己先去获取这个ca然后才能进行验证。
另外,ca的证书一般被称为根证书,也就是不需要上级证书参与签名的证书。

2)TLS服务
先基于tls模块创建服务器端程序

var tls = require('tls');
var fs = require('fs');
var options = {
    key: fs.readFileSync('./keys/server.key'),
    cert: fs.readFileSync('./keys/server.crt'),
    requestCert: true,
    ca: [fs.readFileSync('./keys/ca.crt')]
};
var server = tls.createServer(options, function (stream) {
    console.log('server connected', stream.authorized ? 'authorized' : 'unauthorized');
    stream.write("welcome!\n");
    stream.setEncoding('utf8');
    stream.pipe(stream);
});
server.listen(8000, function () {
    console.log('server bound');
})

创建客户端程序

var tls = require('tls');
var fs = require('fs');
var options = {
    key: fs.readFileSync('./keys/client.key'),
    cert: fs.readFileSync('./keys/client.crt'),
    ca: [fs.readFileSync('./keys/ca.crt')]
};
var stream = tls.connect(8000, options, function () {
    console.log('client connected', stream.authorized ? 'authorized' : 'unauthorized');
    process.stdin.pipe(stream);
});
stream.setEncoding('utf8');
stream.on('data', function (data) {
    console.log(data);
});
stream.on('end', function () {
    server.close();
});

客户端启动之后,就可以在输入流中输入数据了,服务器端将会回应相同的数据。至此我们完成了TLS的服务器端和客户端的创建,与普通的tcp服务器和客户端相比,TLS的服务器核客户端仅仅只是需要配置证书,其他基本一样。
3)HTTPS服务

HTTPS其实就是TLS/SSL基础上的HTTP。换句话说,net模块对应http模块,tls模块对应https模块,我们来创建一个https服务:
1.准备证书

HTTPS服务需要用到私钥和签名证书。

2.创建https服务

var https = require('https');
var fs = require('fs');
var options = {
    key: fs.readFileSync('./keys/server.key'),
    cert: fs.readFileSync('./keys/server.crt')
};
https.createServer(options, function (req, res) {
    res.writeHead(200);
    res.end("hello world\n");
}).listen(8000);

3.https客户端

var https = require('https');
var fs = require('fs');
var options = {
    hostname: 'localhost',
    port: 8000,
    path: '/',
    method: 'GET',
    key: fs.readFileSync('./keys/client.key'),
    cert: fs.readFileSync('./keys/client.crt'),
    ca: [fs.readFileSync('./keys/ca.crt')]
};
options.agent = new https.Agent(options);
var req = https.request(options, function (res) {
    res.setEncoding('utf-8');
    res.on('data', function (d) {
        console.log(d);
    });
});
req.end();
req.on('error', function (e) {
    console.log(e);
});

//输出结果
$ node client.js
hello world

//如果不设置ca的话,会报错
[Error: UNABLE_TO_VERIFY_LEAF_SIGNATURE]

这个异常可以通过添加属性
rejectUnauthorized:false解决,这个与curl -k效果一致









suyue
77 声望7 粉丝