淘先锋技术网

首页 1 2 3 4 5 6 7

本文转载自:众成翻译
译者:levon
链接:http://www.zcfy.cc/article/508
原文:https://medium.com/yet-another-node-js-blog/architecture-of-node-js-internal-codebase-57cd8376b71f#.gxqbcc2zd

首先,讲一些关于JavaScript的故事。。。

Stack Overflow的联合创始人Jeff Atwood曾经在他的技术博客Coding Horror中写道:

任何应用如果能用JavaScript实现,尽量用JavaScript。

最近几年JavaScript的影响和成就越来越大,已经成为最流行的编程语言。实际上,据2016年Stack Overflow Developer调查,JavaScript在 Most Popular TechnologyTop Tech on Stack Overflow中排名第一,并且在其他结果中也排名靠前。

Node.js是服务端JavaScript环境,是服务器关键功能的基础,比如二进制数据操作、文件I/O操作、数据库访问、计算机网络等等。它有一些独一无二的特性,使得它在众多现有的成熟框架中凸显出来,比如Django (Python), Laravel(PHP), RoR (Ruby)等等。一些技术公司 PayPal, Tinder, Medium,LinkedIn, Netflix因为这些特性而使用它,有些甚至在1.0版本就已经开始使用。

我最近在Stack Overflow上回答过一个关于Node.js内部代码架构的问题,从而激发我写出了这篇文章。


官方文档对什么是Node.js并没有介绍的很清楚。

Node.js是运行于Chrome V8引擎的JavaScript语言,使用事件驱动,非阻塞I/O模型。

为了更深刻的理解这句话,我们把Node.js的组件分解,详细阐述一些关键词,然后解释不同代码段之间的相互交互是如何产生了强大的Node.js的运行环境。

Node.js架构(上层到下层)

组件/依赖

V8:高性能JavaScript执行引擎,是谷歌开源软件,C++语言实现。它也是Chrome浏览器的内部引擎。JavaScript脚本被V8编译成机器码(因此非常快),然后执行。为什么V8这么快?请看StackOverflow的回答

libuv: 异步特性的C语言库。它包含时间轮询,线程池,文件系统事件和一些提供关键功能的子进程。

其他C/C++组件:比如c-arescrypto(OpenSSL)http-parserzlib。这些底层组件的交互为服务器提供网络、压缩和编解码等重要功能。

Application/Modules:这里是所有JavaScript代码,你的应用代码,Node.js核心模块,你从npm安装的任何模块,和你写的所有模块。是你工作的主要部分。

Bindings:这时候你可能会发现Node.js是用JavaScript和C/C++写的。为什么有这么多C/C++的代码?因为快。但是,他们是三种不同的编程语言吗?是的。一般情况下,不同编程语言写的程序之间是无法通信的。怎样让你写的代码和C/C++代码能够平滑通信?如果没有bindings,他们确实无法通信。Bindings,正如名字的暗示,像“胶水”一样把不同的语言捆绑到一起。正因为如此,bindings把Node.js内部的C/C++类库(c-ares, zlib, OpenSSL, http-parser等)暴露给JavaScript。写bindings的一个动机是代码复用:如果某个需要的功能已经被实现,为什么还要再全部编写一遍呢?仅仅是因为它们是不同语言的实现吗?为什么不在它们之间“搭桥”呢?另一个动机是性能:C/C++这种系统编程语言通常比高阶语言(比如Python,JavaScript,Ruby等等)快得多。所以某些功能(比如CPU密集操作)用C/C++写是明智的。

C/C++ Addons:Bindings只对Node.js的内部核心类库绑定,比如zlib, OpenSSL, c-ares, http-parser等等。如果想引入第三方的C/C++类库到你的应用,就必须自己写绑定代码。自己写的绑定代码叫做addons。可以把bindings、addons当做JavaScript和C/C++之间的“桥梁”。

术语

I/O输入/输出的缩写。它主要是指操作系统中处理输入/输出的子系统。I/O密集型操作一般是那些和磁盘/驱动交互的操作。比如数据库的访问和文件系统操作。其他相关概念包括CPU密集型操作,内存密集型操作等等。判断一个操作是I/O密集型还是其他密集型的方法是看看增加什么资源可以提高这条指令操作的性能。比如说,因为CPU处理能力的提升,某个操作的速度显著加快了,那这就是CPU密集型操作。

