Node.js技术架构

Node.js是什么?

Node.js最初开始于2009年,让JavaScript代码离开浏览器的执行环境也可以执行

可以将Node.js理解为一个将多种技术组合起来的平台,可以使用JavaScript调用系统接口,开发后端应用

既然说到将多种技术组合起来,那么可以先看看Node.js用到了哪些技术


node v1.0.png

图片是nodejs v1.0也就是最早发布的node版本下的deps文件,也就是nodejs所用到的依赖

  • cares:用C-ares做域名解析
  • gtest:是C/C++的单元测试框架
  • http-parser:用来解析http
  • npm:包管理工具
  • openssl:用来解析https
  • uv:一个跨平台的异步I/O
  • v8:google开发的js引擎,为js提供运行环境
  • zlib:用来做加密

那么这些技术又是怎么进行组合的呢,再看看Node.js的技术架构

node技术架构.png

将Node.js分成三层

  • 首先最上层是node api,提供http模块、流模块、文件模块等等,可以使用js直接调用
  • 中间层node bindings主要是使js和C/C++进行通信
  • 最下面这一层是支撑nodejs运行的关键,主要由v8libuvc-ares等模块组成,向上一层提供api服务

相信我们或多或少都接触过第一层Node api,刚刚也通过Node安装的依赖初步了解了最下层的模块具有什么功能,那么中间的这个Node bindings又是什么呢?

什么是Node bindings?

背景:C/C++实现了一个用来解析http的库http-parser,非常高效,可是对于只会写js的程序员非常的不友好,因为没有办法直接去调用这个C/C++的库,这两个语言连最基本的数据类型都不一样,还怎么做朋友
结论:js无法直接调用C++的库,需要一个中间的桥梁(调用途径)

那么bindings需要怎么实现呢?
Node.js的作者Ryan做了一个中间层处理

  • Node.js用C++对http-parse进行封装,使它符合某些要求(比如统一数据类型),封装好的文件叫做http_parse_binding.cpp
  • 用Node.js提供的编译工具将其编译为.node文件
  • js代码可以直接通过require关键字引入这个.node文件

这样js就能够调用C++库,这个中间的桥梁就是bindings,由于node提供了很多binding,所以就叫做node bindings

JS如何与C++通信?

// test.js
const addon = require('./build/Release/addon');

console.log('This should be eight:', addon.add(3, 5));

上面是js调用,再来看看C++代码(已被编译)

// addon.cc
#include <node.h>

namespace demo {

using v8::Exception;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::NewStringType;
using v8::Number;
using v8::Object;
using v8::String;
using v8::Value;

// 这是 "add" 方法的实现。
// 输入参数使用 const FunctionCallbackInfo<Value>& args 结构传入。
void Add(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  // 检查传入的参数的个数。
  if (args.Length() < 2) {
    // 抛出一个错误并传回到 JavaScript。
    isolate->ThrowException(Exception::TypeError(
        String::NewFromUtf8(isolate, 
                        "参数的数量错误",
                            NewStringType::kNormal).ToLocalChecked()));
    return;
  }

  // 检查参数的类型。
  if (!args[0]->IsNumber() || !args[1]->IsNumber()) {
    isolate->ThrowException(Exception::TypeError(
        String::NewFromUtf8(isolate, 
                        "参数错误",
                            NewStringType::kNormal).ToLocalChecked()));
    return;
  }

  // 执行操作
  double value =
      args[0].As<Number>()->Value() + args[1].As<Number>()->Value();
  Local<Number> num = Number::New(isolate, value);

  // 设置返回值 (使用传入的 FunctionCallbackInfo<Value>&)。
  args.GetReturnValue().Set(num);
}

void Init(Local<Object> exports) {
  NODE_SET_METHOD(exports, "add", Add);
}

NODE_MODULE(NODE
_GYP_MODULE_NAME, Init)

}  // 命名空间示例

Nodejs封装的插件开放一些对象和函数,供运行在Node.js中的JS访问,当JS调用函数addon时,输入参数和返回值与C/C++代码相互映射,统一封装处理。这样就可以直接在Node.js中引入并使用

// test.js
const addon = require('./build/Release/addon');
// 传入一个函数
addon((msg) => {
  console.log(msg);
// 打印: 'hello world'
});

传入C++并执行

// addon.cc
#include <node.h>

