高性能Node.JS : 来自LinkedIn Mobile的10条优化建议

 Nodejs  高性能Node.JS : 来自LinkedIn Mobile的10条优化建议已关闭评论
6月 042013
 

一、避免使用同步代码(Avoid synchronous code)

 

Node.js是单线程的。为了使单线程可以处理多个并发请求,我们不应该让线程来等待一个阻塞的、同步的或是长时间运行的操作。Node.js的一个突出的特性就是它被完全地设计并实现为异步的。这使得其可以非常好地适用于事件驱动的程序。

遗憾的是,我们仍然有可能使用到同步/阻塞的调用。例如,很多对文件系统的操作都提供了同步和异步的两个版本,就像writeFilewriteFileSync。有时即使我们没有在自己的代码中使用同步的方法,但仍然有可能在无意中使用了包含阻塞调用的某个外部库。当您使用到同步/阻塞调用时,对性能的影响是巨大的。

  1. // Good: write files asynchronously
  2. fs.writeFile(message.txtHello Nodefunction (err) {
  3.   console.log("It’s saved and the server remains responsive!");
  4. });
  5.  
  6. // BAD: write files synchronously
  7. fs.writeFileSync(message.txtHello Node);
  8. console.log("It’s saved, but you just blocked ALL requests!");

我们最初的日志系统的实现中,不慎使用了一个写磁盘的同步调用。直到我们做性能测试之前这个情况一直没有被我们注意到。当我们在开发环境中对一个单实例的Node.js程序作基准测试的时候,这个同步调用使得吞吐率(requests per second)从几千一下子下降到了几十!

 

二、关闭socket池(Turn off socket pooling)

 

Node.js的http client自动使用socket池:在默认情况下,主机能同时并发打开的socket数量被限制为5个。虽然socket的重用也许能控制资源消耗的增长,但当我们需要处理多个从同一主机获取数据的并发请求时,这将是一个严重的瓶颈。在这些情况下,好的解决方案是增加maxSockets或者完全禁用socket池:

  1. // Disable socket pooling
  2.  
  3. var http = require(http);
  4. var options = {…..};
  5. options.agent = false;
  6. var req = http.request(options)

 

三、不要用Node.js处理静态文件(Don’t use Node.js for static assets)

 

对于静态文件,例如CSS和图片,建议使用标准的webserver来处理而非Node.js。例如LinkedIn mobiles使用的是nginx。并且我们使用CDN,将静态文件复制到遍布于全世界的各个节点。以上这些将带来两个好处:1、可以减轻Node.js服务器的负载;2、借助于CDN使得静态文件可以从离用户更近的节点传输予之,从而降低时延。

 

四、将渲染放到客户端做(Render on the client-side)

 

让我们来简单地对在服务器端和在客户端渲染一个页面进行一个比较。如果我们使用Node.js在服务器端进行渲染,在每次请求中我们将返回以下的HTML页面:

  1. <!– An example of a simple webpage rendered entirely server side –>
  2.  
  3. <!DOCTYPE html>
  4. <html>
  5.   <head>
  6.     <title>LinkedIn Mobile</title>
  7.   </head>
  8.   <body>
  9.     <div class="header">
  10.       <img src="http://mobile-cdn.linkedin.com/images/linkedin.png"alt="LinkedIn"/>
  11.     </div>
  12.     <div class="body">
  13.       Hello John!
  14.     </div>
  15.   </body>
  16. </html>

请注意到这个页面上除了用户的名称之外的所有东西都是静态的:也就是说,这些对于每个用户以及每次页面重载都是相同的。因此,一个更高效的做法是让Node.js以JSON格式只返回页面所需要的动态数据:

  1. {"name""John"}

页面的其余部分­——所有的静态HTML标记­——可以放入一个JavaScript模板(譬如一个underscore.js模板):

  1. <!– An example of a JavaScript template that can be rendered client side –>
  2.  
  3. <!DOCTYPE html>
  4. <html>
  5.   <head>
  6.     <title>LinkedIn Mobile</title>
  7.   </head>
  8.   <body>
  9.     <div class="header">
  10.       <img src="http://mobile-cdn.linkedin.com/images/linkedin.png"alt="LinkedIn"/>
  11.     </div>
  12.     <div class="body">
  13.       Hello <%= name %>!
  14.     </div>
  15.   </body>
  16. </html>

这样做的性能提升来自前述的第三条建议,静态的JavaScript模板可以用我们的webserver(譬如nginx)来处理,甚至是使用CDN就更好了。此外,JavaScript模板可以被浏览器缓存或者保存在LocalStorage中。这样一来在页面第一次被加载后,需要发送给客户端的数据就只有动态的JSON了,极大地提升了效率。这个方法大大降低了Node.js的CPU、I/O消耗和负载。

 

五、使用gzip(Use gzip)

 

大多数的服务器和客户端程序都支持用gzip来压缩请求和响应。请确保无论是在发送响应到客户端还是向远程服务器发送请求时,您都充分地利用到了它。

 

六、并行化(Go parallel)

 

尝试着将类似请求远程服务器、调用数据库和访问文件系统等将会引致阻塞的操作并行地执行。这样做的好处是,可以将完成一系列阻塞操作的总时延减小到其中最慢的某项操作所需要的时间,否则其总时延将会是完成操作序列中所有操作需要等待的时间总和。为了保持回调和错误处理函数的结构明晰,我们使用了Step来做flow control。

 

七、去session化(Go session-free)

 

LinkedIn mobile使用Express框架来处理请求/响应周期(request/response cycle)。大多数express的示例代码中包括了以下的配置:

  1. app.use(express.session({ secret"keyboard cat" }));

