5.内存控制
6.理解Buffer
7.网络编程
Node基于垃圾回收机制进行内存的自动管理。这种机制,在浏览器环境下,几乎是完美的,可是同java同样,在后端运行的node,若是想要更完美的运行,依然须要判断和管理内存,内存管理的好坏、垃圾回收情况是否优良,都会直接影响服务器的性能。java
1)node 与 V8node
Node在JavaScript的执行上直接受益于V8,能够随着V8的升级就能享受到更好的性能或新的语言特性(如ES5和ES6)等,
同时也受到V8的限制,尤为是内存显示。c++
2)V8的内存限制git
通常的后台开发语言中,内存使用的大小几乎没有限制。可是,V8最初是为浏览器打造的,在V8下,64位系统能够操纵1.4GB内存,32位系统能够操纵0.7GB内存。在这样的限制下,node几乎不能直接操纵大内存。github
3)V8的对象分配web
在V8中全部的js对象都是经过堆来进行分配的,可使用node提供的V8内存使用量的查看方式查看内存分配及使用情况:redis
// 1.rss:resident set size,进程的常驻内存 // 2.heapTotal: 已经申请到的堆内存。 // 3.heapUsed: 当前堆内存使用量。 $ node > process.memoryUsage(); { rss: 14958592, heapTotal: 7195904, heapUsed: 2821496 }
调整内存限制大小:算法
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中,主要将内存分为新生代和老生代两代。新内存中的对象存活时间短,老内存中的对象存活时间长或常驻内存对象。编程
2.新生代垃圾回收算法scavenge算法
新生代中的对象主要经过scavenge算法进行垃圾回收,其主要是采用cheney算法进行具体处理。
cheney算法采用一种复制方式的垃圾回收算法,将堆内存一分为二,只有一部分空间被使用称为From空间,另外一个处于闲置称为To空间。当进行分配对象的时候先在from空间分配,当进行垃圾回收时,会检查from空间中的存活对象,将这些存活对象复制到to空间中,复制完成后From和to空间角色互换,清空to空间,在垃圾回收过程当中就是经过将存活对象在两个空间中进行复制。
当一个对象通过屡次复制依然存活时,就会被认为是生命周期较长的对象,会被移入老生代内存中。
对于移入老生代内存有两个条件:
老内存垃圾回收算法Mark-Sweep & Mark-Compact
采用标记清除,它分为标记清除两个阶段
在标记阶段遍历全部的对象并标记活着的对象,在清除阶段只清除死亡的对象,死亡对象在老生代内存只占一小部分。老生代内存进行一次清除后,内存空间会出现不连续的状态,因此清理完成须要进行一步标记整理。、
Incremental Marking
为了不出现javaScript应用逻辑与垃圾回收器看到不一致的状况,垃圾回收都要将应用逻辑停下来,这种行为会形成停顿,在新生代垃圾回收过程当中由于存活对象比较少,即便停顿基本影响不大。在老生代垃圾回收中,一般存活对象较多,全堆垃圾回收的标记、清除、整理影响较大。
解决办法:分批次进行,拆分红许多小步,每进行一小步就让逻辑运行一会
5)查看垃圾回收日志
在启动时时加入 参数 --trace_gc。
还能够在启动时增长--prof参数,来获得v8执行时的性能分析数据,其中包含了垃圾回收执行时占用的时间等。
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)*小结
能够利用闭包和垃圾回收的机制,来存储一些须要存活时间长一些的对象,并将其做为公共访问的数据区域来使用。可是,闭包和全局变量的使用仍是要当心,因为没法及时回收内存,这会增长常驻内存的产生,会致使老生代中的对象增多。
1)查看内存使用状况
2)堆外内存
堆中的内存用量老是小于进程的常驻内存用量,这意味着Node中的内存使用并不是都是经过V8进行分配的,这些不经过V8分配的内存,称为堆外内存。
例如:buffer对象不通过v8内存分配,所以,也不会有堆内存的大小限制。
3)小结
Node的内存主要由经过V8进行分配的部分和Node 自行分配的部分,受V8的垃圾回收限制的主要是V8的堆内存。
内存泄漏的实质就是应当回收的对象由于意外没有被回收,变成了常驻在老生代中的对象。
形成内存泄漏的主要缘由有:缓存、队列消费不及时、做用域未释放。
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的超时模式和拒绝模式。
定位Node应用的内存泄漏经常使用工具以下:
工具 | 说明 |
---|---|
v8-profiler | 能够对v8堆内存抓取快照,并对cpu进行分析 |
node-heapdump | 能够对v8堆内存抓取快照,用于过后分析 |
node-mtrace | 使用gcc的mtrace工具来分析堆的使用 |
dtrace | 在smartos上使用的内存分析工具 |
node-memwatch | 采用wtfpl许可发布的内存分析工具 |
使用流的方式操做大内存,也就是使用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内存的限制。可是,物理内存依然有限制。
由于在node中须要处理网络协议、操做数据库、处理图片、接受上传文件等,在网络流和文件操做中,还要处理大量二进制数据,js自有的字符串远远不能知足这些需求,因而Buffer对象应运而生。
Buffer是一个像Array的对象,可是它主要用于操做字节。
1)模块结构
buffer是一个典型的js与c++结合的模块,将性能相关的部分用c++实现,将非性能相关的部分用js实现。同时buffer也是node的核心模块,能够直接使用,而且,buffer属于堆外内存,能够经过本身管理其垃圾回收。固然,buffer对象的管理仍是在堆内,再由这个对象去管理堆外的内存。
因为Buffer太常见,Node在进程启动时已经加载了它,并将其放在全局对象上,因此在使用Buffer时,无须经过require()便可直接使用。
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种状态:
分配指定大小的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++层面提供的内存,无需频繁的分配操做。
Buffer对象能够和字符串进行相互转换,支持的编码类型有:ASCII、UTF-八、UTF-16LE/UCS-二、Base6四、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来解决。
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-八、base6四、ucs-二、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); });
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的大小对性能的影响:
Node提供了net、dgrm、http、https这四个模块,分别用于处理TCP、UDP、HTTP、HTTPS,适用于服务器端和客户端。
1)TCP
TCP全称为传输控制协议,在OSI模型上属于传输层协议。
七层协议示意图以下:
TCP是面向链接的协议,其显著特征是在传输以前须要3次握手造成会话。
只有会话造成后,服务器端和客户端才能相互发送数据。在建立会话的过程当中,服务器端和客户端分别提供一个套接字,
这两个套接字共同造成一个链接。服务器端和客户端则经过套接字实现二者之间链接的操做。
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实例,有以下事件:
2.链接事件
服务器能够同时与多个客户端保持链接,对于每一个链接而言是可写可读Stream对象。
Stream对象能够用于服务器端和客户端之间的通讯,能够经过data事件从一端读取另外一端发来的数据,也能够经过write()从一端向另外一端发送数据。
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事件仍是要进行小包合并后触发的,这个须要注意。
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实例,事件以下:
咱们将会使用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的过程进行了封装。
http模块将链接所用的套接字的读写抽象为ServerRequest和ServerResponse对象,在请求产生的过程当中,http模块拿到链接中传来的数据,调用二进制模块http_parser进行解析,在解析完请求报文的报文头后,触发request事件,以后调用用户的业务逻辑。
处理程序对应的代码就是响应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实例。
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个链接,它的实质是一个链接池。
自行构造代理对象:
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客户端事件
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的数据帧协议,实现客户端与服务器的数据交换。
协议升级的过程以下:
当客户端调用send()发送数据时,服务器端触发onmessage(),当服务器端调用send()发送数据时,客户端的onmessage()触发,当咱们调用send()发送一条数据时,协议可能将这个数据封装为一帧或多帧数据,而后逐帧发送。
为了安全考虑,客户端须要发送的数据帧进行掩码处理,服务器一旦收到无掩码帧,好比中间拦截破坏,链接将会关闭。服务器发送到客户端的数据帧无需作掩码,若是客户端收到了带掩码的数据帧,链接也将关闭。
在websocket中的数据帧的定义,每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的ܾ二进制)
SSL(secure socket layer)做为一种安全协议,它在传输层提供对网络链接的加密的功能,对于应用层它是透明的,数据在传递到应用层以前就已经完成了加密和解密的过程。
最开始使用这个协议的是网景的浏览器,而后,为了被更多的服务器核浏览器支持,IETF组织将其标准化,也就是TLS = transport layer security。
node在网络安全方面提供了crypto、tls、https三个模块,crypto用于加密解密,例如sha一、md5等加密算法,tls用于创建一个基于TLS/SSL的tcp连接,它能够当作是net模块的加密升级版本。https用于提供一个加密版本的http,也是http的加密升级版本,甚至提供的接口和事件也跟http模块同样。
1)TLS/SSL
1.密钥
TLS/SSL是一个公钥/私钥的结构,这也是一个非对称的结构,每一个服务器和客户端都有本身的公钥和私钥。公钥用来加密要传输的数据,私钥用来解密接收到的数据。公钥和私钥是配对的,经过公钥加密的数据,只有经过私钥才能解密,因此在创建安全传输以前,客户端和服务器端之间须要互换公钥。客户端发送数据时要经过服务器端的公钥进行加密,服务器端发送数据时则须要客户端的公钥进行加密,如此才能完成加密解密的过程:
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
公钥和私钥的非对称性加密虽然很好,可是网络中依然可能存在窃听的状况,典型的例子就是中间人攻击。客户端和服务器端在交换公钥的过程当中,中间人对客户端扮演服务器端的角色,对服务器端扮演客户端的角色,所以客户端和服务器端几乎感觉不到中间人的存在,为了解决这个问题,数据传输过程当中还须要对获得的公钥进行认证,以确认获得的公钥是出自目标服务器的,若是不能保证这种认证,中间人可能会将伪造的站点响应给用户,从而形成经济损失。
为了解决中间人攻击的问题,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
这样就生成了本身的签名证书,而后再次回到服务器端,服务器须要向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地址等进行检验的过程:
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效果一致