namespace demo {

using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::NewStringType;
using v8::Null;
using v8::Object;
using v8::String;
using v8::Value;

void RunCallback(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();
  Local<Function> cb = Local<Function>::Cast(args[0]);
  const unsigned argc = 1;
  // 这里有一个c++方法,将args[0]也就是我们传入的函数,转化成c++看得懂的,用cb接收
  Local<Value> argv[argc] = {
      String::NewFromUtf8(isolate,
                          "hello world",
                          NewStringType::kNormal).ToLocalChecked() };
  // 调用一下,传入的函数就被调用了,打印出hello world
  cb->Call(context, Null(isolate), argc, argv).ToLocalChecked();
}

void Init(Local<Object> exports, Local<Object> module) {
  NODE_SET_METHOD(module, "exports", RunCallback);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Init)

}

在这个例子中,回调函数被同步地调用,要知道C++是看不懂JS的,所以如何做中间层的封装就交给这些node插件去做

有了这些Node.js提供的插件(node binding),JS和C++就可以进行交互了,也使JS的能力被大大的扩展了

再回顾一下Node.js的技术架构

node技术架构.png

刚刚详细介绍了什么是Node bindings,它是如何工作的,接着再来看最下面一层功能模块


下载.png

什么是V8

它是Google开发的js引擎,为js提供运行环境
为啥是v8?它是现阶段执行js最快的一个引擎

那么v8的功能有哪些呢

  • 将JS源代码变成本地代码并执行
  • 维护调用栈,确保JS函数的执行顺序
  • 内存管理,为所有对象分配内存
  • 垃圾回收,重复利用无用的内存
  • 实现JS的标准库

逐个分析一下:

  • 啥是本地代码?其实本地代码就是机器代码,就比如说0和1,计算机看到这些代码直接就可以执行,不再需要借助其他的任何工具,非常的高效。V8在运行之前将js编译成了机器代码
  • JS函数的执行顺序是由v8引擎决定的
  • 那么v8如何做内存管理呢?比如说new一个对象,它的内存在哪里,也是引擎来决定的
  • 而垃圾回收,是因为内存是有限的,比如用了2k的内存,用完了还得还回来给下一个程序用,所以目的就是为了重复利用
  • 标准库这个怎么理解?其实就是实现数组的sortsplice等等api,v8来实现,js来调用

需要注意的是:js是单线程的,而V8本身是多线程的,开一个线程执行js,开一个线程清理内存,然后再处理一些其他别的活儿,线程和线程之间毫无瓜葛

什么是libuv

背景:因为各个系统的I/O库都不一样,windows系统有IOCP,Linux系统有epoll。Node.js的作者Ryan为了将其整合在一起实现一个跨平台的异步I/O库,开始写libuv

好了,背景说完了,啥是I/O?
例如:

  • 从操作系统写文件到硬盘
  • 访问网络,从操作系统发出数据到别的服务器
  • 打印连接打印机,从操作系统发指令给打印机

以上这些行为都是I/O,可以理解为系统和外界进行交互的过程都叫I/O

libuv会根据你是什么系统,自动的选择当前系统已经实现好了的异步操作(I/O)库,用于TCP/UDP/DNS文件等的异步操作

  • 比如操作TCP,我们都知道http是基于TCP/IP的,如果可以操作TCP那么,就可以做http的服务
  • UDP,用于实时通信,常见的QQ聊天
  • 解析DNS

包括读文件、写文件什么的,libuv都可以帮你管理。这样I/O的部分就全部交给c语言去做,js完全不用管,甩手掌柜,负责调用就行了

v8libuv在整个Node.js架构的底层是最为重要的,其他功能就不做详细介绍了

Node.js工作流程

Node.js工作流程

了解了Node Bindings、v8、还有libuv貌似可以把工作流程串一串了

Application就是咱们写的代码,把它放在v8上面去运行。发现需要去读一个文件,这时候libuv开一个线程去读文件。读完文件,操作系统会返回一个事件给event loopevent loop就把文件传回给v8,再给到代码

Emmm...
还是先了解一下Event Loop吧


images.jpeg

什么是Event Loop

Event Loop,是Event和Loop

  • Event
    计时器到期了、文件可以读取了、读取出错了

比如说在js里面写一个setTimeOut,10秒之后打印一行字,所以当10秒钟到了,就会产生一个事件,执行回调
什么时候文件可以读,什么时候文件可以写,或者说读取出错的时候,就需要操作系统生成一个事件(Event)告诉js,因为js啥也不知道
一般来说事件分两种,内部的和外部事件,比如计时器就是内部事件,文件读取就是外部的,因为文件在硬盘上面,硬盘和操作系统又是分开的

  • Loop
    Loop就是循环,由于事件分优先级,所以处理起来也是分先后顺序,所以Node.js需要按顺序轮询每种事件,轮询是循环的