在默认情况下session数据存储在内存中,在用户数量增加的时候这会显著地增加服务器的开销。我们可以转而使用一个外部的session存储方案,例如MongoDB或Redis,但此时每个请求将增加远程调用获取session数据带来的额外开销。有可能的话,最好的选择是完全不在服务器端存储状态信息。不进行上述的express配置,不保存session,这样可以获得更好的性能。

 

八、使用二进制的模块(Use binary modules)

 

请尽可能地使用编译后的二进制模块而非用JavaScript编写的模块。举个例子,当我们从一个用JavaScript编写的SHA模块切换到Node.js内置的编译版本后,我们看到了巨大的性能提升:

  1. // Use built in or binary modules
  2. var crypto = require(crypto);
  3. var hash =crypto.createHmac("sha1",key).update(signatureBase).digest("base64");

 

九、使用标准的V8 JavaScript而非客户端的库(Use standard V8 JavaScript instead of client-side libraries)

 

JavaScript的版本不统一,而大部分JavaScript库是提供给web浏览器使用的:例如一款浏览器或许支持类似forEach、map和reduce这样的函数,但其他浏览器并不支持。其结果是,客户端的库常常要用很多低效的代码来掩盖浏览器间的差异。另一方面,在使用Node.js时你确切地知道哪些JavaScript函数是可用的:驱动Node.js的V8 JavaScript引擎实现的是ECMAScript的ECMA-262,第五版。通过直接使用标准的V8函数,而不是客户端的库,您将会再一次体验到显著的性能提升。

 

十、保持代码精简、轻量(Keep your code small and light)

 

面向移动设备进行开发时,设备的速度更慢而且时延更高,这迫使我们保持代码精简、轻量。您同样也应该把这种思想带到服务器端代码的开发中。时不时地进行自省,并且问自己:“我们是否真的需要这个模块?”,“为什么我们要使用这个框架?它带来的开销是值得的吗?”,“我们能否用一种更简单的方法来实现?”。更精简,更轻量的代码通常也是更高效,更快的。

原文:http://engineering.linkedin.com/nodejs/blazing-fast-nodejs-10-performance-tips-linkedin-mobile

译者及来源:http://www.rockdai.com/?p=596

 Posted by at 上午11:30  Tagged with:

Node.js 调试 GC 以及内存暴涨的分析

 gc, Nodejs  Node.js 调试 GC 以及内存暴涨的分析已关闭评论
6月 042013
 

转自:http://blog.eood.cn/node-js_gc

最近做的服务器端组件大部分都在使用 Node.js 。因为 Node.js 库管理模式比较先进,并且依托于 Github 的流行,Node.js 开源的库非常多,一般所需要的第三方库都可以找到。虽然这些库有很多明显的 Bug 但是比从零自己开发要快很多。对于服务器端开发,Node.js 还是个不错的选择,不像 Erlang 更接近底层,业务层面的库相对要少很多。



最近写的一个功能在本地开发的时候没有明显问题,但是到真实环境测试的时候发现内存不断增长,并且增长很快,同时 CPU 占用也很高,接近单核心的 100% 。这对于一个大部分都是 IO 操作的进程显然是有问题的。所以尝试分析内存和 CPU 异常的原因。最终发现是因为生产者和消费者速度差异引起的缓冲区暴增。在 MySQL 连接对象的 Queue 中积压了大量的 Query,而不是内存泄漏。

查看 Node.js 进程的 GC log:

node --trace_gc --trace_gc_verbose test.js
61 ms: Scavenge 2.2 (36.0) -> 1.9 (37.0) MB, 1 ms [Runtime::PerformGC].
Memory allocator,   used: 38780928, available: 1496334336
New space,          used:   257976, available:   790600
Old pointers,       used:  1556224, available:        0, waste:        0
Old data space,     used:  1223776, available:     4768, waste:        0
Code space,         used:  1019904, available:        0, waste:        0
Map space,          used:   131072, available:        0, waste:        0
Cell space,         used:    98304, available:        0, waste:        0
Large object space, used:        0, available: 1495269120
      97 ms: Mark-sweep 11.7 (46.1) -> 5.4 (40.7) MB, 10 ms [Runtime::PerformGC] [GC in old space requested].
Memory allocator,   used: 42717184, available: 1492398080
New space,          used:        0, available:  1048576
Old pointers,       used:  1390648, available:   165576, waste:        0
Old data space,     used:  1225920, available:     2624, waste:        0
Code space,         used:   518432, available:   501472, waste:        0
Map space,          used:    60144, available:    70928, waste:        0
Cell space,         used:    23840, available:    74464, waste:        0
Large object space, used:  3935872, available: 1491332864

关于 Node.js 的 GC

Node.js 的 GC 方式为分代 GC (Generational GC)。对象的生命周期由它的大小决定。对象首先进入占用空间很少的 new space (8MB)。大部分对象会很快失效,会频繁而且快速执行 Young GC (scavenging)*直接*回收这些少量内存。假如有些对象在一段时间内不能被回收,则进入 old space (64-128KB chunks of 8KB pages)。这个区域则执行不频繁的 Old GC/Full GC (mark-sweep, compact or not),并且耗时比较长。(Node.js 的 GC 有两类:Young GC: 频繁的小量的回收;Old GC: 长时间存在的数据)

Node.js 最新增量 GC 方式虽然不能降低总的 GC 时间,但是避免了过大的停顿,一般大停顿也限制在了几十 ms 。

为了减少 Full GC 的停顿,可以限制 new space 的大小

--max-new-space-size=1024 (单位为 KB)

 

手动在代码中操作 GC (不推荐)

node --expose-gc test.js

修改 Node.js 默认 heap 大小

node --max-old-space-size=2048 test.js (单位为 MB)

Dump 出 heap 的内容到 Chrome 分析:

安装库