非阻塞/异步:一般来说,当一个请求过来,应用会处理这个请求并且挂起其他操作直到处理结束。这导致一个问题:当有好多请求同时过来的时候,所有请求都要等待直到之前一个请求处理完成。换句话说,之前的操作会阻塞接下来的操作。更糟的是,如果之前的请求需要很长的响应时间(比如计算1000以内的素数,或者从数据库中读取3GB的数据),所有其他请求将会被阻塞很长时间。为了解决这个问题,一种方法是使用多进程或者多线程,所有请求在一个单一线程处理。更聪明的方法是:把请求中的所有I/O操作(文件系统访问,数据库读/写)放到libuv维护的工作线程,在后台运行,换句话说,请求中的I/O操作以异步的方式处理,而不是在主线程中。这样的话主线程就不会被阻塞,就像重物被装到船的各个地方。这样你的代码只需要在主线程工作。libuv隔离了你和线程池中的工作线程。你不需要考虑也不需要担心他们。Node.js帮助你管理他们。这样的架构让I/O操作非常高效。但是,这并不是没有缺点。除了I/O密集型操作,还有CPU密集型,内存密集型等等。Node.js只是针对I/O密集型操作提供了异步功能。有一些方法可以处理CPU密集型操作,但是这不是本文的重点,所以没有提及。

事件驱动: 几乎所有的现代系统,比较典型的做法是,主应用启动之后,当进来一个请求,这时初始化一个进程来处理。但是不同技术的实现方式会有不同,甚至差异极大。处理请求的典型实现过程是:为请求创建一个线程;请求的所有操作一个接着一个的执行,如果某个操作比较慢,接下来的操作会在这个线程中等待;当所有操作成功完成,返回响应。但是,Node.js实现方式是,所有的操作都以事件的方式注册到Node.js中,这些事件会被主应用或者请求触发。

运行环境(系统): Node.js系统就是指上面提到的所有内容,和Node.js应用执行所需要的支持。

总结

现在我们已经对Node.js的组件做了概览,为了对它的架构和组件的交互有一个更好的认识,接下来我们将会研究它的工作流。

一个Node.js应用启动,V8引擎开始执行你写的代码。应用中的对象(注册事件的函数)会变成一系列的观察者。事件发生的时候,相应的观察者会得到通知。

事件发生,观察者的回调函数会被加入消息队列 。只要消息队列有数据,循环函数 会不停取出它们压入执行堆栈 。注意,只有先前的消息处理完成循环函数 才会把下一个压入执行堆栈

执行堆栈中,如果发生I/O操作,会把它移交到libuv处理。libuv默认包含一个有四个工作线程的线程池,线程的数量可以设置。工作线程通过和Node.js的底层类库交互来执行比如数据传输、文件访问等操作。libuv处理完后再把事件加入消息队列,Node.js主线程继续处理。libuv以异步方式处理,Node.js主线程不会等待处理结果而是继续执行。libuv处理完成,加入消息队列,循环函数再次把事件压入执行堆栈,这就是Node.js一个消息处理的生命周期。

mbq 曾经把Node.js比喻成餐馆。我对这个比喻稍加修改以让Node.js的事件驱动更容易理解。把Node.js应用想象成星巴克咖啡馆。一个高效的服务员(主线程)生成订单。如果很多顾客同时来买咖啡,他们会排成一队(消息加入队列)等候服务员。服务员接待一个顾客,把订单给经理(libuv),经理安排咖啡师(工作线程)调制。咖啡师会根据顾客的需求使用相应设备和原料(底层C/C++组件)调制饮料。一般有四个咖啡师(线程池)。如果是高峰时段,会安排更多的咖啡师工作(必须在开始前安排好,而不是午餐的时候)。服务员把订单给经理后接待另一个顾客(循环函数把下一个消息压入执行堆栈)而不是等候咖啡做好。你可以把给当前顾客服务想象成执行堆栈中压入的当前消息。咖啡做好之后,会放到当前最后一个排队顾客后面。如果咖啡移动到柜台,服务员叫顾客的名字,顾客就得到了他的咖啡。 (下划线部分的比喻有点奇怪;如果从程序的观点来看是合理的)


这篇文章浅显的介绍了Node.js内部代码架构和事件驱动,但是并没有涉及很多问题和细节,比如CPU密集型操作的处理、Node.js设计模式等。后面将会有更多文章讲述这些问题。