既然说到事件优先级,举个例子,有三种不同的事件

setTimeout(fn1, 100) // 计时器到期了
fs.readFile(‘/1.txt’, fn2) // 文件可以读了
server.on(‘close’, fn3) // 服务器关闭了

以上三种事件如果同时发生,执行顺序是怎么样的?

  1. 执行读文件,文件来了立马去读
    因为如果文件可以读了现在不读,没准儿过会儿就不能读了
  2. 执行服务器事件
    用户请求进来,可以稍微等一会儿,但是如果太久了也可能就不请求了
  3. 执行定时器的事件
    定时器可以拖一下

这个顺序是人为规定的,接着循环

人为规定了一个优先级也就是一个执行顺序,这个人为规定就是event loop

总结下来就是三句话

  • 对不同的事件分优先级
  • node.js顺序的去轮询每一种事件
  • 把这个过程看成循环圈

示意如图:


图片 4.png

timer:先看看有没有计时器,有了执行
I/O:有咩有其他没有归类的回调
Idle:空闲一会儿,清理战场
Poll:轮询阶段,处理大部分的事件(文件可读了?读!http请求来了,处理!)
Check:处理setImmediate回调
Close callback:看看有没有socket关闭的回调
------------循环-----------------
但是node.js不傻,不会一直循环循环,如果发现没什么事儿做,就会停留在poll(轮询)阶段

轮询的阶段呢,会看看有没有文件可以读,有没有请求可以处理,就等着,时不时的看看有没有新的代码,或者检查一下最近的计时器,看看有没有需要过会儿去执行的callback

如果计时器事件要处理了,我再从下出发,绕回timers

Node.js大部分时间都会停留在poll阶段,大部分事件都在poll阶段被处理,如文件、网络请求

相信大家对Event Loop有了一个初步的了解和认识,那么看回Node.js工作流程

Node.js工作流程
  1. Application就是咱们写的代码,把它放在v8上面去运行
  2. 运行的过程中,发现我们写了个setTimeoutv8就会调用Node.js的bindings,把这个settimeout放进Even loop里面
  3. Event loop就会等待适合的时机去发送一个事件去执行这些js代码,接着循环等待,一般停留在poll阶段久一些
  4. 发现需要去读一个文件,这时候Event loop就会通过libuv开一个线程去专门做读文件这事儿
  5. 读完文件,操作系统会返回一个事件给Event loop,Event loop就把文件传回给v8,最后给到代码

需要注意js从头至尾都不参与读文件这个事情,libuv去读
(可以看到最最重要的部分是libuv和v8,而我们写的代码只占小小的一部分)

一句话就是,代码到v8,通过Node api 使用libuv和其他一些C/C++提供的功能去完成用户所需要的功能
Nodejs将这些模块进行整合,所以说Node.js不是一门语言,就是一个平台

下载 _1_.jpeg

最后的最后,回顾总结一下

  • libuv进行异步I/O操作
    Node.js是使用libuv进行异步I/O操作,一般来说读文件是一个同步的动作,这时候有了libuv,Nodejs就把这活儿交给了libuv,让libuv去读这个文件,这时候Node.js就没有什么事儿可以做了,等libuv读完了发过来一个事件,Node.js再接手处理,这就是个很重要的异步过程
  • 用Event loop管理事件处理顺序
    基于libuv,Nodejs又实现了一个Even loop用来管理不同事件的处理顺序
  • C/C++库高效处理DNS/HTTP
    Nodejs还使用一些C++的库,高效的处理了dns/http等常用功能,有了这些功能,基本上就可以处理文件,处理网络等一些杂七杂八的事情
  • 用bindings让JS能和C/C++沟通
    咱们再如何使用js也使用这些功能呢,这时候bindings的价值就体现出来了,让js能够直接和c++沟通,直接require一下.node文件
  • 用V8运行JS
    接着Nodejs又引入了v8,让js代码离开浏览器的执行环境也能够运行
  • 用Node.js标准库简化JS代码
    Node很贴心的给用户准备了很高效的库,比如httpfs之类的,大大简化了你的js代码

那么为啥nodejs可以高效的处理这些请求呢?因为直接使用c语言的代码,要比js快

这就是Node.js

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270