https://github.com/bnoordhuis/node-heapdump

在应用的开始位置添加

var heapdump = require('heapdump');

在进程运行一小段时间后执行:

kill -USR2 <pid>

这时候就会在当前目录下生成 heapdump-xxxxxxx.heapsnapshoot 文件。
将这个文件 Down 下来,打开 Chrome 开发者工具中的 Profiles,将这个文件加载进去,就可以看到当前 Node.js heap 中的内容了。

可以看到有很多 MySQL 的 Query 堆积在处理队列中。内存暴涨的原因应该是 MySQL 的处理速度过慢,而 Query 产生速度过快。
所以解决方式很简单,降低 Query 的产生速度。内存暴涨还会引起 GC 持续执行,占用了大量 CPU 资源。

node-mysql 库中的相关代码,其实应该限制 _queue 的 size,size 过大则抛出异常或者阻塞,就不会将错误扩大。

Protocol.prototype._enqueue = function(sequence) {
  if (!this._validateEnqueue(sequence)) {
    return sequence;
  }

  this._queue.push(sequence);

  var self = this;
  sequence
    .on('error', function(err) {
      self._delegateError(err, sequence);
    })
    .on('packet', function(packet) {
      self._emitPacket(packet);
    })
    .on('end', function() {
      self._dequeue();
    });

  if (this._queue.length === 1) {
    this._parser.resetPacketNumber();
    sequence.start();
  }

  return sequence;
};

在不修改 node-mysql 的情况下,加入生产者和消费者的同步,调整之后,内存不再增长,一直保持在不到 100M 左右,CPU 也降低到 10% 左右。

Node.js 调试工具 node-inspector

安装:

npm install -g node-inspector

启动自己的程序:

node --debug test.js

node --debug-brk test.js (在代码第一行加断点)

启动调试器界面:

node-inspector

打开 http://localhost:8080/debug?port=5858 可以看到执行到第一行的断点。
右边为局部变量和全局变量、调用栈和常见的断点调试按钮,查看程序步进执行情况。并且你可以修改正在执行的代码,比如在关键的位置增加 console.log 打印信息。

Node.js 命令行调试工具

以 DEBUG 模式启动 Node.js 程序,类似于 GDB:

node debug test.js

debug> help
Commands: run (r), cont (c), next (n), step (s), out (o), backtrace (bt), setBreakpoint (sb), clearBreakpoint (cb),
watch, unwatch, watchers, repl, restart, kill, list, scripts, breakOnException, breakpoints, version

Node.js 其他常用命令参数

node --max-stack-size 设置栈大小

node --v8-options 打印 V8 相关命令

node --trace-opt test.js

node --trace-bailout test.js 查找不能被优化的函数,重写

node --trace-deopt test.js 查找不能优化的函数

Node.js 的 Profiling

V8 自带的 prof 功能:

npm install profiler

node --prof test.js

会在当前文件夹下生成 v8.log

安装 v8.log 转换工具

sudo npm install tick -g

在当前目录下执行

node-tick-processor v8.log

可以关注其中 Javascript 各个函数的消耗和 GC 部分

[JavaScript]:
ticks  total  nonlib   name
67   18.7%   20.1%  LazyCompile: *makeF /opt/data/app/test/test.js:6
62   17.3%   18.6%  Function: ~ /opt/data/app/test/test.js:9
42   11.7%   12.6%  Stub: FastNewClosureStub
38   10.6%   11.4%  LazyCompile: * /opt/data/app/test/test.js:1

[GC]:
ticks  total  nonlib   name
27    7.5%

参考以及一些有用的链接

https://bugzilla.mozilla.org/show_bug.cgi?id=634503
http://cs.au.dk/~jmi/VM/GC.pdf
http://lifecs.likai.org/2010/02/how-generational-garbage-collector.html
http://en.wikipedia.org/wiki/Garbage_collection_(computer_science)#Na.C3.AFve_mark-and-sweep
<a data-cke-saved-href="http://en.wikipedia.org/wiki/Cheney" href="http://en.wikipedia.org/wiki/Cheney" s_algorithm'="" style="color: rgb(7, 54, 66); transition: all 125ms ease-out; -webkit-transition: all 125ms ease-out; text-decoration: none;">http://en.wikipedia.org/wiki/Cheney’s_algorithm
https://github.com/bnoordhuis/node-heapdump
http://mrale.ph/blog/2011/12/18/v8-optimization-checklist.html
http://es5.github.com/
http://blog.caustik.com/2012/04/08/scaling-node-js-to-100k-concurrent-connections/
https://hacks.mozilla.org/2013/01/building-a-node-js-server-that-wont-melt-a-node-js-holiday-season-part-5/
https://gist.github.com/2000999
http://www.jiangmiao.org/blog/2247.html
http://blog.caustik.com/2012/04/08/scaling-node-js-to-100k-concurrent-connections/
http://blog.caustik.com/2012/04/11/escape-the-1-4gb-v8-heap-limit-in-node-js/
https://developers.google.com/v8/embed#handles
https://hacks.mozilla.org/2012/11/fully-loaded-node-a-node-js-holiday-season-part-2/
https://code.google.com/p/v8/wiki/V8Profiler

websocket与node.js的完美结合

 Nodejs  websocket与node.js的完美结合已关闭评论
5月 162013
 

转自:http://cnodejs.org/topic/4f16442ccae1f4aa27001139

 
之所以写下此文,是我觉得越是简单的技术往往能发挥越重要的作用,随着各种新的技术的诞生,实时web技术已经走进我们。websocket和node.js使开发实时应用非常简单,同时性能也非常高。 

关于websocket 

