如何理解NodeJS

(一)基本原理、优势、劣势等

用NodeJS相关技术作为项目的主平台开发已一年有余,写个小文章谈一下如何更好的理解NodeJS。

要理解NodeJS,就必须看看它和其他的Java,C#写的服务器有何不同。

如果你Google一下的话,就会发现很多类似这样的文章:w/1M concurrent connections! 或者这样的:600k concurrent websocket.

而Java服务器,在没有用任何工具的情况下大概一个服务器能撑500-1000个并发。(除非上和NodeJS原理相似的netty

为什么NodeJS能在默认单线程的情况下实现如此高的并发呢?这和NodeJS的三大特点:

  • Single threaded
  • Event Loop
  • Non-blocking I/O

是分不开的。


举个具体的比喻:我们传统的程序处理方式就和去银行办事情一样,我们手里拿着几件要办的事情来到银行。从门口的机器里面拿到一个号,一般有多个窗口在办业务(多线程),当叫到我们的号时,我们去特定的窗口把我们要办的事情都办完(阻塞性),然后下一个人。如下图: (一家拥有3个柜台/服务员的银行)

现在,更高效的NodeJS银行开张了,去里面办事是一种什么体验呢?你发现这个银行竟然只有一个业务窗口(单线程,第一印象,骗子!这怎么可能高效!!!)。你也会拿到一个号,但是当你被叫到号时,你发现业务员竟然不亲自帮你办任务,而是让你把你要办的事情直接写个列表给他之后就让你滚蛋了(非阻塞)。但是,喂,我的事情还没办完啊!银行告诉你,你的事情不是窗口的业务员办的,而是业务员后面的内部人员办的(Event Loop)。你可以继续等听广播,如果你的号码代表的事情做完了,广播再次叫你的号。如果你不关心结果,你可以直接走人(事情办完),或者你可以等事情真的办完,再次叫你的号继续办其他事情(CallBack)。如下图: (注意工作区里面的办事方式也不是一个人拿一个事情傻干,数据库操作,文件操作、网络操作等一些阻塞的事情会有单独的人去做)

所以,NodeJS对高并发操作的效率相当的高,直接裸写的代码几乎达到了Nginx的负载级别。


说完了好话,来看看NodeJS的局限性吧。

1.单线程意味着单点失败(Single Points of Failure)

在NodeJS里面你要非常小心的处理所有的错误,由于默认单线程(虽然现在可以用forever等工具去重启或者上cluster),所有未处理/捕获的错误都会造成整个服务器结束。写的时候多考虑边缘条件,失败处理,如何抛错吧,不然服务器老是down维护会相当苦(不要问我为什么知道。。。)。

2.你的程序逻辑实现方式和传统的不同了(异步、异步、异步)

就拿上面银行的例子来说,效率是提高了,但是如果你不是只办一件事情的话,办的流程复杂了。传统银行里,比如你要:从A转1000到B,再把B里面所有钱都转到C账户。你排到号之后,坐下来,和服务员说你要顺序做下面两件事:

  • (一)A账户转账B,1000
  • (二)把B账户所有的钱转到C账户上

是绝对不会有问题的。最后B账户的余额是0.

如果在NodeJS银行,你排到号,告诉柜员你要做(一)和(二)两件事情,基本上,出来的结果和你想要的很有可能是不一样的。为什么?因为在异步机制下,(一)和(二)只是顺序触发的两个任务而已,(二)的开始和(一)的完成在时间上没有先后的顺序关系,很大几率下,最后的结果是B账户里还剩下1000,而不是你期望的0。

来个实际代码的例子,比如可能人人都会写的登录代码:

String username = getUsername();
String password = getPassword();
User user = getUser(username);
If(user.password.equals(user)){
    …
}

然而,在NodeJS里,你可能必须写成这样:

function authenticate() {
    return getUsername()
      .then(function (username) {
          return getUser(username);
      })
      // chained because we will not need the user name in the next event
      .then(function (user) {
          return getPassword()
         // nested because we need both user and password next
                .then(function (password) {
                    if (user.passwordHash !== hash(password)) {
                        throw new Error("Can't authenticate");
                    }
                });
      });
}

是的,这已经是使用了promise优化过的代码,如果使用默认的callback模式会更加惨不忍睹。


总结,如果你希望快速开发出能支持高并发大数据量的服务器的话,请选择NodeJS,虽然在实现复杂业务逻辑的时候可能会让你痛不欲生。

预告:(二)如何写好异步代码