websocket是html5的重要feature,它直接在浏览器上对与socket的支持,这给了web开发无限的想象,虽然以前也有flash socket+js的实现,不过毕竟不稳定,而且兼容性有很多问题,当然websocket的普及也依赖于支持html5标准的浏览器的更新,目前只有chrome、safari、firefox 4.0等少数浏览器可以支持,不过大势所驱,加上智能移动设备的普及,websocket可以有更大的作为。 

他解决了web实时化的问题,相比传统http有如下好处: 

  • 一个WEB客户端只建立一个TCP连接
  • Websocket服务端可以推送(push)数据到web客户端.
  • 有更加轻量级的头,减少数据传送量
  •  

  •  

本文来重点来分析下。 

websocket的原理和应用

在继续本文之前,让我们了解下websocket的原理: 

websocket通信协议实现的是基于浏览器的原生socket,这样原先只有在c/s模式下的大量开发模式都可以搬到web上来了,基本就是通过浏览器的支持在web上实现了与服务器端的socket通信。 

WebSocket没有试图在HTTP之上模拟server推送,而是直接在TCP之上定义了帧协议,因此WebSocket能够支持双向的通信。 

首先来介绍下websocket客户端与服务端建立连接的过程: 

先用js创建一个WebSocket实例,使用ws协议建立服务器连接,ws://www.cnodejs.org:8088 

ws开头是普通的websocket连接,wss是安全的websocket连接,类似于https。 

客户端与服务端建立握手,发送如下信息: 

GET /echo HTTP/1.1

Upgrade: WebSocket

Connection: Upgrade

Host: www.cnodejs.org:8088

Origin: http://www.cnodejs.com

服务端会发回如下: 

HTTP/1.1 101 Web Socket Protocol Handshake

Upgrade: WebSocket

Connection: Upgrade

WebSocket-Origin: http://www.cnodejs.org

WebSocket-Location: ws://www.cnodejs.org:8088/echo

具体的ws协议,可以参考: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76 

我们在开发过程中不需要考虑协议的细节,因为websocket API已经帮我们封装好了。 

需要注意的是所有的通信数据都是以”x00″开头以”xFF”结尾的,并且都是UTF-8编码的。 

这个过程类似于http的建立连接过程,不同的是,建立连接之后,接下来客户端和服务端的任何交互都需要再有这个动作。客户端通过websocket API提供的如下4个事件进行编程: 

  • onopen 建立连接后触发
  • onmessage 收到消息后触发
  • onerror 发生错误时触发
  • onclose 关闭连接时触发
  •  

  •  

让我们全面了解一下websocket API,他其实非常简单,下面是所有的API: 

[Constructor(in DOMString url, in optional DOMString protocols)]

[Constructor(in DOMString url, in optional DOMString[] protocols)]

interface WebSocket {

readonly attribute DOMString url;



// ready state

const unsigned short CONNECTING = 0;

const unsigned short OPEN = 1;

const unsigned short CLOSING = 2;

const unsigned short CLOSED = 3;

readonly attribute unsigned short readyState;

readonly attribute unsigned long bufferedAmount;



// networking

attribute Function onopen;

attribute Function onmessage;

attribute Function onerror;

attribute Function onclose;

readonly attribute DOMString protocol;

void send(in DOMString data);

void close();

};

WebSocket implements EventTarget;

详细的websocket API,可以参考此文: http://dev.w3.org/html5/websockets/ 

node.js与websocket的结合

终于讲到了正题了,node.js如何与websocket结合,websocket API是基于事件的,他是对于客户端而言,而对于服务端来说,如何来处理呢?其实可以简单的理解为实现websocket协议的socket server开发。 

node.js天生就是一个高效的服务端语言,可以直接使用javascript直接来处理来自客户端的请求,这样如果服务端这边需要大量的业务逻辑开发,则可以直接使用node开发。通过node和websocket的结合可以开发出很多实时性要求很高的web应用,如游戏、直播、股票、监控、IM等等。 

而node.js如何实现websocket的支持,已经有一个比较成熟的开源系统node-websocket-server: https://github.com/miksago/node-websocket-server,让我们来探究一二: 

其实原理也是很简单就是用node实现了websocket draft-76的协议,同时他对外提供了api,可以方便其他应用程序简化编程。 

它继承了node的http.Server的事件和方法,这样它简化了服务端的编程,同时可以处理http的请求。 

为了实现连接之间的通信和消息的广播,它实现了一个manager类,给每一个连接创建一个id,然后在内存中维护一个连接链表,并提供了上线和下线的自动管理。 

它还提供对以下几个事件的接口: 

  • listening 当服务器准备好接受客户端请求时
  • request 当一个http 请求发生时触发
  • stream
  • close
  • clientError
  • error
  •  

  •  

让我们看看一个node-websocket-server提供的一个server的例子: 

var sys = require("sys")

  , ws = require('../lib/ws/server');



var server = ws.createServer({debug: true});



// Handle WebSocket Requests

server.addListener("connection", function(conn){

  conn.send("Connection: "+conn.id);



  conn.addListener("message", function(message){

    conn.broadcast("<"+conn.id+"> "+message);



    if(message == "error"){

      conn.emit("error", "test");

    }

  });

});



server.addListener("error", function(){

  console.log(Array.prototype.join.call(arguments, ", "));

});



server.addListener("disconnected", function(conn){

  server.broadcast("<"+conn.id+"> disconnected");

});



server.listen(8000);

 

这个例子非常的简单,可以看到对于websocket的server端开发,我们已经不需要考虑websocket协议的实现,他几乎有着和客户端浏览器上websocket API一样的事件,只有对连接、断开连接、消息、错误等事件进行处理,这样应用的开发就非常的灵活了。

 

实例:用websocket和node.js搭建实时监控系统

通过websocket打通了浏览器和服务端之后,我们就可以尝试搭建一个实际的应用,这里以实时监控系统为例。 

 

直接与linux自身监控工具的结合,将监控结果通过websocket直接更到网页上,由于建立了socket长连接,绑定iostat的标准输出的事件,做到了真正的实时。同时可以支持对监控结果的讨论,增加了一个简单的chat,基于事件的通讯中,chat和监控同时发送完全不受影响,所以还可以把更多的事件加入进来。 

让我们来看看这个过程: 

首先是用node.js捕获iostat的输出: 

var sys = require("sys")

  , ws = require('../lib/ws/server');



var sys = require('sys');

var spawn = require('child_process').spawn;

var mon = spawn("iostat",["-I","5"]);

spawn可以根据参数启动一个进程,同时可以对stdout, stderr, exit code进行捕获,当这些事件触发时,可以绑定我们的函数,同时捕获其输出。 
这里是iostat的标准输出: 



disk0 cpu load average

KB/t tps MB/s us sy id 1m 5m 15m

14.64 4 0.06 7 5 88 0.76 0.95 0.90

我们捕获他的输出,将其发送到客户端去: 

  mon.stdout.on('data',function(data) {

    data = format_string(data);

    sys.puts(data);

    conn.send("#mon:"+data+"");

  });

客户端也就是浏览器,在收到消息后,对其进行简单的字符串处理,然后就可以直接在网页中输出了。 

w.onmessage = function(e) {

    var msg = e.data;

    if(msg.match(/#mon:/)) {

        var monarr = msg.split(":")[1].split(" ");

        var body = "";

        for(var item in monarr) {

            body += ""+monarr[item]+""

        }

        $("#iobody").html(body);

        //log(monarr[0]);



    }

    else

        log(e.data);

}

这里自定义了一个#mon的简单协议,这样可以对更多类型的输出分开处理。 

服务端和客户端总共100多行的代码,就已经实现了一个实时服务器性能监控系统。 
全部代码下载地址: http://cnodejs.googlecode.com/svn/trunk/monsocket/examples/ 
(注:本程序仅在mac osx下测试通过) 

如果加上RGraph(基于html5),则可以打造更加精美的实时展现:  http://www.rgraph.net/docs/dynamic.html 

总结

这篇文章适合node.js的初学者或者对于websocket不够了解的人,总结起来,就是以下几个点: 

  • 使用websocket API可以开发实时web应用
  • websocket api和 node.js可以很完美的配合
  • node-websocket-server 封装了websocket协议,使服务端进行websocket的开发,非常的简单
  • node的易用性,使其在服务端略加编程,即可以打造一个完美的后台服务
  • node的事件驱动的机制保证了系统的高效,可以使用EventEmitter定义自己的事件触发
  • 对于命令行输出可以使用spawn来捕获,通过在web应用中充分利用linux的各种系统工具
  •  

WebSocket实战

 Nodejs  WebSocket实战已关闭评论
5月 162013
 

转自:http://ued.sina.com.cn/?p=900

前言

互联网发展到现在,早已超越了原始的初衷,人类从来没有像现在这样依赖过他;也正是这种依赖,促进了互联网技术的飞速发展。而终端设备的创新与发展,更加速了互联网的进化;

 

HTTP/1.1规范发布于1999年,同年12月24日,HTML4.01规范发布;尽管已到2012年,但HTML4.01仍是主流;虽然HTML5的草案已出现了好几个年头,但转正日期,遥遥无期,少则三五年,多则数十年;而HTML5的客户代理(对于一般用户而言,就是浏览器),则已百家争鸣,星星向荣;再加上移动终端的飞速发展,在大多数情况下,我们都可以保证拥有一个HTML5的运行环境,所以,我们来分享一下HTML5中的WebSocket协议;

本文包含以下六个方面:
1. WebSocket的前世今生
2. WebSocket是什么
3. 为什么使用WebSocket
4. 搭建WebSocket服务器
5. WebSocket API
6. 实例解析

以上六点分为两大块,前3点侧重理论,主要让大家明白WebSocket是什么,而后3点则结合代码实战,加深对WebSocket的认知。

一、WebSocket的前世今生

Web 应用的信息交互过程通常是客户端通过浏览器发出一个请求,服务器端接收和审核完请求后进行处理并返回结果给客户端,然后客户端浏览器将信息呈现出来,这种机制对于信息变化不是特别频繁的应用尚能相安无事,但是对于那些实时要求比较高的应用来说就显得捉襟见肘了。我们需要一种高效节能的双向通信机制来保证数据的实时传输。有web TCP之称的WebSocket应运而生,给开发人员提供了一把强有力的武器来解决疑难杂症。
(PS:其实,在早期的HTML5规范中,并没有包含WebSocket的定义,一些早期的HTML5书籍中,完全没有WebSocket的介绍。直到后来,才加入到当前的草案中。)

二、WebSocket是什么?

其实,从背景介绍中,我们大致的可以猜出,WebSocket是干什么用的。前面我们提到,WebSocket有web TCP之称,既然是TCP,肯定是用来做通信的,但是它又有不同的地方,WebSocket作为HTML5中新增的一种通信协议,由通信协议和编程API组成,它能够在浏览器和服务器之间建立双向连接,以基于事件的方式,赋予浏览器原生的实时通信能力,来扩展我们的web应用,增加用户体验,提升应用的性能。何谓双向?服务器端和客户端可以同时发送并响应请求,而不再像HTTP的请求和响应。

三、为什么使用WebSocket

在WebSocket出现之前,我们有一些其它的实时通讯方案,比较常用的有轮询,长轮询,流,还有基于Flash的交换数据的方式,接下来,我们一一分析一下,各种通信方式的特点。

① 轮询
这是最早的一种实现实时web应用的方案;原理比较简单易懂,就是客户端以一定的时间间隔向服务器发送请求,以频繁请求的方式来保持客户端和服务器端的数据同步。但是问题也很明显:当客户端以固定频率向服务器端发送请求时,服务器端的数据可能并没有更新,这样会带来很多无谓的请求,浪费带宽,效率低下。

② 长轮询
长轮询是对定时轮询的改进和提高,目地是为了降低无效的网络传输。当服务器端没有数据更新的时候,连接会保持一段时间周期直到数据或状态改变或者时间过期,通过这种机制来减少无效的客户端和服务器间的交互。当然,如果服务端的数据变更非常频繁的话,这种机制和定时轮询比较起来没有本质上的性能的提高。

③ 流
长轮询是对定时轮询的改进和提高,目地是为了降低无效的网络传输。当服务器端没有数据更新的时候,连接会保持一段时间周期直到数据或状态改变或者时间过期,通过这种机制来减少无效的客户端和服务器间的交互。当然,如果服务端的数据变更非常频繁的话,这种机制和定时轮询比较起来没有本质上的性能的提高。

④ 基于Flash的实时通讯方式
Flash有自己的socket实现,这为实时通信提供了可能。我们可以利用Flash完成数据交换,再利用Flash暴露出相应的接口,方便JavaScript调用,来达到实时传输数据的目的。这种方式比前面三种方式都要高效,而且应用场景比较广泛;因为flash本身的安装率很高;但是在当前的互联网环境下,移动终端对flash的支持并不好,以IOS为主的系统中根本没有flash的存在,而在android阵营中,虽然有flash的支持,但实际的使用效果差强人意,即使是配置较高的移动设备,也很难让人满意。就在前几天(2012年6月底),Adobe官方宣布,不在支持android4.1以后的系统,这基本上宣告了flash在移动终端上的死亡。

下面是轮询和长轮询的信息流转图:

对比完四种不同的实时通信方式,不难发现,除了基于flash的方案外,其它三种方式都是用AJAX方式来模拟实时的效果,每次客户端和服务器端交互时,都是一次完整的HTTP请求和应答的过程,而每一次的HTTP请求和应答都带有完整的HTTP头信息,这就增加每次的数据传输量,而且这些方案中客户端和服务端的编程实现比较复杂。

接下来,我们再来看一下WebSocket,为什么要使用它呢?高效节能,简单易用。
下图是来自websocket.org的测试结果:

在流量和负载增大的情况下,WebSocket 方案相比传统的 Ajax 轮询方案有极大的性能优势;而在开发方面,也十分简单,我们只需要实例化WebSocket,创建连接,查看是否连接成功,然后就可以发送和相应消息了。我们会在后面的实例中去详细的说明API。

四、搭建WebSocket服务器

其实,在服务器的选择上很广,基本上,主流语言都有WebSocket的服务器端实现,而我们作为前端开发工程师,当然要选择现在比较火热的NodeJS作为我们的服务器端环境了。

NodeJS本身并没有原生的WebSocket支持,但是有第三方的实现(大家要是有兴趣的话,完全可以参考WebSocket协议来做自己的实现),我们选择了“ws”作为我们的服务器端实现。

由于本文的重点是讲解WebSocket,所以,对于NodeJS不做过多的介绍,不太熟悉的朋友可以去参考NodeJS入门指南(http://www.nodebeginner.org/index-zh-cn.html)。

安装好NodeJS之后,我们需要安装“ws”,也就是我们的WebSocket实现,安装方法很简单,在终端或者命令行中输入:

1 npm install ws

,等待安装完成就可以了。

接下来,我们需要启动我们的WebSocket服务。首先,我们需要构建自己的HTTP服务器,在NodeJS中构建一个简单的HTTP服务器很简单,so easy。代码如下:

1 var app = http.createServer( onRequest ).listen( 8888 );

 

onRequest()作为回调函数,它的作用是处理请求,然后做出响应,实际上就是根据接收的URL,在服务器上查找相应的资源,最终返回给浏览器。
在构建了HTTP服务器后,我们需要启动WebSocket服务,代码如下:

1 var WebSocketServer = require('ws').Server;
2 var wss = new WebSocketServer( { server : app } );

 

从代码中可以看出,在初始化WebSocket服务时,把我们刚才构建好的HTTP实例传递进去就好。到这里,我们的服务端代码差不多也就编写完成了。怎么样?很简单吧。

五、WebSocket API

上面我们介绍了WebSocket服务端的知识,接下来,我们需要编写客户端代码了。在前面我们说过,客户端的API也是一如既往的简单:

见上图:ready state中定义的是socket的状态,分为connection、open、closing和closed四种状态,从字面上就可以区分出它们所代表的状态。


上图描述的是WebSocket的事件,分为onopen、onerror和onclose;


上图为消息的定义,主要是接收和发送消息。注意:可以发送二进制的数据。

以上个图的具体的含义就不再一一赘述,详细描述请参考:
http://www.w3.org/TR/2012/WD-websockets-20120524/
PS:由于WebSocket API(截止到2012年7月)还是草案,API文档和上文所描述的会有所不同,请以官方文档为主,这也是我为什么不详细描述API中各个属性的原因。

另外一点需要提醒大家的是:在前端开发中,浏览器兼容是必不可少的,而WebSocket在主浏览器中的兼容还是不错的,火狐和Chrome不用说,最新版的支持非常不错,而且支持二进制数据的发送和接收。但是IE9并不支持,对于国内的大多数应用场景,WebSocket无法大规模使用。

截图来自(http://tongji.baidu.com/data/browser),之所以选择百度的统计数据,是因为更加符合国内的实际情况。图中所展示的是2012年4月1日到2012年6月30日之间的统计数据,从图中不难看出IE6.0、奇虎360、IE7.0和IE8.0加起来一共占据了77%的市场,FireFox属于其他,chrome只有5.72%的份额,再一次告诉我们,我们的主战场依然是IE系。

既然是IE系,那么对于WebSocket在实际app中的应用就基本不可能了。但我们完全可以在chrome、FireFox、以及移动版的IOS浏览器中使用它。

六、实例解析

搭建好了服务端,熟悉了API,接下来,我们要开始构建我们的应用了。鉴于WebSocket自身的特点,我们的第一个demo选择了比较常见的聊天程序,我们暂且取名为chat。

说到聊天,大家最先想到的肯定是QQ,没错,我们所实现的应用和QQ类似,而且还是基于web的。因为是demo,我们的功能比较简陋,仅实现了最简单的会话功能。就是启动WebSocket服务器后,客户端发起连接,连接成功后,任意客户端发送消息,都会被服务器广播给所有已连接的客户端,包括自己。

既然需要客户端,我们需要构建一个简单的html页面,页面中样式和元素,大家可以自由发挥,只要能够输入消息,有发送按钮,最后有一个展示消息的区域即可。具体的样子大家可以看附件中的demo。

写玩HTML页面之后,我们需要添加客户端脚本,也就是和WebSocket相关的代码;前面我们说过,WebSocket的API本身很简单,所以,我们的客户端代码也很直接,如下:

1 var wsServer = 'ws://localhost:8888/';
2 var websocket = new WebSocket(wsServer);
3 websocket.binaryType = "arraybuffer";
4 websocket.onopen = onOpen;
5 websocket.onclose = onClose;
6 websocket.onmessage = onMessage;
7 websocket.onerror = onError;

 

首先,我们需要指定WebSocket的服务地址,也就是var wsServer = ‘ws://localhost:8888/’;

然后,我们实例化WebSocket,new WebSocket(wsServer),
剩下的就是指定相应的回调函数了,分别是onOpen,onClose,onMessage和onError,对于咱们的实验应用来说,onopen、onclose、onerror甚至可以不管,咱们重点关注一下onmessage。

onmessage()这个回调函数会在客户端收到消息时触发,也就是说,只要服务器端发送了消息,我们就可以通过onmessage拿到发送的数据,既然拿到了数据,接下去该怎么玩,就随便我们了。请看下面的伪代码:

1 function onMessage(evt) {
2     var json = JSON.parse(evt.data);
3     commands[json.event](json.data);
4 }

 

因为onmessage只接收字符串和二进制类型的数据,如果需要发送json格式的数据,就需要我们转换一下格式,把字符串转换成JSON格式。只要是支持WebSocket,肯定原生支持window.JSON,所以,我们可以直接使用JSON.parse()和JSON.stringify()来进行转换。
转换完成后,我们就得到了我们想要的数据了,接下来所做的工作就是将消息显示出来。实际上就是

1 Elements.innerHTML += data + '</br>';

 

上面展现了客户端的代码,服务器端的代码相对要简单一些,因为我们的服务器端使用的是第三方实现,我们只需要做一些初始化工作,然后在接收到消息时,将消息广播出去即可,下面是具体的代码:

01 var app = http.createServer( onRequest ).listen( 8888 );
02 var WebSocketServer = require('ws').Server,
03     wss = new WebSocketServer( { server : app } );
04 wss.on('connection', function( ws ) {
05     console.log('connection successful!');
06     ws.on('message', function( data, flags ) {
07         console.log(data);
08         //do something here
09     });
10     ws.on('close', function() {
11         console.log('stopping client');
12     });
13 });

 

我们可以通过wss.clients获得当前已连接的所有客户端,然后遍历,得到实例,调用send()方法发送数据;

1 var clients = wss.clients, len = clients.length, i = 0;
2         for( ; i < len; i = i + 1 ){
3             clients[i].send( msg );
4         }

 

说到这里,一个双向通信的实例基本完成,当然,上面都是伪代码,完整的demo请查看附件。

除了常见的聊天程序以外,大家完全可以发挥创意,构建一些“好玩”的应用;
接下来,分享另外一个应用,“你画我猜”这个应用,很多人都接触过,大致上是:某个人在屏幕上画一些图形,这些图片会实时展示在其它人的屏幕上,然后来猜画的是什么。

利用WebSocket和canvas,我们可以很轻松的构建类似的应用。当然,我们这里只是demo,并没有达到产品级的高度,这里只是为大家提供思路;
首先,我们再次明确一下,WebSocket赋予了我们在浏览器端和服务器进行双向通信的能力,这样,我们可以实时的将数据发送给服务器,然后再广播给所有的客户端。这和聊天程序的思路是一致的。

接下来,服务器端的代码不用做任何修改,在html页面中准备一个canvas,作为我们的画布。如何在canvas上用鼠标画图形呢?我们需要监听mousedown、mousemove和mouseup三个鼠标事件。说到这里,大家应该知道怎么做了。没错,就是在按下鼠标的时候,记录当前的坐标,移动鼠标的时候,把坐标发送给服务器,再由服务器把坐标数据广播给所有的客户端,这样就可以在所有的客户端上同步绘画了;最后,mouseup的时候,做一些清理工作就ok了。下面是一些伪代码:

01 var WhiteBoard = function( socket, canvasId ){
02                 var lastPoint = null,
03                     mouseDown = false,
04                     canvas = getById(canvasId),
05                     ctx = canvas.getContext('2d');
06  
07                 var handleMouseDown = function(event) {
08                     mouseDown = true;
09                     lastPoint = resolveMousePosition.bind( canvas, event )();
10                 };
11  
12                 var handleMouseUp = function(event) {
13                     mouseDown = false;
14                     lastPoint = null;
15                 };
16  
17                 var handleMouseMove = function(event) {
18                     if (!mouseDown) { return; }
19                     var currentPoint = resolveMousePosition.bind( canvas, event )();
20                     socket.send(JSON.stringify({
21                         event: 'draw',
22                         data: {
23                             points: [
24                                 lastPoint.x,
25                                 lastPoint.y,
26                                 currentPoint.x,
27                                 currentPoint.y
28                             ]
29                         }
30                     }));
31  
32                     lastPoint = currentPoint;
33                 };         
34  
35                 var init = function(){
36                     addEvent( canvas, 'mousedown', handleMouseDown );
37                     addEvent( canvas, 'mouseup', handleMouseUp );
38                     addEvent( canvas, 'mousemove', handleMouseMove );
39  
40                     var img = new Image();
41                     addEvent( img, 'load', function(e){
42                         canvas.width = img.width;
43                         canvas.height = img.height;
44                         ctx.drawImage( img, 0, 0 );
45                     } );
46                     img.src = '/img/diablo3.png';
47                 };
48  
49                 var drawLine = function(data) {
50                     var points = data.points;
51                     ctx.strokeStyle = 'rgb(255, 15, 255)';
52                     ctx.beginPath();
53                     ctx.moveTo( points[0] + 0.5, points[1] + 0.5 );
54                     ctx.lineTo( points[2] + 0.5, points[3] + 0.5 );
55                     ctx.stroke();
56                 };
57  
58                 function resolveMousePosition(event) {
59                     var x, y;
60                     if (event.offsetX) {
61                         x = event.offsetX;
62                         y = event.offsetY;
63                     } else {  //(注意)实际开发中,这样获取鼠标相对canvas的坐标是不对的
64                         x = event.layerX - this.offsetLeft;
65                         y = event.layerY - this.offsetTop;
66                     }
67                     return { x: x, y: y };
68                 };
69  
70                 init();
71  
72                 return {
73                     draw : drawLine
74                     //ctx : ctx,
75                     //canvas : canvas
76                 }
77             }( websocket, 'drawsomething' );

对于canvas不熟悉的同学,请自己去搜索一下,有许多不错的教程。其它方面,和聊天应用的思路基本一样。

最后,我们需要明确一点,WebSocket本身的优点很明显,但是作为一个正在演变中的web规范,我们必须清楚的认识到WebSocket在构建应用时的一些风险;虽然本身有很多局限性,但是这项技术本身肯定是大势所趋,WebSocket在移动终端,在chrome web store都有用武之地,我们可以进行大胆的尝试,让我们在技术的革新中不被淘汰。

Resources:
http://www.w3.org/TR/websockets/
W3 API的官方文档,有详细的接口设计文档和实现步骤
http://tools.ietf.org/html/rfc6455
WebSocket协议
http://tools.ietf.org/html/rfc6202
Known Issues and Best Practices for the Use of Long Polling and Streaming in Bidirectional HTTP
http://msdn.microsoft.com/en-us/library/ie/hh673567(v=vs.85).aspx
msdn中关于WebSocket的介绍
https://developer.mozilla.org/en/WebSockets
http://caniuse.com/#feat=websockets
Compatibility tables for support of HTML5, CSS3, SVG and more in desktop and mobile browsers.

NodeJS ——-NPM的使用

 Nodejs  NodeJS ——-NPM的使用已关闭评论
2月 272013
 

NPM是一个Node包管理和分发工具,已经成为了非官方的发布Node模块(包)的标准。有了NPM,可以很快的找到特定服务要使用的包,进行下载、安装以及管理已经安装的包。

NPM常用的命令有:

(1)$ npm install moduleNames

安装Node模块

注意事项:如果在使用模块的时候不知道其名字,可以通过http://search.npmjs.org网站按照

索引值找到想要的模块。npm也提供了查询的功能  $ npm search indexName

安装完毕后会产生一个node_modules目录,其目录下就是安装的各个node模块。

node的安装分为全局模式和本地模式。一般情况下会以本地模式运行,包会被安装

到和你的应用代码统计的本地node_modules目录下。在全局模式下,Node包会被

安装到Node的安装目录下的node_modules下。全局安装命令为

$ npm install -g moduleName。获知使用$npm set global=true来设定安装模式

,$npm get global可以查看当前使用的安装模式。

(2)$ npm view moduleNames

查看node模块的package.json文件夹

注意事项:如果想要查看package.json文件夹下某个标签的内容,可以使用

$ npm view moduleName labelName

(3)$ npm list

查看当前目录下已安装的node包

注意事项:Node模块搜索是从代码执行的当前目录开始的,搜索结果取决于当前使用的目录中

的node_modules下的内容。$ npm list parseable=true可以目录的形式来展现当

前安装的所有node包

(4)$ npm help

查看帮助命令

(5)$ npm view moudleName dependencies

查看包的依赖关系

(6)$ npm view moduleName repository.url

查看包的源文件地址

(7)$ npm view moduleName engines

查看包所依赖的Node的版本

(8)$npm help folders

查看npm使用的所有文件夹

(9)$ npm rebuild moduleName

用于更改包内容后进行重建

(10)$ npm outdated

检查包是否已经过时,此命令会列出所有已经过时的包,可以及时进行包的更新

(11)$ npm update moduleName

更新node模块

(12)$ npm uninstall moudleName

卸载node模块

(13)一个npm包是包含了package.json的文件夹,package.json描述了这个文件夹的结构。访

问npm的json文件夹的方法如下:

$ npm help json

此命令会以默认的方式打开一个网页,如果更改了默认打开程序则可能不会以网页的形式打

开。

(14)发布一个npm包的时候,需要检验某个包名是否已存在

$ npm search packageName