├── chapter2 ├── README.md ├── Context.png ├── 1354452360_3578.png ├── FgrfI3a1NyQLu0FoX76R5DbjdoL0.png ├── c3ad9f4a15cb36af631932a52dec3e96.png ├── e09d7b330d9e754f7ff1282a1af55295.png ├── chapter2-0.md ├── chapter2-1.md └── chapter2-2.md ├── .gitignore ├── alipay.jpg ├── chapter11 ├── 14fig27.gif ├── figure1.gif ├── figure2.gif ├── figure3.gif ├── figure4.gif ├── figure5.gif ├── node-aio.png ├── filesystem.png ├── read-write.png ├── unix-file.jpg ├── README.md ├── chapter11-2.md ├── chapter11-3.md ├── chapter11-1.md ├── chapter11-4.md ├── chapter11-6.md └── chapter11-5.md ├── chapter5 ├── settimeout.jpeg ├── 5fee18eegw1ewjpoxmdf5j20k80b1win.jpg └── chapter5-1.md ├── chapter13 ├── README.md ├── chapter13-2.md └── chapter13-1.md ├── chapter7 ├── 2016-05-09 14.13.19.png └── chapter7-1.md ├── chapter1 ├── 270064-edbf9b53812f0433.png ├── FuX1qcGJgwYtX9zNbBAOSaQeD8Qz.png ├── a9e67142615f49863438cc0086b594e48984d1c9.jpeg ├── README.md ├── chapter1-0.md ├── chapter1-1.md └── chapter1-2.md ├── chapter10 ├── README.md ├── chapter10-1.md └── chapter10-2.md ├── chapter9 ├── README.md ├── chapter9-3.md ├── chapter9-2.md └── chapter9-1.md ├── book.json ├── chapter14 ├── README.md ├── chapter14-5.md ├── chapter14-3.md ├── chapter14-1.md ├── chapter14-2.md └── chapter14-0.md ├── .editorconfig ├── .github └── FUNDING.yml ├── SUMMARY.md ├── README.md ├── chapter12 └── chapter12-1.md ├── chapter6 └── chapter6-1.md ├── chapter4 └── chapter4-1.md ├── chapter8 └── chapter8-1.md └── chapter3 ├── chapter3-2.md └── chapter3-1.md /chapter2/README.md: -------------------------------------------------------------------------------- 1 | # Chapter2 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _book/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /alipay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjhjstz/deep-into-node/HEAD/alipay.jpg -------------------------------------------------------------------------------- /chapter2/Context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjhjstz/deep-into-node/HEAD/chapter2/Context.png -------------------------------------------------------------------------------- /chapter11/14fig27.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjhjstz/deep-into-node/HEAD/chapter11/14fig27.gif -------------------------------------------------------------------------------- /chapter11/figure1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjhjstz/deep-into-node/HEAD/chapter11/figure1.gif -------------------------------------------------------------------------------- /chapter11/figure2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjhjstz/deep-into-node/HEAD/chapter11/figure2.gif -------------------------------------------------------------------------------- /chapter11/figure3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjhjstz/deep-into-node/HEAD/chapter11/figure3.gif -------------------------------------------------------------------------------- /chapter11/figure4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjhjstz/deep-into-node/HEAD/chapter11/figure4.gif -------------------------------------------------------------------------------- /chapter11/figure5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjhjstz/deep-into-node/HEAD/chapter11/figure5.gif -------------------------------------------------------------------------------- /chapter11/node-aio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjhjstz/deep-into-node/HEAD/chapter11/node-aio.png -------------------------------------------------------------------------------- /chapter11/filesystem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjhjstz/deep-into-node/HEAD/chapter11/filesystem.png -------------------------------------------------------------------------------- /chapter11/read-write.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjhjstz/deep-into-node/HEAD/chapter11/read-write.png -------------------------------------------------------------------------------- /chapter11/unix-file.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjhjstz/deep-into-node/HEAD/chapter11/unix-file.jpg -------------------------------------------------------------------------------- /chapter5/settimeout.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjhjstz/deep-into-node/HEAD/chapter5/settimeout.jpeg -------------------------------------------------------------------------------- /chapter2/1354452360_3578.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjhjstz/deep-into-node/HEAD/chapter2/1354452360_3578.png -------------------------------------------------------------------------------- /chapter13/README.md: -------------------------------------------------------------------------------- 1 | # Chapter13 2 | 3 | * [进程](/chapter13/chapter13-1.md) 4 | * [Cluster](/chapter4/chapter4-1.md) 5 | -------------------------------------------------------------------------------- /chapter7/2016-05-09 14.13.19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjhjstz/deep-into-node/HEAD/chapter7/2016-05-09 14.13.19.png -------------------------------------------------------------------------------- /chapter1/270064-edbf9b53812f0433.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjhjstz/deep-into-node/HEAD/chapter1/270064-edbf9b53812f0433.png -------------------------------------------------------------------------------- /chapter10/README.md: -------------------------------------------------------------------------------- 1 | # Chapter10 2 | 3 | * [HTTP Server](/chapter10/chapter10-1.md) 4 | * [HTTP Client](/chapter10/chapter10-2.md) 5 | -------------------------------------------------------------------------------- /chapter1/FuX1qcGJgwYtX9zNbBAOSaQeD8Qz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjhjstz/deep-into-node/HEAD/chapter1/FuX1qcGJgwYtX9zNbBAOSaQeD8Qz.png -------------------------------------------------------------------------------- /chapter2/FgrfI3a1NyQLu0FoX76R5DbjdoL0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjhjstz/deep-into-node/HEAD/chapter2/FgrfI3a1NyQLu0FoX76R5DbjdoL0.png -------------------------------------------------------------------------------- /chapter2/c3ad9f4a15cb36af631932a52dec3e96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjhjstz/deep-into-node/HEAD/chapter2/c3ad9f4a15cb36af631932a52dec3e96.png -------------------------------------------------------------------------------- /chapter2/e09d7b330d9e754f7ff1282a1af55295.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjhjstz/deep-into-node/HEAD/chapter2/e09d7b330d9e754f7ff1282a1af55295.png -------------------------------------------------------------------------------- /chapter5/5fee18eegw1ewjpoxmdf5j20k80b1win.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjhjstz/deep-into-node/HEAD/chapter5/5fee18eegw1ewjpoxmdf5j20k80b1win.jpg -------------------------------------------------------------------------------- /chapter9/README.md: -------------------------------------------------------------------------------- 1 | # Chapter9 2 | 3 | * [Socket](/chapter9/chapter9-1.md) 4 | * [构建应用](/chapter9/chapter9-2.md) 5 | * [加密](/chapter9/chapter9-3.md) 6 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["disqus"], 3 | "pluginsConfig": { 4 | "disqus": { 5 | "shortName": "yjhjstz" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /chapter1/a9e67142615f49863438cc0086b594e48984d1c9.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjhjstz/deep-into-node/HEAD/chapter1/a9e67142615f49863438cc0086b594e48984d1c9.jpeg -------------------------------------------------------------------------------- /chapter14/README.md: -------------------------------------------------------------------------------- 1 | # Chapter14 2 | 3 | * [Node.js & Android](/chapter14/chapter14-1.md) 4 | * [Node.js & Docker](/chapter14/chapter14-2.md) 5 | * [Node.js 调优](/chapter14/chapter14-3.md) 6 | -------------------------------------------------------------------------------- /chapter1/README.md: -------------------------------------------------------------------------------- 1 | # Chapter1 2 | 3 | * [架构一览](/chapter1/chapter1-0.md) 4 | * [为啥是libuv](/chapter1/chapter1-1.md) 5 | * [V8 概念](/chapter2/chapter2-0.md) 6 | * [C++和JS 交互](/chapter2/chapter2-1.md) 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /chapter11/README.md: -------------------------------------------------------------------------------- 1 | # Chapter11 2 | 3 | * [文件系统](/chapter11/chapter11-2.md) 4 | * [文件抽象](/chapter11/chapter11-1.md) 5 | * [IO 那些事儿](/chapter11/chapter11-3.md) 6 | * [libuv的选型](/chapter11/chapter11-4.md) 7 | * [文件 IO](/chapter11/chapter11-5.md) 8 | * [Fs 精粹](/chapter11/chapter11-6.md) 9 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: https://github.com/yjhjstz/deep-into-node/blob/master/alipay.jpg 13 | -------------------------------------------------------------------------------- /chapter14/chapter14-5.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### javascript 的坑 4 | 5 | #### 弱类型 6 | * 判断对象是否不存在使用if(!obj), 此时如果obj为'' 或 0 或 false,也会误认为不存在; 7 | 正确的写法也是纷繁复杂:见参考 8 | 9 | * 字符串连接,一般会自动转成string, 但不巧如果头两个正好可以转数字,那么它会按数字相加.如: 10 | 11 | ```js 12 | var a = 1,b = 'b',c = 10; 13 | var s1 = a + c + b; 14 | var s2 = '' + a + c + b; 15 | console.log(s1); //"11b" 16 | console.log(s2); //"110b" 17 | 18 | ``` 19 | 20 | 保险起见:注意使用 `Number`转换。 21 | 22 | #### this指针 23 | 24 | * 一般调用时,this是窗口的全局对象,比如在浏览器中就是window对象 25 | * `.call`和`.apply`方法调用时可以改变`this`的值 26 | * 在`prototype`函数内部,this指向该类创造的实例对象 27 | 28 | #### 排序 sort 29 | JavaScript的Array的sort()方法就是用于排序的,但是排序结果可能让你大吃一惊: 30 | ```js 31 | [10, 20, 1, 2].sort(); // [1, 10, 2, 20] 32 | ``` 33 | 这是因为Array的sort()方法默认把所有元素先转换为String再排序,结果'10'排在了'2'的前面,因为字符'1'比字符'2'的ASCII码小。 34 | 35 | 如果不知道sort()方法的默认排序规则,直接对数字排序,绝对栽进坑里! 36 | 幸运的是,sort()方法也是一个高阶函数,它还可以接收一个比较函数来实现自定义的排序。 37 | 38 | 39 | #### 异步处理 40 | * 错误处理忘记 `return`. 41 | * 一个异步方法中处理不当有可能会多次触发callback,又或者是一个callback都没触发,需要仔细处理逻辑流程. 42 | 43 | 44 | #### 科学计算 45 | * v8 引擎中 js 的 Number 对象的内部实现只有两种,一是smi(也就是小整数),二是 `double`。Node.js 根本没有 float! 46 | 如果使用了 float , 注意存在精度的缺失。 47 | * 位运算, javascript 只支持32位,超过32位的,需要用大数模拟。 https://github.com/justmoon/node-bignum 48 | 49 | ### 参考 50 | * http://www.ruanyifeng.com/blog/2011/05/how_to_judge_the_existence_of_a_global_object_in_javascript.html -------------------------------------------------------------------------------- /chapter1/chapter1-0.md: -------------------------------------------------------------------------------- 1 | ## 架构一览 2 | 3 | ### 体系架构 4 | Node.js主要分为四大部分,Node Standard Library,Node Bindings,V8,Libuv,架构图如下: 5 | ![node.js](a9e67142615f49863438cc0086b594e48984d1c9.jpeg) 6 | - Node Standard Library 是我们每天都在用的标准库,如Http, Buffer 模块。 7 | - Node Bindings 是沟通JS 和 C++的桥梁,封装V8和Libuv的细节,向上层提供基础API服务。 8 | - 这一层是支撑 Node.js 运行的关键,由 C/C++ 实现。 9 | - V8 是Google开发的JavaScript引擎,提供JavaScript运行环境,可以说它就是 Node.js 的发动机。 10 | - Libuv 是专门为Node.js开发的一个封装库,提供跨平台的异步I/O能力. 11 | - C-ares:提供了异步处理 DNS 相关的能力。 12 | - http_parser、OpenSSL、zlib 等:提供包括 http 解析、SSL、数据压缩等其他的能力。 13 | 14 | ### 代码结构 15 | 树形结构查看,使用 tree 命令 16 | ```shell 17 | ➜ nodejs git:(master) tree -L 1 18 | . 19 | ├── AUTHORS 20 | ├── BSDmakefile 21 | ├── BUILDING.md 22 | ├── CHANGELOG.md 23 | ├── CODE_OF_CONDUCT.md 24 | ├── COLLABORATOR_GUIDE.md 25 | ├── CONTRIBUTING.md 26 | ├── GOVERNANCE.md 27 | ├── LICENSE 28 | ├── Makefile 29 | ├── README.md 30 | ├── ROADMAP.md 31 | ├── WORKING_GROUPS.md 32 | ├── android-configure 33 | ├── benchmark 34 | ├── common.gypi 35 | ├── config.gypi 36 | ├── config.mk 37 | ├── configure 38 | ├── deps 39 | ├── doc 40 | ├── icu_config.gypi 41 | ├── lib 42 | ├── node.gyp 43 | ├── out 44 | ├── src 45 | ├── test 46 | ├── tools 47 | └── vcbuild.bat 48 | ``` 49 | 进一步查看 `deps`目录: 50 | ```shell 51 | ➜ nodejs git:(master) tree deps -L 1 52 | deps 53 | ├── cares 54 | ├── gtest 55 | ├── http_parser 56 | ├── npm 57 | ├── openssl 58 | ├── uv 59 | ├── v8 60 | └── zlib 61 | ``` 62 | `node.js`核心依赖六个第三方模块。其中核心模块 http_parser, uv, v8这三个模块在后续章节我们会陆续展开。 `gtest`是C/C++单元测试框架。 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /chapter1/chapter1-1.md: -------------------------------------------------------------------------------- 1 | ### 为啥是libuv 2 | 3 | #### 背景 4 | node.js最初开始于2009年,是一个可以让Javascript代码离开浏览器的执行环境也可以执行的项目。 node.js使用了Google的V8解析引擎和Marc Lehmann的libev。Node.js将事件驱动的I/O模型与适合该模型的编程语言(Javascript)融合在了一起。随着node.js的日益流行,node.js需要同时支持windows, 但是libev只能在Unix环境下运行。Windows 平台上与kqueue(FreeBSD)或者(e)poll(Linux)等内核事件通知相应的机制是IOCP。libuv提供了一个跨平台的抽象,由平台决定使用libev或IOCP。在node-v0.9.0版本中,libuv移除了libev的内容。 5 | 6 | 7 | #### 为啥是异步 8 | 9 | 我们先看一张表: 10 | 11 | | 分类 | 操作 | 时间成本 | 12 | | --- | --- | --- | 13 | | 缓存 | L1缓存 | 1纳秒 | 14 | | | L2缓存 | 4纳秒 | 15 | | | 主存储器 | 100 ns | 16 | | | SSD 随机读取 | 16000 ns | 17 | | I/O | 往返在同一数据中心 | 500000 ns | 18 | | | 物理磁盘寻道 | 4,000,000 ns | 19 | 20 | 我们看到即便是 SSD 的访问相较于高速的 CPU,仍然是慢速设备。于是基于事件驱动的 IO 模型就应运而生,解决了高速设备同步等待慢速设备或访问的问题。这不是 libuv 的独创,linux kernel 原生支持的 NIO也是这个思路。 但 libuv 统一了网络访问,文件访问,做到了跨平台。 21 | 22 | 23 | ### libuv 架构 24 | ![](FuX1qcGJgwYtX9zNbBAOSaQeD8Qz.png) 25 | 从左往右分为两部分,一部分是与网络I/O相关的请求,而另外一部分是由文件I/O, DNS Ops以及User code组成的请求。 26 | 27 | 从图中可以看出,对于Network I/O和以File I/O为代表的另一类请求,异步处理的底层支撑机制是完全不一样的。 28 | 29 | 对于Network I/O相关的请求, 根据OS平台不同,分别使用Linux上的epoll,OSX和BSD类OS上的kqueue,SunOS上的event ports以及Windows上的IOCP机制。 30 | 31 | 32 | 而对于File I/O为代表的请求,则使用thread pool。利用thread pool的方式实现异步请求处理,在各类OS上都能获得很好的支持。 33 | 34 | 笔者曾经给 libuv 社区提出过linux 平台下用原生的NIO替换 thread pool 的建议并实现[2],测试发现有3%的提升. 考虑到 NIO 对内核版本的依赖,利用thread pool的方式实现异步请求处理,在各类OS上都能获得很好的支持,相信是 libuv 作者权衡再三的结果。 35 | 36 | 后面详细的模块源码分析时,陆续的会一一剖析。 37 | 38 | ### 参考 39 | 40 | - [1]. http://luohaha.github.io/Chinese-uvbook/ 41 | - [2]. https://github.com/libuv/libuv/issues/461 42 | -------------------------------------------------------------------------------- /chapter1/chapter1-2.md: -------------------------------------------------------------------------------- 1 | ### 从「hello world」讲起 2 | 3 | 先贴一段代码,再熟悉不过,https://nodejs.org/en/about/, 和学习每一种语言一样,从一个简单「hello world」程序对 node.js 有个感性的认识。 4 | ```js 5 | const http = require('http'); 6 | const hostname = '127.0.0.1'; 7 | const port = 1337; 8 | 9 | http.createServer((req, res) => { 10 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 11 | res.end('Hello World\n'); 12 | }).listen(port, hostname, () => { 13 | console.log(`Server running at http://${hostname}:${port}/`); 14 | }); 15 | ``` 16 | 我们从第一句代码看看到底涉及了多少核心模块,让我们开启 node.js 源码分析之旅吧。 17 | 18 | 第一句: `const http = require('http');` 就涉及到2个模块,分别是module 和 http 模块。 19 | 20 | 主体代码 21 | ```js 22 | http.createServer((req, res) => { 23 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 24 | res.end('Hello World\n'); 25 | }).listen(port, hostname, () => { 26 | ... 27 | }); 28 | ``` 29 | 30 | - 首先了解一下HTTP Server的继承关系,有利于更好的理解代码。 31 | 32 | ![http](270064-edbf9b53812f0433.png) 33 | 34 | 这就又涉及了 event和net模块。 35 | 36 | 最后一句: 37 | ```js 38 | console.log(`Server running at http://${hostname}:${port}/`); 39 | ``` 40 | 这里用到了 console模块,但却没有通过 `require` 获取,这就要说到global对象了,Node.js的顶层对象。这里笔者先卖个关子,后面会在 global 章节中详细讲述。 41 | 42 | 43 | 44 | 如果想查看 node 的一些调试日志,可以通过设置 NODE_DEBUG 环境变量,比如: 45 | ```shell 46 | NODE_DEBUG=HTTP,STREAM,MODULE,NET node http.js 47 | ``` 48 | 查看 V8的日志 49 | ```shell 50 | node --trace http.js 51 | ``` 52 | 53 | ### 总结 54 | 一个简单的 hello world 程序却涉及了多个模块,背后却是 Node社区智慧的结晶,在 web服务,异步 IO 上的高度抽象。真所谓大道至简! 55 | 56 | 下面以Linus Torvalds的一句名言来开启Node.js的源码之旅吧。 57 | 58 | > Talk is cheap, show me the code. 59 | 60 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Table of content 2 | 3 | * [Node简介](chapter1/README.md) 4 | * [架构一览](chapter1/chapter1-0.md) 5 | * [为啥是libuv](chapter1/chapter1-1.md) 6 | * [V8 概念](chapter2/chapter2-0.md) 7 | * [C++和JS 交互](chapter2/chapter2-1.md) 8 | * [从「hello world」讲起](chapter1/chapter1-2.md) 9 | * [模块加载](chapter2/chapter2-2.md) 10 | * [Global对象](chapter12/chapter12-1.md) 11 | * [事件循环](chapter5/chapter5-1.md) 12 | * [Timer 解读](chapter3/chapter3-1.md) 13 | * [Yield 魔法](chapter3/chapter3-2.md) 14 | * [Buffer](chapter6/chapter6-1.md) 15 | * [Event](chapter7/chapter7-1.md) 16 | * [Domain](chapter13/chapter13-2.md) 17 | * [Stream 流](chapter8/chapter8-1.md) 18 | * [Net 网络](chapter9/README.md) 19 | * [Socket](chapter9/chapter9-1.md) 20 | * [构建应用](chapter9/chapter9-2.md) 21 | * [加密](chapter9/chapter9-3.md) 22 | * [HTTP](chapter10/README.md) 23 | * [HTTP Server](chapter10/chapter10-1.md) 24 | * [HTTP Client](chapter10/chapter10-2.md) 25 | * [FS 文件系统](chapter11/README.md) 26 | * [文件系统](chapter11/chapter11-2.md) 27 | * [文件抽象](chapter11/chapter11-1.md) 28 | * [IO 那些事儿](chapter11/chapter11-3.md) 29 | * [libuv的选型](chapter11/chapter11-4.md) 30 | * [文件 IO](chapter11/chapter11-5.md) 31 | * [Fs 精粹](chapter11/chapter11-6.md) 32 | * [进程](chapter13/README.md) 33 | * [进程](chapter13/chapter13-1.md) 34 | * [Cluster](chapter4/chapter4-1.md) 35 | * [Node.js 的坑](chapter14/chapter14-5.md) 36 | * [其他](chapter14/README.md) 37 | * [Node.js & Android](chapter14/chapter14-1.md) 38 | * [Node.js & Docker](chapter14/chapter14-2.md) 39 | * [Node.js 调优](chapter14/chapter14-3.md) 40 | * [附录](chapter14/chapter14-0.md) 41 | -------------------------------------------------------------------------------- /chapter11/chapter11-2.md: -------------------------------------------------------------------------------- 1 | ## 文件系统实现原理 2 | 3 | 文件系统存储在磁盘上,一般磁盘会被划分为多个分区,每个分区可以是一个独立的文件系统。 4 | 5 | 磁盘的0号扇区为主引导记录(Master Boot Record,MBR),用来引导计算机。 6 | 7 | 在MBR的结尾是分区表,该分区表标识了每个分区的起始和结束地址。表中的一个分区被标识为活动分区,在计算机被引导时,BIOS读入并执行MBR。MBR首先做的是确定活动分区,读入它的第一个块,称为引导块,并执行。 8 | 9 | 10 | ### 一种常见的文件系统(分区)结构图 11 | 12 | ![](filesystem.png) 13 | 14 | - Super块 15 | - 在系统启动时,会读取Super块,它包含了文件系统的所要重要参数, 通常会做多个备份。 16 | - i-bmap 块 17 | - 用来管理inode,标识inode是否空闲或被使用了。 18 | - d-bmap块 19 | - 用来管理磁盘块,标识磁盘块是空闲还是被使用了。 20 | 21 | ### 文件系统实现 22 | 文件系统一个重要的功能是记录文件使用了哪些磁盘块以及磁盘块的管理,标识磁盘块是被使用,还是空闲。实现思路有下面几种。 23 | 24 | * 连续分配 25 | * 把每个文件存储在相邻的磁盘块上。如磁盘块大小2KB,200KB的文件,需要 100个磁盘块,如果磁盘块大小为4KB,刚需要50个磁盘块。 26 | 27 | 由于每个文件都是从一个新的磁盘块开始的,这样如果一个文件只占了磁盘块大小的一半,那么另一半就被浪费了,没法被别的文件使用,不过连续分配的实现,实现比较简单,只需要记录文件的第一个磁盘块位置和块数,另外读取快速,因为只需要一次寻道,之后不需要寻道和旋转延迟。 28 | 29 | 随着时间推移,磁盘碎片比较严重。因为反复写文件,删除文后,容易在磁盘块上形成空洞。 30 | 31 | * 链表分配 32 | * 为每个文件构造磁盘块链表,每个块在前面指向文件的下一个磁盘块。 33 | 34 | 因为是一个链表,所以顺序读取很快,但随机读取很慢,且每个块中,指向下一个磁盘块的指针是要占用空间的,这样导致每个磁盘块能够存储的数据不再是2的整数次幂。 35 | 36 | 但程序读写文件一般是以2的整数次幂来读写磁盘,这样造成额外的开销,因为读一个块的数据,要读取二个磁盘块。 37 | 38 | * inode 节点方案 39 | * 使用一个特殊的东西来记录每个文件的所使用的磁盘块, 这特殊的东西称为i节点数据结构,其存储了文件 一些属性及文件所使用的到的磁盘块。 40 | 41 | i节点只有在对应的文件被打开时,才会存在内存中,这样即使文件系统文件非常多,只要打开的文件不多,就不会占用太多的内存。 42 | 43 | 文件的元数据和文件数据是分开存储,也就是一个文件有i节点和数据文件这两个属性。 44 | 45 | 每个存储i节点的磁盘块空间是有限,且一个文件只有一个i节点,那么当一个文件比较大时,怎么解决?大家想没有想起C语言中的指针及指针的指针。 46 | 47 | 一种解决办法是预留部分数据块,用来存储指向磁盘块的指针,而不是直接直接指向磁盘块。 48 | 49 | 50 | ### Linux文件系统的实现 51 | Unix/Linux文件系统的实现是采用i节点的方案,文件系统ext2、ext3都是如此,ext4相比前面二种文件 ,做了不少优化,本书便不在此展开。 52 | 53 | 54 | ### 文件系统与操作系统的协作 55 | 56 | 文件系统是操作系统的一部分,操作系统是相对稳定,但文件系统却是有好多,如ext2、ext3 、ext4 、ZFS 等,那么操作系统是如何兼容这些不同的文件系统,以对外提供统一服务。 57 | 58 | C++、JAVA程序员很容易想到利用多态来实现,对,操作系统也是采用类似的思路。对不同的文件系统,抽象出所有文件系统都支持的、基本的、概念上的数据结构和接口 ,如前文描述的关于文件和目录的基本操作。 59 | 60 | 这些统一抽象组件,叫做虚拟文件系统(Virtual File System ,VFS),VFS作为系统内核组件,为用户空间程序提供了文件和文件系统相关的接口。 61 | 62 | VFS使用得用户可以直接调用write、read 这样的文件系统调用,而不用考虑底层是什么文件系统。 63 | 64 | 横向地看下它们的关系,如下图所示 65 | ![](read-write.png) 66 | 67 | ### 总结 68 | 69 | 文件系统是对磁盘设备的抽象,屏蔽了具体的存储类型,比如磁盘,内存。 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /chapter11/chapter11-3.md: -------------------------------------------------------------------------------- 1 | ## IO 那些事儿 2 | Linux 异步 I/O 是 Linux 内核中提供的一个相当新的增强。它是 2.6 版本内核的一个标准特性,AIO 背后的基本思想是允许进程发起很多 I/O 操作,而不用阻塞或等待任何操作完成。稍后或在接收到 I/O 操作完成的通知时,进程就可以检索 I/O 操作的结果。 3 | 4 | ### I/O 模型 5 | 在深入介绍 AIO API 之前,让我们先来探索一下 Linux 上可以使用的不同 I/O 模型。这并不是一个详尽的介绍,但是我们将试图介绍最常用的一些模型来解释它们与异步 I/O 之间的区别。图 1 给出了同步和异步模型,以及阻塞和非阻塞的模型。 6 | 7 | ![](figure1.gif) 8 | 9 | 每个 I/O 模型都有自己的使用模式,它们对于特定的应用程序都有自己的优点。本节将简要对其一一进行介绍。 10 | 11 | ### 同步阻塞 I/O 12 | 13 | 最常用的一个模型是同步阻塞 I/O 模型。在这个模型中,用户空间的应用程序执行一个系统调用,这会导致应用程序阻塞。这意味着应用程序会一直阻塞,直到系统调用完成为止(数据传输完成或发生错误)。调用应用程序处于一种不再消费 CPU 而只是简单等待响应的状态,因此从处理的角度来看,这是非常有效的。 14 | 图 2 给出了传统的阻塞 I/O 模型,这也是目前应用程序中最为常用的一种模型。在调用 read 系统调用时,应用程序会阻塞并对内核进行上下文切换。然后会触发读操作,当响应返回时(从我们正在从中读取的设备中返回),数据就被拷贝到用户空间的缓冲区中。然后应用程序就会解除阻塞(read 调用返回)。 15 | ![](figure2.gif) 16 | 17 | 从应用程序的角度来说,read 调用会延续很长时间。实际上,在内核执行读操作和其他工作时,应用程序的确会被阻塞。 18 | 19 | ### 同步非阻塞 I/O 20 | 21 | 同步阻塞 I/O 的一种效率稍低的变种是同步非阻塞 I/O。在这种模型中,设备是以非阻塞的形式打开的。这意味着 I/O 操作不会立即完成,read 操作可能会返回一个错误代码,说明这个命令不能立即满足(EAGAIN 或 EWOULDBLOCK),如图 3 所示。 22 | 23 | ![](figure3.gif) 24 | 25 | 非阻塞的实现是 I/O 命令可能并不会立即满足,需要应用程序调用许多次来等待操作完成。这可能效率不高,因为在很多情况下,当内核执行这个命令时,应用程序必须要进行忙碌等待,直到数据可用为止,或者试图执行其他工作。正如图 3 所示的一样,这个方法可以引入 I/O 操作的延时,因为数据在内核中变为可用到用户调用 read 返回数据之间存在一定的间隔,这会导致整体数据吞吐量的降低。 26 | 27 | 28 | 29 | ### 异步阻塞 I/O 30 | 另外一个阻塞解决方案是带有阻塞通知的非阻塞 I/O。在这种模型中,配置的是非阻塞 I/O,然后使用阻塞 select 系统调用来确定一个 I/O 描述符何时有操作。使 select 调用非常有趣的是它可以用来为多个描述符提供通知,而不仅仅为一个描述符提供通知。对于每个提示符来说,我们可以请求这个描述符可以写数据、有读数据可用以及是否发生错误的通知。 31 | 32 | ![](figure4.gif) 33 | 34 | select 函数所提供的功能(异步阻塞 I/O)与 AIO 类似。不过,它是对通知事件进行阻塞,而不是对 I/O 调用进行阻塞。 35 | 36 | ### 异步非阻塞 I/O 37 | 38 | 异步非阻塞 I/O 模型是一种处理与 I/O 重叠进行的模型。读请求会立即返回,说明 read 请求已经成功发起了。在后台完成读操作时,应用程序然后会执行其他处理操作。当 read 的响应到达时,就会产生一个信号或执行一个基于线程的回调函数来完成这次 I/O 处理过程。 39 | 40 | ![](figure5.gif) 41 | 42 | 在一个进程中为了执行多个 I/O 请求而对计算操作和 I/O 处理进行重叠处理的能力利用了处理速度与 I/O 速度之间的差异。当一个或多个 I/O 请求挂起时,CPU 可以执行其他任务;或者更为常见的是,在发起其他 I/O 的同时对已经完成的 I/O 进行操作。 43 | 44 | 45 | ### 总结 46 | 慢速的 IO 设备和高速的 CPU 如何协作是门学问。 47 | 但这里并不是说同步阻塞IO 一定不好,还是要根据场景灵活选择。 48 | 49 | 依照作者的经验, 同步 IO 适用于时间可控,少量调用的场景。 50 | 异步 IO 适用于时间不可控(如网络异常),大量调用的场景。 51 | 52 | 53 | ### 参考 54 | 55 | - https://www.ibm.com/developerworks/cn/linux/l-async/ 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 《深入理解Node.js:核心思想与源码分析》 2 | 3 | Node.js 的源码分析,基于node v6.0.0。 4 | 5 | 源码分析包括(libuv, v8), 需要有一定的 C、C++基础。 Node.js 的源码到处闪烁着开发者的智慧和追求极致的精神。 6 | 包括但不限于: 7 | 8 | - 系统架构 9 | 10 | - 设计模式 11 | 12 | - 性能优化 13 | 14 | - 奇技淫巧 15 | 16 | 本书通过分析 node 核心模块的实现,向读者阐述 node 异步 IO,事件循环的核心思想。帮助开发者更好的使用 Node.js。 17 | 18 | 通过追溯 node 社区开发issue, 探讨 node 的变迁和演进,学习 node.js 的设计哲学。 19 | 20 | # Table of content 21 | 22 | * [惊鸿一瞥](chapter1/README.md) 23 | * [架构一览](chapter1/chapter1-0.md) 24 | * [为啥是libuv](chapter1/chapter1-1.md) 25 | * [V8 概念](chapter2/chapter2-0.md) 26 | * [C++和JS 交互](chapter2/chapter2-1.md) 27 | * [从「hello world」讲起](chapter1/chapter1-2.md) 28 | * [模块加载](chapter2/chapter2-2.md) 29 | * [Global对象](chapter12/chapter12-1.md) 30 | * [事件循环](chapter5/chapter5-1.md) 31 | * [Timer 解读](chapter3/chapter3-1.md) 32 | * [Yield 魔法](chapter3/chapter3-2.md) 33 | * [Buffer](chapter6/chapter6-1.md) 34 | * [Event](chapter7/chapter7-1.md) 35 | * [Domain](chapter13/chapter13-2.md) 36 | * [Stream 流](chapter8/chapter8-1.md) 37 | * [Net 网络](chapter9/README.md) 38 | * [Socket](chapter9/chapter9-1.md) 39 | * [构建应用](chapter9/chapter9-2.md) 40 | * [加密](chapter9/chapter9-3.md) 41 | * [HTTP](chapter10/README.md) 42 | * [HTTP Server](chapter10/chapter10-1.md) 43 | * [HTTP Client](chapter10/chapter10-2.md) 44 | * [FS 文件系统](chapter11/README.md) 45 | * [文件系统](chapter11/chapter11-2.md) 46 | * [文件抽象](chapter11/chapter11-1.md) 47 | * [IO 那些事儿](chapter11/chapter11-3.md) 48 | * [libuv的选型](chapter11/chapter11-4.md) 49 | * [文件 IO](chapter11/chapter11-5.md) 50 | * [Fs 精粹](chapter11/chapter11-6.md) 51 | * [进程](chapter13/README.md) 52 | * [子进程](chapter13/chapter13-1.md) 53 | * [Cluster](chapter4/chapter4-1.md) 54 | * [Node.js 的坑](chapter14/chapter14-5.md) 55 | * [其他](chapter14/README.md) 56 | * [Node.js & Android](chapter14/chapter14-1.md) 57 | * [Node.js & Docker](chapter14/chapter14-2.md) 58 | * [Node.js 调优](chapter14/chapter14-3.md) 59 | * [附录](chapter14/chapter14-0.md) 60 | 61 | 62 | 本书版权归作者所有,未经作者授权,禁止一切方式转载。 63 | 64 | - 联系作者 @江凌 微博:http://weibo.com/yangjianghua 65 | 66 | - 邮箱:yjhjstz#gmail.com, 博客:http://alinode.aliyun.com 67 | 68 | 本书尚在撰写中,欢迎读者讨论https://github.com/yjhjstz/deep-into-node/issues 69 | 70 | **如果您觉得还不错, 请我喝杯咖啡,欢迎Star, 提交PR** 71 | 72 | ![zhi](alipay.jpg) 73 | 74 | -------------------------------------------------------------------------------- /chapter14/chapter14-3.md: -------------------------------------------------------------------------------- 1 | ### 楔子 2 | `node-profiler` 作为 alinode 团队的另一款产品能够帮助您线下深入分析 javascript 代码的性能,将Google V8的性能细节展现在您的面前,优化而知其所以然。线上调优请使用 `alinode`。 3 | 4 | ### 下载安装 5 | 推荐安装工具`tnvm`,支持 node, alinode, profiler 的安装切换。 6 | ``` 7 | wget -O- https://raw.githubusercontent.com/aliyun-node/tnvm/master/install.sh | bash 8 | ``` 9 | 完成安装后,您需要将tnvm添加为命令行程序。根据平台的不同,可能是~/.bashrc,~/.profile或~/.zshrc. 10 | ```shell 11 | tnvm install profiler-v0.12.6 12 | tnvm use profiler-v0.12.6 13 | ``` 14 | 15 | 16 | ### 使用示例 17 | ```js 18 | var http = require('http'); 19 | http.createServer(function (req, res) { 20 | res.writeHead(200); 21 | res.end('hello world!'); 22 | }).listen(1334); 23 | ``` 24 | 25 | ```shell 26 | $ node-profiler server.js 27 | start agent 28 | webkit-devtools-agent: A proxy got connected. 29 | webkit-devtools-agent: Waiting for commands... 30 | webkit-devtools-agent: Websockets service started on 0.0.0.0:9999 <==启动成功 31 | ``` 32 | 如出现如下: 33 | ```shell 34 | Error: listen EADDRINUSE <== 可能是由于端口被占用 35 | ``` 36 | 37 | 成功启动后,则用chrome(推荐)手动打开url (http://alinode.aliyun.com/profiler/inspector.html?host=localhost:9999&page=0) 38 | 出现如下界面: 39 | ![](https://cloud.githubusercontent.com/assets/3832082/8587127/7b54f88c-262a-11e5-9298-3a49c2b71d7c.jpg) 40 | 41 | 默认**Collect JavaSript CPU Profile**,单击**Start**。 42 | 43 | 可以采用压测脚本实现对服务进行压力测试,保证更多的结果: 44 | ```shell 45 | $ wrk http://localhost:1334/ # 这里使用wrk,也可以使用其他工具,如ab 46 | ``` 47 | 点击**Stop**,得到如下图的结果: 48 | ![](https://cloud.githubusercontent.com/assets/3832082/8587247/8dc33cbc-262b-11e5-8a10-59c8f9de280e.jpg) 49 | 50 | 可以看到更多关于函数在运行时的信息。 51 | 52 | ### UI含义 53 | UI 栏目 | 示意 54 | ---- | ---- 55 | Self | exclusive time 56 | Total | inclusive time 57 | # Hidden Classes | 隐藏类个数 58 | Bailout | v8中提取的最后一次去优化原因 59 | Function | 函数名称 script : line 60 | 61 | **红色表示函数未被优化, 淡绿色表示函数被V8优化过。** 62 | 63 | ### 原理介绍 64 | - 基于V8内置采样收集器; 65 | - 固定采样频率,默认1ms, 可配置; 66 | - 会暂停主线程,采样函数call stack,统计时间; 67 | - 需保证采样足够长的时间(预热)。 68 | 69 | #### 解释下两个概念 70 | - exclusive time :独占时间 71 | - inclusive time :包含时间 72 | 73 | ```js 74 | function foo() { 75 | bar(); 76 | } 77 | function bar() { 78 | <==采样点 79 | } 80 | foo(); 81 | ``` 82 | 83 | 这个例子,采样点在bar(),那么bar所消耗的时间叫作 **exclusive time**,而foo调用了bar, foo所消耗的时间包括了bar的时间,叫作 **inclusive time** . 84 | 85 | 86 | ### 注意事项 87 | * 该工具目前只支持 X64 平台(Linux, Mac)。 88 | * 切勿部署到线上,如需线上调优请使用 [alinode](alinode.aliyun.com)。 -------------------------------------------------------------------------------- /chapter14/chapter14-1.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Node.js在物联网 4 | 5 | ### 1、Build Node.js for Android 6 | #### Linux构建环境 7 | ```shell 8 | jiangling@young:~/node/deps/npm$ uname -a 9 | Linux young 3.11.0-15-generic #25~precise1-Ubuntu SMP Thu Jan 30 17:39:31 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux 10 | ``` 11 | #### 32位 Android NDK 12 | ```shell 13 | drwxr-xr-x 10 jiangling jiangling 4096 3月 1 2014 android-ndk-r9d 14 | ``` 15 | 16 | #### clone node.js 17 | ```shell 18 | git clone https://github.com/joyent/node.git 19 | ``` 20 | 21 | #### android-configure patch 22 | ```shell 23 | jiangling@young:~/node$ git diff 24 | diff --git a/android-configure b/android-configure 25 | index 7acb7f3..aae0bf1 100755 26 | --- a/android-configure 27 | +++ b/android-configure 28 | @@ -3,7 +3,7 @@ 29 | export TOOLCHAIN=$PWD/android-toolchain 30 | mkdir -p $TOOLCHAIN 31 | $1/build/tools/make-standalone-toolchain.sh \ 32 | - --toolchain=arm-linux-androideabi-4.7 \ 33 | + --toolchain=arm-linux-androideabi-4.8 \ 34 | --arch=arm \ 35 | --install-dir=$TOOLCHAIN \ 36 | --platform=android-9 37 | ``` 38 | 否则会出现`arm-linux-androideabi-gcc not found`的错误。 39 | #### configure && make 40 | ```shell 41 | source ./android-configure ~/android-ndk-r9b 42 | mv python2.7 oldpython2.7 43 | ln -s /usr/bin/python2.7 python2.7 44 | cd ~/node 45 | make 46 | ``` 47 | node bin大小 48 | ```shell 49 | jiangling@young:~/node$ file out/Release/node 50 | out/Release/node: ELF 32-bit LSB executable, ARM, version 1 (SYSV), dynamically linked (uses shared libs), not stripped 51 | 52 | root@android:/data/local/tmp # ls -al node 53 | ls -al node 54 | -rwx------ shell shell 12158228 2014-11-24 17:01 node 55 | ``` 56 | 配置without-ssl后大小 57 | ``` 58 | jiangling@young:~/node$ ls -al out/Release/node 59 | -rwxrwxr-x 1 jiangling jiangling 9804644 11月 26 13:55 out/Release/node 60 | ``` 61 | 62 | ### 2、Run Node.js on Android 63 | 这边我选择了ROOT过的小米M1手机, 安装了”瑞士军刀“[busybox](http://www.busybox.net/downloads/binaries/latest/ 64 | ), 以便查看系统信息。 65 | ```shell 66 | root@android:/data/local/tmp # ./busybox uname -a 67 | ./busybox uname -a 68 | Linux localhost 3.4.0-perf-g1ccebb5-00146-gd6845ec #1 SMP PREEMPT Mon Nov 4 20:10:00 CST 2013 armv7l GNU/Linux 69 | ``` 70 | 71 | 用ADB将 `node`, `test.js`(经典的hello world) push到M1上: 72 | ```shell 73 | adb push d:\node /data/local/tmp 74 | adb push d:\test.js /data/local/tmp 75 | adb shell chmod 755 /data/local/tmp/node 76 | ``` 77 | 78 | 运行 和 结果 79 | ![360_1126_10_38_01](http://img1.tbcdn.cn/L1/461/1/c60b8459e72b1c8f2356f6482d09cec2194d6abe) 80 | ![360_1125_16_29_01](http://img3.tbcdn.cn/L1/461/1/219c69b83c49b3ff789190c3378ac017c148e240) 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /chapter11/chapter11-1.md: -------------------------------------------------------------------------------- 1 | 2 | ## 文件抽象 3 | 4 | fs模块是文件操作的封装,它提供了文件的读取、写入、更名、删除、遍历目录、链接POSIX文件系统操作。与其他模块不同的是,fs模块中的所有操作都提供了异步和同步两个版本,例如读取文件内容函数的异步方法:readFile(),同步方法readFileSync()。 5 | 6 | 7 | ### 一切皆文件 8 | “一切皆是文件”是 Unix/Linux 的基本哲学之一。不仅普通的文件,目录、字符设备、块设备、 套接字等在 Unix/Linux 中都是以文件被对待;它们虽然类型不同,但是对其提供的却是同一套操作接口。 9 | 10 | ![](unix-file.jpg) 11 | 12 | 文件是一种抽象机制,它对磁盘进行了抽象。 13 | 14 | 文件就是字节序列,每个I/O设备,包括磁盘、键盘、显示器、甚至网络,都可以抽象成文件,在Unix/Linux系统中,系统中所有的输入输出都是通过调用IO系统调用来完成。 15 | 16 | 文件是对IO的抽象,就像虚拟存储器是对程序存储的抽象,进程是对一个正在运行程序的抽象。这些都是操作系统重要的抽象。 17 | 18 | 抽象机制最重要的特性是对管理对象的命名,所以文件有文件名,且文件名要符合一定的规范。 19 | 20 | 21 | ### 文件主要操作 22 | - open 23 | - read 24 | - write 25 | - close 26 | 27 | 上面的操作比较简单,就不是细说,后面会写文章再介绍读文件、写文件、刷新数据这几个重要的操作。如果有兴趣,可以通过man 2 read 命令来查看帮助文档。 28 | 29 | 30 | ### 文件类型 31 | 可以通过ls -l查看文件类型,主要有下面几种常见的。 32 | - 普通文件 33 | - 包括文本文件和二进制文件 34 | - 目录 35 | - 和普通文件相比,目录也存储在介质上,但是目录不存储常规文件,它只是用来组织、管理文件。 36 | - proc文件 37 | - proc不存储,所以不占用任何空间,proc使得内核可以生成与系统状态和配置相关的信息,该信息可以由用户和系统内核从普通文件读取,无需专门的工具。 38 | 其它更多的文件类型,可以通过man ls 查看。 39 | 40 | 41 | ### 文件属性 42 | 文件属性包括文件权限信息、创建时间、最后修改时间、最后读取时间、文件大小、文件引用数等信息,这些文件属性也称为文件元数据。 43 | 44 | 45 | ### 文件系统之高级读写 46 | #### 文件映射 mmap 47 | 48 | `man 2 mmap` 查看: 49 | 50 | ```c++ 51 | 52 | #include 53 | 54 | void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset); 55 | 56 | ``` 57 | 通过mmap系统调用,把一个文件映射到进程虚拟地址空间上。也就是说磁盘上的文件,现在在系统看来是一个内存数组了,这样应用程序访问文件就不需要系统IO调用,而是直接读取内存。 58 | 59 | #### 优点: 60 | * 1、从内存映像文件中读写,避免了read、write多余的拷贝。 61 | * 2、从内存映像文件中读写,避免了多余的系统调用和用户-内核模式的切换 62 | * 3、可以多个进程共享内存映像文件。 63 | 64 | #### 缺点: 65 | * 1、内存映像需要整数倍页大小,如果文件较小,会浪费内存。 66 | * 2、内存映像需要进程地址空间,大的内存映像可能导致地址空间碎片,找不到足够大的空余连续区域供其它用。 67 | 68 | 69 | #### 离散 I/O 70 | readv和writev函数让我们在单个函数调用里从多个不连续的缓冲里读入或写出。这些操作被称为分散读(scatter read)和集合写(gather write)。 71 | 72 | ```c++ 73 | 74 | #include 75 | 76 | ssize_t readv(int filedes, const struct iovec *iov, int iovcnt); 77 | 78 | ssize_t writev(int filedes, const struct iovec *iov, int iovcnt); 79 | 80 | 两者都返回读或写的字节数,错误返回-1。 81 | ``` 82 | 这两个函数的第二个参数是指向iovec结构数组的一个指针: 83 | ```c++ 84 | struct iovec { 85 | void *iov_base; /* starting address of buffer */ 86 | size_t iov_len; /* size of buffer */ 87 | }; 88 | ``` 89 | iov数组中的元素数由iovcnt说明。其最大值受限于IOV_MAX。 90 | ![](14fig27.gif) 91 | 92 | writev以顺序iov[0],iov[1]至iov[iovcnt-1]从缓冲区中聚集输出数据。writev返回输出的字节总数,通常,它应等于所有缓冲区长度之和。 93 | 94 | readv则将读入的数据按上述同样顺序散布到缓冲区中。readv总是先填满一个缓冲区,然后再填写下一个。readv返回读到的总字节数。如果遇到文件结尾,已无数据可读,则返回0。 95 | 96 | 97 | ### 总结 98 | * 零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之间不必要的中间拷贝次数,从而有效地提高数据传输效率。而且,零拷贝技术减少了用户应用程序地址空间和操作系统内核地址空间之间因为上下文切换而带来的开销。 99 | 100 | 是用户程序尝试优化的重要可选的优化手段。 101 | 102 | * 向量 I/O 操作可以取代多个线性 I/O 操作, 性能更好 103 | 104 | - 除了减少了发起的系统调用次数,通过内部优化,向量 I/O 可以比线性 I/O 提供更好的性能。 105 | - 支持原子性, 一个进程可以执行单个向量 I/O 操作,避免了和其他进程交叉操作的风险。 106 | 107 | ### 参考 108 | http://www.ibm.com/developerworks/cn/linux/l-cn-zerocopy1/ 109 | -------------------------------------------------------------------------------- /chapter9/chapter9-3.md: -------------------------------------------------------------------------------- 1 | ## Crypto 2 | 3 | ### 什么是 SSL ? 4 | Secure Sockets Layer,这是其全名,他的作用是协议,定义了用来对网络发出的数据进行加密的格式和规则。 5 | 6 | ```js 7 | +------+ +------+ 8 | 服务器 | data | -- SSL 加密 --> 发送 --> 接收 -- SSL 解密 -- | data | 客户端 9 | +------+ +------+ 10 | 11 | +------+ +------+ 12 | 服务器 | data | -- SSL 解密 --> 接收 <-- 发送 -- SSL 加密 -- | data | 客户端 13 | +------+ +------+ 14 | ``` 15 | > 注:TLS 1.0 等同于 SSL 3.1,TLS 1.1 等同于 SSL 3.2,TLS 1.2 等同于 SSL 3.3。 16 | 17 | 18 | 19 | ### OpenSSL 20 | OpenSSL 是在程序上对 SSL 标准的一个实现,提供了: 21 | 22 | * libcrypto 通用加密库 23 | * libssl TLS/SSL 的实现 24 | * openssl 命令行工具 25 | 26 | Node.js 是完全采用 OpenSSL 进行加密的,其相关的 TLS HTTPS 服务器模块和 Crypto 加密模块都是通过 C++ 在底层调用 OpenSSL 。 27 | 28 | OpenSSL 实现了对称加密: 29 | ```shell 30 | AES(128) | DES(64) | Blowfish(64) | CAST(64) | IDEA(64) | RC2(64) | RC5(64) 31 | ``` 32 | 非对称加密: 33 | ```shell 34 | DH | RSA | DSA | EC 35 | ``` 36 | 以及一些信息摘要: 37 | ```shell 38 | MD2 | MD5 | MDC2 | SHA | RIPEMD | DSS 39 | ``` 40 | 其中信息摘要是一些采用哈希算法的加密方式,也意味着这种加密是单向的,不能反向解密。这种方式的加密大多是用于保护安全口令,比如登录密码。这里面我们最长用的是 MD5 和 SHA (建议采用更稳定的 SHA1, MD5通过查表大法已经不再单向)。 41 | 42 | 使用非对称加密会损耗性能,对称加密又不能在网络传输,那该怎么办呢? 43 | 答案是:结合两者一起使用。`ssl/tls`实际上也是如此,`tls`将两者完美组合使用。 44 | 45 | 46 | ### TLS HTTPS 服务器 47 | 想要建立一个 Node.js TLS 服务器,需要使用 tls 模块: 48 | 49 | `var Tls = require('tls');` 50 | 51 | 在开始搭建服务器之前,我们还有些重要的工作要做,那就是证书,签名证书。 52 | 53 | 基于 SSL 加密的服务器,在与客户端开始建立连接时,会发送一个签名证书。客户端在自己的内部存储了一些公认的权威证书认证机构,即 CA。客户端通过在自己的 CA 表中查找,来匹配服务器发送的证书上的签名机构,以此来判断面对的服务器是不是一个可信的服务器。 54 | 55 | 如果这个服务器发送的证书,上面的签名机构不在客户端的 CA 列表中,那么这个服务器很有可能是伪造的,你应该听说过“中间人攻击”。 56 | 57 | ```js 58 | +------------+ 59 | | 真正的服务器 | 选择权:连接?不连接? +-------+ 60 | +------------+ +------------------ | 客户端 | 61 | https | +-------+ 62 | +--------------+ | 拦截通信包 63 | | 破坏者的服务器 | ----------+ 64 | +--------------+ 证书是伪造的 65 | ``` 66 | 67 | 68 | 69 | ### 总结 70 | 71 | 加密技术的安全性和性能主要取决于以下几个关键因素: 72 | 73 | 1. **加密算法的选择** 74 | 不同的加密算法在安全性和性能上存在差异。对称加密算法(例如 AES-256)在性能上通常优于非对称加密,但它们需要安全的密钥交换方式。非对称加密算法(如 RSA、DSA 或 EC)虽然在密钥管理和交换上更为便捷,但由于计算量较大,通常只用于密钥交换或数字签名。在实际应用中,例如 TLS/SSL 协议,就巧妙地结合了两者的优势:利用非对称加密进行安全的密钥交换,再使用对称加密进行数据传输,从而既保证了安全性,也提高了性能。 75 | 76 | 2. **密钥长度与管理** 77 | 密钥(key)的长度直接影响加密强度。在相同的算法下,密钥长度每增加一位,暴力破解的难度呈指数级增长。当前,AES-256 被认为足够安全,而对于非对称加密来说,RSA 的 1024 位密钥已不再安全,推荐至少使用 2048 位密钥。此外,合理的密钥管理策略也是保障整个加密体系安全的重要环节。 78 | 79 | 3. **证书与身份验证** 80 | 在 TLS/SSL 协议中,服务器在建立连接时会向客户端发送签名证书,客户端则通过内置的权威 CA 列表对证书的签名机构进行验证。只有通过验证的证书才被视为可信,否则可能存在中间人攻击的风险。这一机制在确保通信双方身份真实可靠方面发挥着至关重要的作用。 81 | 82 | 4. **最新标准与实践** 83 | 随着技术的发展,安全标准也在不断更新。例如,TLS 1.2 和 TLS 1.3 相较于早期的 SSL/TLS 版本在安全性和性能上都有显著提升。采用最新的安全协议和算法,并定期更新和审查系统配置,可以有效防止潜在的安全漏洞和攻击。 84 | 85 | 综合来看,安全加密不仅依赖于算法本身的强度和密钥长度,还需要在实际应用中合理地结合对称与非对称加密技术,以及配合严格的身份验证与证书管理策略。只有这样,才能在确保数据传输安全的同时,兼顾加密性能,构建一个既高效又安全的通信系统。 86 | 87 | ### 参考 88 | * http://www.jianshu.com/p/a8b87e436ac7 89 | -------------------------------------------------------------------------------- /chapter12/chapter12-1.md: -------------------------------------------------------------------------------- 1 | 2 | ## Global对象 3 | 4 | 所有属性都可以在程序的任何地方被访问,即全局变量。在javascript中,通常window是全局对象,而node.js的全局对象是global,所有全局变量都是global对象的属性,如:console、process等。 5 | 6 | 7 | ### 全局对象与全局变量 8 | global最根本的作用是作为全局变量的宿主。满足以下条件成为全局变量。 9 | 10 | - 在最外层定义的变量 11 | - 全局对象的属性 12 | - 隐式定义的变量(未定义直接赋值的变量) 13 | 14 | node.js中不可能在最外层定义变量,因为所有的用户代码都是属于当前模块的,而模块本身不是最外层上下文。node.js中也不提倡自定义全局变量。 15 | 16 | **Node提供以下几个全局对象,它们是所有模块都可以调用的。** 17 | - global:表示Node所在的全局环境,类似于浏览器的window对象。需要注意的是,如果在浏览器中声明一个全局变量,实际上是声明了一个全局对象的属性,比如var x = 1等同于设置window.x = 1,但是Node不是这样,至少在模块中不是这样(REPL环境的行为与浏览器一致)。在模块文件中,声明var x = 1,该变量不是global对象的属性,global.x等于undefined。这是因为模块的全局变量都是该模块私有的,其他模块无法取到。 18 | 19 | - process:该对象表示Node所处的当前进程,允许开发者与该进程互动。 20 | 21 | - console:指向Node内置的console模块,提供命令行环境中的标准输入、标准输出功能。 22 | 23 | **Node还提供一些全局函数。** 24 | - setTimeout():用于在指定毫秒之后,运行回调函数。实际的调用间隔,还取决于系统因素。间隔的毫秒数在1毫秒到2,147,483,647毫秒(约24.8天)之间。如果超过这个范围,会被自动改为1毫秒。该方法返回一个整数,代表这个新建定时器的编号。 25 | - clearTimeout():用于终止一个setTimeout方法新建的定时器。 26 | - setInterval():用于每隔一定毫秒调用回调函数。由于系统因素,可能无法保证每次调用之间正好间隔指定的毫秒数,但只会多于这个间隔,而不会少于它。指定的毫秒数必须是1到2,147,483,647(大约24.8天)之间的整数,如果超过这个范围,会被自动改为1毫秒。该方法返回一个整数,代表这个新建定时器的编号。 27 | - clearInterval():终止一个用setInterval方法新建的定时器。 28 | - require():用于加载模块。 29 | - Buffer():用于操作二进制数据。 30 | 31 | **伪全局变量。** 32 | * _filename:指向当前运行的脚本文件名。 33 | * _dirname:指向当前运行的脚本所在的目录。 34 | 除此之外,还有一些对象实际上是模块内部的局部变量,指向的对象根据模块不同而不同,但是所有模块都适用,可以看作是伪全局变量,主要为module, module.exports, exports等。 35 | 36 | ### module.exports vs exports 37 | 38 | 如果想不借助global,在不同模块之间共享代码,就需要用到exports属性。令人有些迷惑的是,在node.js里,还有另外一个属性,是module.exports。一般情况下,这2个属性的作用是一致的,但是如果对exports或者module.exports赋值的话,又会呈现出令人奇怪的结果。 39 | 40 | 41 | 首先,exports和module.exports都是某个对象的引用(reference),初始情况下,它们指向同一个object,如果不修改module.exports的引用的话,这个object稍后会被导出。 42 | ```shell 43 | exports module.exports 44 | | / 45 | | / 46 | V V 47 | Object 48 | ``` 49 | 50 | 所以如果只是给对象添加属性,不改变exports和module.exports的引用目标的话,是完全没有问题的。 51 | 52 | 但是有时候,希望导出的是一个构造函数,那么一般会这么写: 53 | ```js 54 | // b.js 55 | module.exports = function (name, age) { 56 | this.name = name; 57 | this.age = age; 58 | } 59 | 60 | exports.sex = "male"; 61 | ``` 62 | ```js 63 | var Person = require("./b"); 64 | var person = new Person("Tony", 33); 65 | console.log(person); // {name:"Tony", age:33} 66 | console.log(Person.sex); // undefined 67 | ``` 68 | 这个sex属性不会导出,因为引用关系已经改变: 69 | ```shell 70 | exports module.exports 71 | | | 72 | | | 73 | V V 74 | Object function 75 | ``` 76 | 77 | 而node.js导出的,永远是module.exports指向的对象,在这里就是function。所以exports指向的那个object,现在已经不会被导出了,为其增加的属性当然也就没用了。 78 | 79 | 80 | 如果希望把sex属性也导出,就需要这样写: 81 | ```js 82 | exports = module.exports = function (name, age) { 83 | this.name = name; 84 | this.age = age; 85 | } 86 | 87 | exports.sex = "male"; 88 | ``` 89 | 90 | 91 | 92 | 93 | ### 总结 94 | 95 | * node.js 设计的2个导出引用的对象,反而增加了迷惑性。 96 | * 避免污染全局空间。 97 | 98 | 99 | ### 参考 100 | 101 | *https://nodejs.org/api/globals.html 102 | *https://nodejs.org/api/modules.html 103 | *https://medium.com/geekculture/understanding-the-difference-between-module-exports-and-exports-in-node-js-264fc500a409 104 | 105 | -------------------------------------------------------------------------------- /chapter11/chapter11-4.md: -------------------------------------------------------------------------------- 1 | ## libuv 选型 2 | 3 | ### linux native aio 4 | 5 | Linux native aio 有两种API,一种是libaio提供的API,一种是利用系统调用封装成的API,后者使用的较多,因为不需要额外的库且简单。 6 | 7 | - io_setup : 是用来设置一个异步请求的上下文,第一个参数是请求事件的个数,第二个参数唯一标识一个异步请求。 8 | - io_commit: 是用来提交一个异步io请求的,在提交之前,需要设置一下结构体`iocb`。 9 | - io_getevents: 用来获取完成的io事件,参数`min_nr`是事件个数的最小值,`nr`是事件个数的最大值,如果没有足够的事件发生,该函数会阻塞。 10 | - io_destroy:在所有事件处理完之后,调用此函数销毁异步io请求。 11 | 12 | #### 限制 13 | aio只能使用于常规的文件IO,不能使用于socket,管道等IO,但对于 libuv 的 fs 模块使用需求已经足够了。 14 | 15 | io_getevents在调用之后会阻塞直到有足够的事件发生,因此要实现真正的异步IO,需要借助eventfd和epoll达到目的。 16 | 17 | #### libuv native aio 实现 18 | 笔者实现过一个基于 libuv 的 native aio,https://github.com/yjhjstz/libuv/commit/2748728635c4f74d6f27524fd36e680a88e4f04a 19 | 20 | 从理论上看,在libuv中实现AIO, 21 | * 其一:比原来的libuv实现少了一次write系统调用,无需在用户态实现线程池和工作队列. 22 | * 其二:native aio实现可以实现批量回调。 23 | 24 | 我们看下性能对比数据, 测试脚本是简单的文件读取: 25 | * Threadpool 模型 26 | ```shell 27 | jiangling@young:~/workspace/libuv$ wrk -t4 -c100 -d30s http://127.0.0.1:30003/ 28 | Running 30s test @ http://127.0.0.1:30003/ 29 | 4 threads and 100 connections 30 | Thread Stats Avg Stdev Max +/- Stdev 31 | Latency 16.77ms 1.14ms 31.68ms 86.68% 32 | Req/Sec 1.51k 162.66 2.08k 81.34% 33 | 178925 requests in 30.00s, 104.26MB read 34 | Requests/sec: 5963.45 35 | Transfer/sec: 3.47MB 36 | ``` 37 | 38 | * Native AIO 模型 39 | ```shell 40 | jiangling@young:~/workspace/libuv$ wrk -t4 -c100 -d30s http://127.0.0.1:30003/ 41 | Running 30s test @ http://127.0.0.1:30003/ 42 | 4 threads and 100 connections 43 | Thread Stats Avg Stdev Max +/- Stdev 44 | Latency 16.22ms 0.95ms 26.39ms 88.12% 45 | Req/Sec 1.57k 191.14 2.08k 68.50% 46 | 185084 requests in 30.00s, 107.85MB read 47 | Requests/sec: 6169.28 48 | Transfer/sec: 3.59MB 49 | ``` 50 | 51 | ** Max Latency减小16%,tps提升3%。** 52 | 53 | 54 | ### Threadpool 模型 55 | 我们先看下一次 node.js read 的调用示意图: 56 | ![](node-aio.png) 57 | 58 | 代码的运行经历了以下步骤: 59 | - 1 node, libuv 初始化; 60 | 61 | - 2 node_file.cc中的Read方法调用libuv(fs.c)的 `uv_fs_read` , 封装请求; 62 | 63 | - 3 libuv 将请求封装成 uv_work, 提交到任务队列尾部,触发信号; 64 | 65 | - 4 此时主线程的read调用返回。 66 | 67 | - 5 线程池从uv_work队列中取出一个请求,开始执行read IO; 68 | 69 | - 6 向主线程发送信号表明任务完成,等待执行read调用后的其它操作。 70 | 71 | - 7 主线程 epoll,从响应队列取已经完成的请求; 72 | 73 | - 8 主线程响应 epoll事件; 74 | 75 | - 9 主线程执行请求的callback函数。 76 | 77 | 78 | node.js 异步 IO 的脉络已经清晰,我们清楚的看到这样的一个 Threadpool 模型是全平台适用的。 79 | 80 | > Linux 上的 AIO 81 | > AIO 在 2.5 版本的内核中首次出现,现在已经是 2.6 版本的产品内核的一个标准特性了。 82 | 83 | 并且由于 Native AIO 是在 linux 2.6之后引入,并且并不稳定。 社区也有过激烈的讨论: 84 | 85 | - https://github.com/libuv/libuv/issues/28 86 | - https://github.com/libuv/libuv/issues/461 87 | 88 | 89 | 权衡再三,笔者也非常支持社区采用的模型,赋予用户更多的选择性和可靠性。 90 | 91 | ### 总结 92 | 用户态的线程池实现给了用户更大的灵活性和选择性。比如: 93 | 94 | * 1.线程池的个数,默认是4个,用户可以通过设置环境变量 `UV_THREADPOOL_SIZE`指定。 95 | * 2.和耗时的 GETADDRINFO 复用线程池。 96 | 97 | 需要指出的是,线程池模型还有改进的空间: 98 | * `static uv_mutex_t mutex;`全局锁的优化; 99 | * 支持任务优先级。 100 | 101 | ### 参考 102 | * [GitHub Issue #28 关于 libuv 在 Linux 上 AIO 实现的讨论](citehttps://github.com/libuv/libuv/issues/28) 103 | * [GitHub Issue #461 对 native AIO 实现稳定性的讨论](citehttps://github.com/libuv/libuv/issues/461) 104 | * [基于 libuv 的 native AIO 实现提交记录](citehttps://github.com/yjhjstz/libuv/commit/2748728635c4f74d6f27524fd36e680a88e4f04a) 105 | * [简书文章《什么是 SSL?》及相关加密技术讨论](citehttp://www.jianshu.com/p/a8b87e436ac7) 106 | -------------------------------------------------------------------------------- /chapter11/chapter11-6.md: -------------------------------------------------------------------------------- 1 | ## FAQ 2 | 3 | 4 | ### 同步 require() 5 | 6 | ```js 7 | // Native extension for .js 8 | Module._extensions['.js'] = function(module, filename) { 9 | var content = fs.readFileSync(filename, 'utf8'); 10 | module._compile(internalModule.stripBOM(content), filename); 11 | }; 12 | ``` 13 | 大家可能都有疑问:为什么会选择使用同步而不用异步实现呢? 14 | 15 | 之所以同步是 Node.js 所遵循的 CommonJS 的模块规范要求的, 具体来说 16 | 17 | 在当年,CommonJS 社区对此就有很多争议,导致了坚持异步的 AMD 从 CommonJS 中分裂出来。 18 | 19 | CommonJS 模块是同步加载和同步执行,AMD 模块是异步加载和异步执行,CMD(Sea.js)模块是异步加载和同步执行。ES6 的模块体系最后选择的是异步加载和同步执行。也就是 Sea.js 的行为是最接近 ES6 模块的。不过 Sea.js 这样做是需要付出代价的——需要扫描代码提取依赖,所以它不像 CommonJS/AMD 是纯运行时的模块系统。 20 | 21 | 注意 Sea.js 是 2010年之后开发的,提出 CMD 更晚。Node.js 当年(2009年)只有 CommonJS 和 AMD 两个选择。就算当时已经有 CMD 的等价提案,从性能角度出发,Node.js 不太可能选择需要静态分析开销的 类 CMD 方案。考虑到 Node.js 的模块是来自于本地文件系统,最后 Node.js 选择了看上去更简单的 CommonJS 模块规范,直到今天。 22 | 23 | 24 | * 从模块规范的角度来看,依赖的同步获取是几乎所有模块机制的首选,是符合由无数的语言奠定的开发者的直觉。 25 | 26 | * 从模块本身的特性来说的,结论就是使用异步的require收益很小,同时对开发者并不友好。 27 | 28 | ### fs.realpath 缓存 29 | 如今的 `realpath`的实现变得非常简洁, 直接调用系统调用realpath。 30 | ```js 31 | fs.realpath = function realpath(path, options, callback) { 32 | if (!options) { 33 | options = {}; 34 | } else if (typeof options === 'function') { 35 | callback = options; 36 | options = {}; 37 | } else if (typeof options === 'string') { 38 | options = {encoding: options}; 39 | } else if (typeof options !== 'object') { 40 | throw new TypeError('"options" must be a string or an object'); 41 | } 42 | callback = makeCallback(callback); 43 | if (!nullCheck(path, callback)) 44 | return; 45 | var req = new FSReqWrap(); 46 | req.oncomplete = callback; 47 | binding.realpath(pathModule._makeLong(path), options.encoding, req); 48 | return; 49 | }; 50 | ``` 51 | 52 | 大家可能又有疑问了, 原本提升性能的路径缓存去哪里了,不是说缓存都是提升性能的重要手段吗? 53 | 54 | 社区的修改可以在 https://github.com/nodejs/node/pull/3594 看到, 55 | > fs: optimize realpath using uv_fs_realpath() 56 | 57 | > Remove realpath() and realpathSync() cache. 58 | > Use the native uv_fs_realpath() which is faster 59 | > then the JS implementation by a few orders of magnitude 60 | 61 | 去掉了缓存反而提升了性能, 作者的 commit 提交也写的非常清楚:native uv_fs_realpath 实现要大大优于js层的实现, 62 | 但并没有说具体原因。 63 | 64 | 65 | 前面我已经提到过了文件系统的基本原理和大致实现,VFS中引入了高速磁盘缓存的机制,这属于一种软件机制,允许内核将原本存在磁盘上的某些信息保存在RAM中,以便对这些数据的进一步访问能快速进行,而不必慢速访问磁盘本身。 66 | 高速磁盘缓存可大致分为以下三种: 67 | * 目录项高速缓存——主要存放的是描述文件系统路径名的目录项对象 68 | * 索引节点高速缓存——主要存放的是描述磁盘索引节点的索引节点对象 69 | * 页高速缓存——主要存放的是完整的数据页对象,每个页所包含的数据一定属于某个文件,同时,所有的文件读写操作都依赖于页高速缓存。其是Linux内核所使用的主要磁盘高速缓存。 70 | 71 | `readpath` 的 native 实现的高性能得益于目录项高速缓存,有自身的淘汰机制,保持自身的高效的访问。其实缓存机制依然存在,只是下移到 VFS文件系统层面了。 72 | 73 | 74 | ### 流式读 75 | nodejs的fs模块并没有提供一个copy的方法,但我们可以很容易的实现一个,比如: 76 | ```js 77 | var source = fs.readFileSync('/path/to/source', {encoding: 'utf8'}); 78 | fs.writeFileSync('/path/to/dest', source); 79 | ``` 80 | 这种方式是把文件内容全部读入内存,然后再写入文件,对于小型的文本文件,这没有多大问题。但是对于体积较大的二进制文件,比如音频、视频文件,动辄几个GB大小,如果使用这种方法,很容易使内存“爆仓”。具体的说,对于32位系统是1GB,64位是2GB。 81 | 82 | 理想的方法应该是读一部分,写一部分,不管文件有多大,只要时间允许,总会处理完成,这里就需要用到流的概念。 83 | 84 | 上面的文件复制可以简单实现一下: 85 | ```js 86 | // pipe自动调用了data,end等事件 87 | fs.createReadStream('/path/to/source').pipe(fs.createWriteStream('/path/to/dest')); 88 | ``` 89 | 源文件通过管道自动流向了目标文件。 90 | 91 | 92 | ### 总结 93 | - 不要迷信异步, 使用时评估同步和异步的开销,包括复杂度和性能。 94 | 95 | - 缓存策略需要综合考虑,这离不开对系统的了解(更多的涉猎),重复缓存只会带来没必要的开销。 96 | 97 | - 大文件的操作,使用流式操作。 98 | 99 | 100 | ### 参考 101 | * https://www.zhihu.com/question/38041375 102 | 103 | 104 | -------------------------------------------------------------------------------- /chapter9/chapter9-2.md: -------------------------------------------------------------------------------- 1 | ## 应用构建 2 | 3 | 4 | ### 创建TCP服务端 5 | 下面是一个在NodeJS中创建TCP服务端套接字的简单例子,相关说明见代码注释。 6 | ```js 7 | var net = require('net'); 8 | 9 | var HOST = '127.0.0.1'; 10 | var PORT = 6969; 11 | 12 | // 创建一个TCP服务器实例,调用listen函数开始监听指定端口 13 | // 传入net.createServer()的回调函数将作为”connection“事件的处理函数 14 | // 在每一个“connection”事件中,该回调函数接收到的socket对象是唯一的 15 | var server = net.createServer(); 16 | server.listen(PORT, HOST); 17 | console.log('Server listening on ' + 18 | server.address().address + ':' + server.address().port); 19 | 20 | server.on('connection', function(sock) { 21 | console.log('CONNECTED: ' + 22 | sock.remoteAddress +':'+ sock.remotePort); 23 | }); 24 | ``` 25 | 26 | 首先我们来看下 `net.createServer`, 它返回一个 `Server`的实例,如下。 27 | ```js 28 | 1075 function Server(options, connectionListener) { 29 | 1076 if (!(this instanceof Server)) 30 | 1077 return new Server(options, connectionListener); 31 | 1078 32 | 1079 EventEmitter.call(this); 33 | 1080 34 | 1081 var self = this; 35 | 1082 36 | 1083 if (typeof options === 'function') { 37 | 1084 connectionListener = options; 38 | 1085 options = {}; 39 | 1086 self.on('connection', connectionListener); 40 | 1087 } else { 41 | 1088 options = options || {}; 42 | 1089 43 | 1090 if (typeof connectionListener === 'function') { 44 | 1091 self.on('connection', connectionListener); 45 | 1092 } 46 | 1093 } 47 | 1094 48 | 1095 this._connections = 0; 49 | 1096 // ... 50 | 1111 51 | 1112 this._handle = null; 52 | 1113 this._usingSlaves = false; 53 | 1114 this._slaves = []; 54 | 1115 this._unref = false; 55 | 1116 56 | 1117 this.allowHalfOpen = options.allowHalfOpen || false; 57 | 1118 this.pauseOnConnect = !!options.pauseOnConnect; 58 | 1119 } 59 | 1120 util.inherits(Server, EventEmitter); 60 | ``` 61 | 62 | `Server` 继承了 `EventEmitter`, 如果传入 callback 函数, L1086,L1091 则把传入的函数作为监听者 63 | 绑定到 `connnection`事件上, 然后 listen 。我们看看作为 server 端连接到来的回调处理。 64 | ```js 65 | 1400 function onconnection(err, clientHandle) { 66 | 1401 var handle = this; 67 | 1402 var self = handle.owner; 68 | 1403 69 | 1404 debug('onconnection'); 70 | 1405 71 | 1406 if (err) { 72 | 1407 self.emit('error', errnoException(err, 'accept')); 73 | 1408 return; 74 | 1409 } 75 | 1410 76 | 1411 if (self.maxConnections && self._connections >= self.maxConnections) { 77 | 1412 clientHandle.close(); 78 | 1413 return; 79 | 1414 } 80 | 1415 81 | 1416 var socket = new Socket({ 82 | 1417 handle: clientHandle, 83 | 1418 allowHalfOpen: self.allowHalfOpen, 84 | 1419 pauseOnCreate: self.pauseOnConnect 85 | 1420 }); 86 | 1421 socket.readable = socket.writable = true; 87 | 1422 88 | 1423 89 | 1424 self._connections++; 90 | 1425 socket.server = self; 91 | 1426 socket._server = self; 92 | 1427 // ... 93 | 1431 self.emit('connection', socket); 94 | 1432 } 95 | ``` 96 | 此函数由 `TCPWrap::OnConnection`回调,`tcp_wrap->MakeCallback(env->onconnection_string(), ARRAY_SIZE(argv), argv);`, 第一个参数标识状态,第二个参数为连接的句柄。 97 | 98 | L1416-L1421, 根据传过来的句柄,创建 JS 层面的 Socket。并在 L1431 向观察者发送 connection 事件。 99 | 100 | 101 | 上面 TCP 服务端的例子,server 监听connection 事件,自定义用户处理逻辑。 102 | 103 | 104 | 105 | ### 创建TCP客户端 106 | 107 | 现在让我们创建一个TCP客户端连接到刚创建的服务器上,该客户端向服务器发送一串消息,并在得到服务器的反馈后关闭连接。下面的代码描述了这一过程。 108 | ```js 109 | var net = require('net'); 110 | 111 | var HOST = '127.0.0.1'; 112 | var PORT = 6969; 113 | 114 | var client = new net.Socket(); 115 | client.connect(PORT, HOST, function() { 116 | 117 | console.log('CONNECTED TO: ' + HOST + ':' + PORT); 118 | // 建立连接后立即向服务器发送数据,服务器将收到这些数据 119 | client.write('I am Chuck Norris!'); 120 | 121 | }); 122 | 123 | // 为客户端添加“data”事件处理函数 124 | // data是服务器发回的数据 125 | client.on('data', function(data) { 126 | 127 | console.log('DATA: ' + data); 128 | // 完全关闭连接 129 | client.destroy(); 130 | 131 | }); 132 | 133 | // 为客户端添加“close”事件处理函数 134 | client.on('close', function() { 135 | console.log('Connection closed'); 136 | }); 137 | ``` 138 | 139 | 创建 `Socket`对象后,client 端向server端发起连接,在真正的连接之前,需要进行 DNS 查询(提供 IP 的不用), 140 | 调用 `lookupAndConnect`, 之后才是调用 `function connect(self, address, port, addressType, localAddress, localPort) `发起连接。 141 | 142 | 我们注意到五元组: ``, 他们唯一的标识了一个网络连接。 143 | 144 | 建立起全双工的 Socket 后,用户程序就可以监听 「data」事件,获取数据了。 145 | 146 | 147 | 148 | ### 总结 149 | 150 | 151 | 152 | ### 参考 -------------------------------------------------------------------------------- /chapter13/chapter13-2.md: -------------------------------------------------------------------------------- 1 | 2 | ## 异常处理与domain 3 | 4 | ### 异步异常捕获 5 | 由于node的回调异步特性,无法通过try catch来捕捉所有的异常: 6 | ```js 7 | try { 8 | process.nextTick(function () { 9 | foo.bar(); 10 | }); 11 | } catch (err) { 12 | //can not catch it 13 | } 14 | ``` 15 | 如果try catch能够捕获所有的异常,这样我们可以在代码出现一些非预期的错误时,能够记录下错误的同时,友好的给调用者返回一个500错误。可惜,try catch无法捕获异步中的异常。所以我们能做的只能是: 16 | ```js 17 | app.get('/index', function (req, res) { 18 | // 业务逻辑 19 | }); 20 | 21 | process.on('uncaughtException', function (err) { 22 | logger.error(err); 23 | }); 24 | ``` 25 | 26 | 这个时候,虽然我们可以记录下这个错误的日志,且进程也不会异常退出,但是我们是没有办法对发现错误的请求友好返回的,因为异常处理只返回给我们一个冷冰冰的 `error`, 脱离了上下文,我们只能够让它超时返回。 27 | 28 | ### domain 29 | 在node v0.8+版本的时候,发布了一个模块domain。这个模块做的就是try catch所无法做到的:捕捉异步回调中出现的异常。 于是乎,我们上面那个无奈的例子好像有了解决的方案: 30 | ```js 31 | var domain = require('domain'); 32 | 33 | //引入一个domain的中间件,将每一个请求都包裹在一个独立的domain中 34 | //domain来处理异常 35 | app.use(function (req,res, next) { 36 | var d = domain.create(); 37 | //监听domain的错误事件 38 | d.on('error', function (err) { 39 | logger.error(err); 40 | res.statusCode = 500; 41 | res.json({sucess:false, messag: '服务器异常'}); 42 | d.dispose(); 43 | }); 44 | 45 | d.add(req); 46 | d.add(res); 47 | d.run(next); 48 | }); 49 | ``` 50 | `domain`虽然捕捉到了异常,但是还是由于异常而导致的堆栈丢失会导致内存泄漏,所以出现这种情况的时候还是需要重启这个进程的。 51 | 52 | ### Domain剖析 53 | Domain 自身其实也是 Event 模块一个典型的应用, 它通过事件的方式来传递捕获的错误。 54 | 55 | ```js 56 | inherits(Domain, EventEmitter); 57 | 58 | function Domain() { 59 | EventEmitter.call(this); 60 | 61 | this.members = []; 62 | } 63 | ``` 64 | 65 | 另外, domain 为了支持深层次的嵌套, 提供了 `Domain#enter` 和 `Domain#exit` 的 API。 66 | 先来看 `enter`的实现, 67 | ```js 68 | Domain.prototype.enter = function() { 69 | if (this._disposed) return; 70 | 71 | // note that this might be a no-op, but we still need 72 | // to push it onto the stack so that we can pop it later. 73 | exports.active = process.domain = this; 74 | stack.push(this); 75 | _domain_flag[0] = stack.length; 76 | }; 77 | ``` 78 | 设置当前活跃的 `domain`, 并且为了便于回溯,将当前的 `domain` 加入到队列的后面,更新栈的深度。 79 | 80 | 再看 `exit`实现, 81 | ```js 82 | Domain.prototype.exit = function() { 83 | // skip disposed domains, as usual, but also don't do anything if this 84 | // domain is not on the stack. 85 | var index = stack.lastIndexOf(this); 86 | if (this._disposed || index === -1) return; 87 | 88 | // exit all domains until this one. 89 | stack.splice(index); 90 | _domain_flag[0] = stack.length; 91 | 92 | exports.active = stack[stack.length - 1]; 93 | process.domain = exports.active; 94 | }; 95 | ``` 96 | 相反的, 退出当前的 `domain`, 更新长度,设置当前活跃的 `domain`。 97 | 98 | 读者可能好奇,我并没有显式地调用 `enter`, `exit`, 而只是简单的创建了一个 domain, 怎么会达到这种效果? 99 | 100 | 读者可以看看 `AsyncWrap::MakeCallback()`, 每次C++ --> JS, 都会检查 domain, 如果使用,则会显式地调用他们。 101 | 其他地方读者可以自行寻找。 102 | 103 | 为了解决不在当前作用域的异常处理, Domain 也提供 `Domain#add` 和 `Domain#remove` 来增加 `emitter` 或者 104 | `Timer`。 105 | 106 | 107 | 回到事件的根本, 什么时候触发domain的error事件? 108 | ```js 109 | process._fatalException = function(er) { 110 | var caught; 111 | 112 | if (process.domain && process.domain._errorHandler) 113 | caught = process.domain._errorHandler(er) || caught; 114 | 115 | if (!caught) 116 | caught = process.emit('uncaughtException', er); 117 | 118 | // If someone handled it, then great. otherwise, die in C++ land 119 | // since that means that we'll exit the process, emit the 'exit' event 120 | if (!caught) { 121 | try { 122 | if (!process._exiting) { 123 | process._exiting = true; 124 | process.emit('exit', 1); 125 | } 126 | } catch (er) { 127 | // nothing to be done about it at this point. 128 | } 129 | 130 | // if we handled an error, then make sure any ticks get processed 131 | } else { 132 | NativeModule.require('timers').setImmediate(process._tickCallback); 133 | } 134 | 135 | return caught; 136 | }; 137 | ``` 138 | 139 | 如果当前 process 使用了 domain, 也是就 `process.domain` 不为空,就调用 `_errorHandler` 来处理, 140 | 当前也存在没有处理的情况,职责链来到 process, process 则触发 `uncaughtException` 事件。 141 | 142 | 143 | ### 总结 144 | domain很强大,但它只能捕获在其作用域范围内的异常。对于非预期的异常产生的时候, 我们最好让当前请求超时,然后让这个进程停止服务,之后重新启动。 145 | 146 | 但始终 Domain 在异常处理上有各种不完美,目前该模块处于即将废除阶段,取代他的可能是另一种机制。 147 | 148 | 详细讨论见: 149 | > https://github.com/nodejs/node/issues/66 150 | 151 | ### 参考 152 | * http://node.alibaba-inc.com/post/async-error-handle-and-domain.html?spm=0.0.0.0.7r8vQ2 153 | 154 | -------------------------------------------------------------------------------- /chapter13/chapter13-1.md: -------------------------------------------------------------------------------- 1 | 2 | ## 子进程 (child_process) 3 | 4 | child_process 是Node的一个十分重要的模块,通过它可以实现创建多进程,以利用单机的多核计算资源。虽然,Nodejs天生是单线程单进程的,但是有了child_process模块,可以在程序中直接创建子进程,并使用主进程和子进程之间实现通信。 5 | 6 | ### 进程通信 7 | 每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核, 8 | 在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。 9 | 10 | 11 | 类型 | 无连接 | 可靠 | 流控制 | 优先级 12 | ----|---------|------|----------| ----- 13 | 普通PIPE |N | Y | Y | N 14 | 命名PIPE| N | Y | Y | N 15 | 消息队列| N | Y | Y | N 16 | 信号量 | N | Y | Y | Y 17 | 共享存储| N | Y | Y | Y 18 | UNIX流SOCKET | N | Y | Y | N 19 | UNIX数据包SOCKET| Y |Y |N | N 20 | 21 | 22 | * 注:无连接: 指无需调用某种形式的open,就有发送消息的能力流控制: 23 | 24 | Node 中实现 IPC 通信的是管道技术,但只是抽象的称呼,具体细节实现由 libuv提供, 在 windows 下由命名管道(named pipe)实现, *nix 系统则采用 Unix Domain Socket实现。 也就是上表中的最后第二个。 25 | 26 | Socket API原本是为网络通讯设计的,但后来在socket的框架上发展出一种IPC机制,就是UNIX Domain Socket。虽然网络socket也可用于同一台主机的进程间通讯(通过loopback地址127.0.0.1),但是UNIX Domain Socket用于IPC更有效率:不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。 27 | 28 | > Depending on the platform, unix domain sockets can achieve around 50% more throughput than the TCP/IP loopback (on Linux for instance). 29 | 30 | 这是因为,IPC机制本质上是可靠的通讯,而网络协议是为不可靠的通讯设计的。UNIX Domain Socket也提供面向流和面向数据包两种API接口,类似于TCP和UDP,但是面向消息的UNIX Domain Socket也是可靠的,消息既不会丢失也不会顺序错乱。 31 | 32 | 33 | ### 创建子进程 34 | * spawn()启动一个子进程来执行命令 35 | * exec()启动一个子进程来执行命令, 带回调参数获知子进程的情况, 可指定进程运行的超时时间 36 | * execFile()启动一个子进程来执行一个可执行文件, 可指定进程运行的超时时间 37 | * fork() 与spawn()类似, 不同在于它创建的node子进程只需指定要执行的js文件模块即可 38 | ```js 39 | // don't call this example code 40 | var cp = require('child_process'); 41 | cp.spawn('node', ['work.js']); 42 | cp.exec('node work.js', function(err, stdout, stderr) { 43 | // some code 44 | }); 45 | cp.execFile('work.js', function(err, stdout, stderr) { 46 | // some code 47 | }); 48 | cp.fork('./work.js'); 49 | ``` 50 | 51 | exec方法会直接调用bash(/bin/sh程序)来解释命令,所以如果有用户输入的参数,exec方法是不安全的。 52 | ```js 53 | var path = ";user input"; 54 | child_process.exec('ls -l ' + path, function (err, data) { 55 | console.log(data); 56 | }); 57 | ``` 58 | 上面代码表示,在bash环境下,`ls -l; user input` 59 | 会直接运行。如果用户输入恶意代码,将会带来安全风险。因此,在有用户输入的情况下,最好不使用exec方法,而是使用execFile方法。 60 | 61 | 62 | 63 | ### 建立 IPC 通道 64 | 父进程在创建子进程前创建IPC通道并监听, 用环境变量NODE_CHANNEL_FD告诉子进程的IPC的文件描述符。 65 | ```js 66 | startup.processChannel = function() { 67 | // If we were spawned with env NODE_CHANNEL_FD then load that up and 68 | // start parsing data from that stream. 69 | if (process.env.NODE_CHANNEL_FD) { 70 | var fd = parseInt(process.env.NODE_CHANNEL_FD, 10); 71 | assert(fd >= 0); 72 | 73 | // Make sure it's not accidentally inherited by child processes. 74 | delete process.env.NODE_CHANNEL_FD; 75 | 76 | var cp = NativeModule.require('child_process'); 77 | 78 | // Load tcp_wrap to avoid situation where we might immediately receive 79 | // a message. 80 | // FIXME is this really necessary? 81 | process.binding('tcp_wrap'); 82 | 83 | cp._forkChild(fd); 84 | assert(process.send); 85 | } 86 | }; 87 | 88 | ``` 89 | 90 | 子进程在启动的过程中连接IPC的FD 91 | ```js 92 | exports._forkChild = function(fd) { 93 | // set process.send() 94 | var p = new Pipe(true); 95 | p.open(fd); 96 | p.unref(); 97 | const control = setupChannel(process, p); 98 | process.on('newListener', function(name) { 99 | if (name === 'message' || name === 'disconnect') control.ref(); 100 | }); 101 | process.on('removeListener', function(name) { 102 | if (name === 'message' || name === 'disconnect') control.unref(); 103 | }); 104 | }; 105 | ``` 106 | 建立连接后父子进程就可以自由的,全双工的通信了。 107 | 108 | 109 | ### 句柄传递 110 | 111 | ChildProcess 类的实例,通过调用 ChildProcess#send(message[, sendHandle[, options]][, callback]) 方法,我们可以实现与子进程的通信,其中的 sendHandle 参数支持传递 net.Server ,net.Socket 等多种句柄,使用它,我们可以很轻松的实现在进程间转发 TCP socket。 112 | 113 | send方法可以发送的对象包括如下集中: 114 | 115 | - net.Socket对象: TCP套接字 116 | - net.Server对象: TCP服务器 117 | - net.Native: C++层面的TCP套接字和IPC管道 118 | - dgram.Socket: UDP套接字 119 | - dgram.Native: C++层面的UDP套接字 120 | 121 | 传递的过程: 122 | 123 | **主进程**: 124 | 125 | - 传递消息和句柄。 126 | - 将消息包装成内部消息,使用 JSON.stringify 序列化为字符串。 127 | - 通过对应的 handleConversion[message.type].send 方法序列化句柄。 128 | - 将序列化后的字符串和句柄发入 IPC channel 。 129 | 130 | **子进程**: 131 | 132 | - 使用 JSON.parse 反序列化消息字符串为消息对象。 133 | - 触发内部消息事件(internalMessage)监听器。 134 | - 将传递来的句柄使用 handleConversion[message.type].got 方法反序列化为 JavaScript 对象。 135 | - 带着消息对象中的具体消息内容和反序列化后的句柄对象,触发用户级别事件。 136 | 137 | 138 | ### 总结 139 | 很多应用比如 redis提供了本地访问的接口,进程通信使用的是 socket 的回环地址。当然它是通用性的考虑,否则要区分本地环境还是网络环境,如果不考虑这点,其实可以用 unix domain socket 140 | 代替,以获取更好的相互性能。 141 | 142 | > Here you have the results on a single CPU 3.3GHz Linux machine : 143 | 144 | 类型 | TCP | UDS | PIPE 145 | -----| -----| ------| ---- 146 | latency | 6us | 2us | 2us 147 | throughput | 253702 msg/s| 1733874 msg/s | 1682796 msg/s 148 | 149 | * UDS: UNIX Domain Socket 150 | 151 | 152 | ### 参考 153 | 154 | [1]. https://github.com/rigtorp/ipc-bench 155 | 156 | -------------------------------------------------------------------------------- /chapter6/chapter6-1.md: -------------------------------------------------------------------------------- 1 | 2 | ## Buffer 3 | 4 | 5 | ### 6 | 在Node.js中,Buffer类是随Node内核一起发布的核心库。Buffer库为Node.js带来了一种存储原始数据的方法,可以让Nodejs处理二进制数据,每当需要在Nodejs中处理I/O操作中移动的数据时,就有可能使用Buffer库。原始数据存储在 Buffer 类的实例中。一个 Buffer 类似于一个整数数组,但它对应于 V8 堆内存之外的一块原始内存。 7 | 8 | Buffer 和 Javascript 字符串对象之间的转换需要显式地调用编码方法来完成。以下是几种不同的字符串编码: 9 | 10 | - ‘ascii’ – 仅用于 7 位 ASCII 字符。这种编码方法非常快,并且会丢弃高位数据。 11 | - ‘utf8’ – 多字节编码的 Unicode 字符。许多网页和其他文件格式使用 UTF-8。 12 | - ‘ucs2’ – 两个字节,以小尾字节序(little-endian)编码的 Unicode 字符。它只能对 BMP(基本多文种平面,U+0000 – U+FFFF) 范围内的字符编码。 13 | - ‘base64’ – Base64 字符串编码。 14 | - ‘binary’ – 一种将原始二进制数据转换成字符串的编码方式,仅使用每个字符的前 8 位。这种编码方法已经过时,应当尽可能地使用 Buffer 对象。 15 | - 'hex' - 每个字节都采用 2 进制编码。 16 | 17 | 在Buffer中创建一个数组,需要注意以下规则: 18 | - Buffer 是内存拷贝,而不是内存共享。 19 | 20 | - Buffer 占用内存被解释为一个数组,而不是字节数组。比如,new Uint32Array(new Buffer([1,2,3,4])) 创建了4个 Uint32Array,它的成员为 [1,2,3,4] ,而不是[0x1020304] 或 [0x4030201]。 21 | 22 | 23 | 24 | ### slab 分配 25 | 在 lib/buffer.js 模块中,有个模块私有变量 pool, 它指向当前的一个8K 的slab : 26 | 27 | ```js 28 | Buffer.poolSize = 8 * 1024; 29 | var pool; 30 | 31 | function allocPool() { 32 | pool = new SlowBuffer(Buffer.poolSize); 33 | pool.used = 0; 34 | } 35 | ``` 36 | SlowBuffer 为 src/node_buffer.cc 导出,当用户调用new Buffer时 ,如果你要申请的空间大于8K,node 会直接调用SlowBuffer ,如果小于8K ,新的Buffer 会建立在当前slab 之上: 37 | 38 | - 新创建的Buffer的 parent成员变量会指向这个slab , 39 | - offset 变量指向在这个slab 中的偏移: 40 | ```js 41 | if (!pool || pool.length - pool.used < this.length) allocPool(); 42 | this.parent = pool; 43 | this.offset = pool.used; 44 | pool.used += this.length; 45 | ``` 46 | 47 | PS: 在 lib/_tls_legacy.js 中, `SlabBuffer` 创建了一个 10MB 的 slab。 48 | 49 | ```js 50 | function alignPool() { 51 | // Ensure aligned slices 52 | if (poolOffset & 0x7) { 53 | poolOffset |= 0x7; 54 | poolOffset++; 55 | } 56 | } 57 | ``` 58 | 这里做了8字节的内存对齐处理。 59 | * 如果不按照平台要求对数据存放进行对齐,会带来存取效率上的损失。比如32位的Intel处理器通过总线访问(包括读和写)内存数据。每个总线周期从偶地址开始访问32位内存数据,内存数据以字节为单位存放。如果一个32位的数据没有存放在4字节整除的内存地址处,那么处理器就需要2个总线周期对其进行访问,显然访问效率下降很多。 60 | 61 | * Node.js 是一个跨平台的语言,第三方的C++ addon 也是非常多,避免破坏了第三方模块的使用,比如 directIO 就必须要内存对齐。 62 | 63 | * 兼容 node.js v0.10 64 | 65 | > 详细:https://github.com/nodejs/node/pull/2487 66 | 67 | ### 浅拷贝 68 | Buffer更像是可以做指针操作的C语言数组。例如,可以用[index]方式直接修改某个位置的字节。 69 | 需要注意的是:Buffer#slice 方法, 不是返回一个新的Buffer, 而是返回对原 Buffer 某个区间数值的引用。 70 | ```js 71 | const buf1 = Buffer.allocUnsafe(26); 72 | 73 | for (var i = 0 ; i < 26 ; i++) { 74 | buf1[i] = i + 97; // 97 is ASCII a 75 | } 76 | 77 | const buf2 = buf1.slice(0, 3); 78 | buf2.toString('ascii', 0, buf2.length); 79 | // Returns: 'abc' 80 | buf1[0] = 33; 81 | buf2.toString('ascii', 0, buf2.length); 82 | // Returns : '!bc' 83 | 84 | ``` 85 | 86 | 上面是官方 API 提供的例子, `buf2`是对 `buf1`前3个字节的引用,对 `buf2`的修改就相当于作用在 `buf1`上。 87 | 88 | 89 | ### 深拷贝 90 | 如果想要拷贝一份Buffer,得首先创建一个新的Buffer,并通过.copy方法把原Buffer中的数据复制过去。 91 | 92 | ```js 93 | const buf1 = Buffer.allocUnsafe(26); 94 | const buf2 = Buffer.allocUnsafe(26).fill('!'); 95 | 96 | for (let i = 0 ; i < 26 ; i++) { 97 | buf1[i] = i + 97; // 97 is ASCII a 98 | } 99 | 100 | buf1.copy(buf2, 8, 16, 20); 101 | console.log(buf2.toString('ascii', 0, 25)); 102 | // Prints: !!!!!!!!qrst!!!!!!!!!!!!! 103 | ``` 104 | 105 | 通过深拷贝的方式,`buf2` 截取了 `buf1` 的部分内容,之后对 `buf2`的修改并不会作用于 `buf1`, 两者内容独立不共享。 106 | 107 | 需要注意的事:深拷贝是一种消耗 CPU 和内存的操作,请知道自己在做什么。 108 | 109 | 110 | ### 内存碎片 111 | 112 | 动态分配将不可避免会产生内存碎片的问题,那么什么是内存碎片? 113 | 内存碎片即“碎片的内存”描述一个系统中所有不可用的空闲内存,这些碎片之所以不能被使用,是因为负责动态分配内存的分配算法使得这些空闲的内存无法使用。 114 | 115 | 上述的 slab 分配,存在明显的内存碎片,即 8KB 的内存并没有完全被使用,存在一定的浪费。通用的slab实现,会浪费约1/2的空间。 116 | 117 | 当然存在更高效,更省内存的内存管理分配,比如 tcmalloc, 但也必须承受一定的管理代价。node.js 在这方面并没有一味的执着于此,而是达到一种性能与空间使用的平衡。 118 | 119 | ### zero fill 120 | 121 | Node.js 在v5.10.0 加入了命令行选项 `--zero-fill-buffers`, 强制在申请 `Buffer`时用0填充分配的内存。 122 | 123 | 为什么要引入这个特性呢? 124 | 125 | - 防止你代码中本该初始化的地方没有初始化; 126 | - 防止其他代码访问到你之前写入 Buffer 的数据, 这边存在安全隐患, 如下 127 | ```js 128 | ✗ node -p "new Buffer(1024).toString('ascii')" 129 | `7(@ P 130 | xn?_k7x0x0' @#k 131 | :ArrayBuffer kh 132 | &7;?m@bFn?_ @`` n?0'h2R'Lq083~C[e;@string k (R!~!H3kl 133 | ``` 134 | 135 | 代码实现上则是通过 `--zero-fill-buffers` 区分申请内存是用 `malloc()`或者 `calloc()`。 136 | 137 | 当然性能上 `calloc()` 还是差很多,所以社区开放了一个选项,而不是默认开启。 138 | 139 | 140 | * here are benchmark results for allocating a 1mb Buffer: 141 | 142 | 143 | 144 | xxx | v5.4.0 | v4.2.3 | v0.12.9 | v0.10.41 145 | --- | -------- | -------| ---------|--------- 146 | new Buffer | 41,515 ops/sec ±3.00% | 43,670 ops/sec ±1.86% | 53,969 ops/sec ±1.41% | 147,350 ops/sec ±1.82% 147 | new Buffer (zero-filled) | 5,041 ops/sec ±2.00% | 4,293 ops/sec ±1.79% | 7,953 ops/sec ±0.55% | 8,167 ops/sec ±2.38% 148 | 149 | 150 | 151 | > 具体了解:https://github.com/nodejs/node/issues/4660 152 | 153 | ### 总结 154 | Buffer是一个典型的Javascript和C++结合的模块,性能相关部分用C++实现,非性能相关部分用javascript实现。 155 | 156 | Node在进程启动时Buffer就已经加装进入内存,并将其放入全局对象,因此无需require。 157 | 158 | 159 | Buffer内存分配,Buffer对象的内存分配不是在V8的堆内存中,在Node的C++层面实现内存的申请。 160 | 161 | 162 | ### 参考 163 | 164 | * https://nodesource.com/blog/nsolid-deepdive-into-security-policies-zero-fill-buffer-allocations/ 165 | 166 | -------------------------------------------------------------------------------- /chapter14/chapter14-2.md: -------------------------------------------------------------------------------- 1 | ## Node.js Docker 2 | 3 | ### docker vs host 4 | ```js 5 | var http = require('http'); 6 | http.createServer(function (req, res) { 7 | res.writeHead(200, {'Content-Type': 'text/plain'}); 8 | res.end('Hello World\n'); 9 | }).listen(1337); 10 | console.log('Server running at http://0.0.0.0:1337/'); 11 | ``` 12 | 针对我们最关心的性能问题,用 `node-v4.2.3` 做了对比测试: 13 | `docker` vs `host` node 。结论是性能损失在1%~4%之间,依据网络,业务代码因子而不定,数据如下: 14 | - 外部网络环境 15 | 16 | ```shell 17 | docker node 18 | root@ubuntu-512mb-nyc3-01:~/wrk# ./wrk http://192.241.209.*:1337 19 | Running 10s test @ http://192.241.209.*:1337 20 | 2 threads and 10 connections 21 | Thread Stats Avg Stdev Max +/- Stdev 22 | Latency 76.01ms 1.24ms 88.79ms 83.68% 23 | Req/Sec 65.51 16.12 101.00 76.77% 24 | 1305 requests in 10.04s, 198.81KB read 25 | Requests/sec: 129.99 26 | Transfer/sec: 19.80KB 27 | host node 28 | root@ubuntu-512mb-nyc3-01:~/wrk# ./wrk http://192.241.209.*:1337 29 | Running 10s test @ http://192.241.209.*:1337 30 | 2 threads and 10 connections 31 | Thread Stats Avg Stdev Max +/- Stdev 32 | Latency 75.85ms 1.87ms 87.10ms 61.29% 33 | Req/Sec 65.66 12.35 101.00 57.07% 34 | 1307 requests in 10.03s, 199.11KB read 35 | Requests/sec: 130.27 36 | Transfer/sec: 19.85KB 37 | ``` 38 | - 内部网络环境 39 | 40 | ```shell 41 | ** host node** 42 | [root@centos7-x64 statusbar]# wrk http://localhost:1337 43 | Running 10s test @ http://localhost:1337 44 | 2 threads and 10 connections 45 | Thread Stats Avg Stdev Max +/- Stdev 46 | Latency 1.51ms 1.34ms 39.81ms 98.03% 47 | Req/Sec 3.46k 821.79 4.29k 76.50% 48 | 68971 requests in 10.05s, 10.26MB read 49 | Requests/sec: 6862.84 50 | Transfer/sec: 1.02MB 51 | 52 | docker node `--net=host`模式 53 | [root@centos7-x64 ~]# wrk http://localhost:1337 54 | Running 10s test @ http://127.0.0.1:1337 55 | 2 threads and 10 connections 56 | Thread Stats Avg Stdev Max +/- Stdev 57 | Latency 1.49ms 287.14us 3.81ms 92.99% 58 | Req/Sec 3.30k 424.64 3.60k 91.00% 59 | 65866 requests in 10.03s, 9.80MB read 60 | Requests/sec: 6564.82 61 | Transfer/sec: 0.98MB 62 | 63 | ``` 64 | 总的来说,对于正常 web 应用,走外部网络连接,性能损失很小,却极大的方便了我们的开发运维。 65 | 66 | ### 部署应用 67 | > 什么是 MEAN 架构? 68 | > MEAN 表示 Mongodb / ExpressJS / AngularJS /NodeJS,是目前流行的网站应用开发组合,涵盖前端至后台。由于这些框架用的语言都是 Javascript,所以又戏称 Javascript Fullstack。 69 | 70 | 本例子中,我们将尝试部署一个 MEAN 架构的 NodeJS 应用。 71 | ```shell 72 | 目录结构 73 | ├── docker-node-full/ 74 | │ ├── start.sh 75 | │ ├── Dockerfile 76 | ``` 77 | 78 | ### 安装 Docker 79 | Ubuntu的系统,使用 apt 安装: 80 | ```sh 81 | $ sudo apt-get install -y docker-engine 82 | ``` 83 | 84 | 85 | ### 创建步骤 86 | * 创建文件夹 87 | ```shell 88 | mkdir ~/docker-node-full && cd $_ 89 | ``` 90 | 91 | * 创建 Dockerfile 配置文件 92 | 93 | ```shell 94 | # 设置基础镜像 95 | FROM ubuntu:14.10 96 | 97 | # 安装 NodeJS 和 npm 98 | RUN apt-get install -y nodejs npm 99 | 100 | # 由于 apt-get 下载的 Node 实际上是 nodejs,所以要创建一个 node 的快捷方式 101 | RUN ln -s /usr/bin/nodejs /usr/bin/node 102 | 103 | # 安装 Git 104 | RUN apt-get install -y git 105 | 106 | # 安装 Mongodb(来自官方教程) 107 | RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 108 | RUN echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | tee /etc/apt/sources.list.d/mongodb.list 109 | RUN apt-get update 110 | RUN apt-get install -y mongodb-org 111 | 112 | # 设置工作目录 113 | WORKDIR /srv/full 114 | 115 | # 清空已存在的文件(如果有) 116 | RUN rm -rf /srv/full 117 | 118 | # 通过 Git 下载准备好的 MEAN 架构的网站代码 119 | RUN git clone https://github.com/chuyik/fullstack-demo-dist.git . 120 | 121 | # 安装 NodeJS 依赖库 122 | RUN npm install --production 123 | 124 | # 创建 mongodb 数据文件夹 125 | RUN mkdir -p /data/db 126 | 127 | # 暴露端口(分别是 NodeJS 应用和 Mongodb) 128 | EXPOSE 5566 27017 129 | 130 | # 设置 NodeJS 应用环境变量 131 | ENV NODE_ENV=production PORT=5566 132 | 133 | # 添加启动脚本 134 | ADD start.sh /tmp/ 135 | RUN chmod +x /tmp/start.sh 136 | 137 | # 设置启动时默认运行命令 138 | CMD ["bash", "/tmp/start.sh"] 139 | ``` 140 | 141 | * 创建 start.sh 启动脚本 142 | 143 | ```shell 144 | # 后台启动 Mongodb 145 | mongod --fork --logpath=/var/log/mongo.log --logappend 146 | 147 | # 运行 NodeJS 应用 148 | npm start 149 | ``` 150 | 151 | #### 构建镜像 152 | 153 | ```shell 154 | # 通过该命令,按照 Dockerfile 所配置的信息构建出镜像 155 | docker build --rm -t node-full . 156 | 157 | # 检查镜像是否创建成功 158 | docker images 159 | ``` 160 | 161 | 162 | #### 运行镜像 163 | 164 | ```shell 165 | # 运行刚刚创建的镜像 166 | # -p 设置端口,格式为「主机端口:容器端口」 167 | docker run -p 5566:5566 node-full 168 | ``` 169 | 170 | 171 | #### 访问应用 172 | 可以用浏览器访问 http://localhost:5566, 或运行 curl -s http://localhost:5566。 173 | 174 | 175 | 176 | ### 保存 Mongodb 数据文件 177 | 由于 Mongodb 服务运行在 Docker 容器 (container) 中,所以数据也在里面,但这并不利于数据管理和保存。因此,可以通过一些方法,将 Mongodb 数据文件保存在容器的外头。 178 | 179 | 180 | #### 磁盘映射 181 | 这个是最简单的方式,在 docker run 命令当中,就有磁盘映射的参数 -v。 182 | ```shell 183 | # -v 磁盘映射,格式为「主机目录:容器目录」 184 | docker run -p 5566:5566 -v /var/mongodata:/data/db node-full 185 | ``` 186 | 但这个命令在 Mac 和 Windows 中执行失败,因为 boot2docker 的虚拟机不支持。 187 | 所以,可以将数据保存在 boot2docker 内,并设置共享文件夹便于 Mac 或 Windows 访问。 188 | 189 | 190 | -------------------------------------------------------------------------------- /chapter10/chapter10-1.md: -------------------------------------------------------------------------------- 1 | 2 | ## HTTP 1/2 3 | 4 | 回到我们之前的 「Hello World」例子, 短短数行即可。 5 | ```js 6 | const http = require('http'); 7 | const hostname = '127.0.0.1'; 8 | const port = 1337; 9 | 10 | http.createServer((req, res) => { 11 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 12 | res.end('Hello World\n'); 13 | }).listen(port, hostname, () => { 14 | console.log(`Server running at http://${hostname}:${port}/`); 15 | }); 16 | ``` 17 | 18 | 因为 Node.js 把许多细节都已在源码中封装好了,主要代码在 lib/_http_*.js 这些文件中,现在就让我们照着上述代码,看看从一个 HTTP 请求的到来直到响应,Node.js 都为我们在源码层做了些什么。 19 | 20 | ### Server 21 | 在 Node.js 中,若要收到一个 HTTP 请求,首先需要创建一个 http.Server 类的实例,然后监听它的 request 事件。由于 HTTP 协议属于应用层,在下层的传输层通常使用的是 TCP 协议,所以 net.Server 类正是 http.Server 类的父类。 22 | 23 | ```js 24 | // lib/_http_server.js 25 | // ... 26 | 27 | function Server(requestListener) { 28 | if (!(this instanceof Server)) return new Server(requestListener); 29 | net.Server.call(this, { allowHalfOpen: true }); 30 | 31 | if (requestListener) { 32 | this.addListener('request', requestListener); 33 | } 34 | 35 | // ... 36 | this.addListener('connection', connectionListener); 37 | 38 | this.addListener('clientError', function(err, conn) { 39 | conn.destroy(err); 40 | }); 41 | 42 | this.timeout = 2 * 60 * 1000; 43 | 44 | this._pendingResponseData = 0; 45 | } 46 | util.inherits(Server, net.Server); 47 | ``` 48 | 49 | `requestListener` 回调函数作为观察者,监听了 `request` 事件, 默认超时时间为2分钟。 50 | 51 | 而当连接建立时,观察者 connectionListener 处理 `connection` 事件。 52 | 53 | 这时,则需要一个 HTTP parser 来解析通过 TCP 传输过来的数据: 54 | 55 | ```js 56 | // lib/_http_server.js 57 | const parsers = common.parsers; 58 | // ... 59 | 60 | function connectionListener(socket) { 61 | // ... 62 | var parser = parsers.alloc(); 63 | parser.reinitialize(HTTPParser.REQUEST); 64 | parser.socket = socket; 65 | socket.parser = parser; 66 | parser.incoming = null; 67 | // ... 68 | } 69 | ``` 70 | 71 | #### HTTP Parser 72 | 73 | 值得一提的是,parser 是从一个“池”中获取的,这个“池”使用了一种叫做 freelist的数据结构。 74 | 为了尽可能的对 parser 进行重用,并避免了不断调用构造函数的消耗,且设有数量上限(http 模块中为 1000)。 75 | 76 | HTTPParser 的实现目前由 C++绑定实现,具体参见 deps/http_parser 目录。但笔者这边拓展一下: 77 | 78 | 社区有过对 http_parser 实现性能的争论, 性能上 JS 实现的版本超越 C 的实现。 79 | 80 | 原因是多方面的: 81 | * 去调了 C++ 绑定层。 82 | * JS 实现,避免了 C 栈和 JS 堆栈的切换和参数拷贝。 83 | * V8 JIT 对热点函数的优化。 84 | 85 | 即便有上述优势,社区目前还是没有合并,处于pending 状态,结合个人和社区观点: 86 | * 并发请求会导致 garbage collection 频繁,触发GC 停顿。 87 | * 可以作为第三方模块存在。 88 | 89 | > pull request: https://github.com/nodejs/node/pull/1457/ 90 | 91 | 这里的 parser 也是基于事件的,很符合 Node.js 的核心思想。 92 | ```js 93 | // lib/_http_common.js 94 | // ... 95 | const binding = process.binding('http_parser'); 96 | const HTTPParser = binding.HTTPParser; 97 | const FreeList = require('internal/freelist').FreeList; 98 | // ... 99 | 100 | var parsers = new FreeList('parsers', 1000, function() { 101 | var parser = new HTTPParser(HTTPParser.REQUEST); 102 | // ... 103 | parser[kOnHeaders] = parserOnHeaders; 104 | parser[kOnHeadersComplete] = parserOnHeadersComplete; 105 | parser[kOnBody] = parserOnBody; 106 | parser[kOnMessageComplete] = parserOnMessageComplete; 107 | parser[kOnExecute] = null; 108 | 109 | return parser; 110 | }); 111 | exports.parsers = parsers; 112 | 113 | // lib/_http_server.js 114 | // ... 115 | 116 | function connectionListener(socket) { 117 | parser.onIncoming = parserOnIncoming; 118 | } 119 | ``` 120 | 所以一个完整的 HTTP 请求从接收到完全解析,会挨个经历 parser 上的如下事件监听器: 121 | 122 | * parserOnHeaders:不断解析推入的请求头数据。 123 | * parserOnHeadersComplete:请求头解析完毕,构造 header 对象,为请求体创建 http.IncomingMessage 实例。 124 | * parserOnBody:不断解析推入的请求体数据。 125 | * parserOnExecute:请求体解析完毕,检查解析是否报错,若报错,直接触发 clientError 事件。若请求为 CONNECT 方法,或带有 Upgrade 头,则直接触发 connect 或 upgrade 事件。 126 | * parserOnIncoming:处理具体解析完毕的请求。 127 | 128 | 前面提到的 `request`事件到底是在哪里触发的呢?回到源码 129 | 130 | ```js 131 | // lib/_http_server.js 132 | // ... 133 | 134 | function connectionListener(socket) { 135 | var outgoing = []; 136 | var incoming = []; 137 | // ... 138 | 139 | function parserOnIncoming(req, shouldKeepAlive) { 140 | incoming.push(req); 141 | // ... 142 | var res = new ServerResponse(req); 143 | 144 | if (socket._httpMessage) { 145 | outgoing.push(res); 146 | } else { 147 | res.assignSocket(socket); 148 | } 149 | 150 | res.on('finish', resOnFinish); 151 | function resOnFinish() { 152 | incoming.shift(); 153 | // ... 154 | var m = outgoing.shift(); 155 | if (m) { 156 | m.assignSocket(socket); 157 | } 158 | } 159 | // ... 160 | self.emit('request', req, res); 161 | } 162 | } 163 | ``` 164 | 165 | 我们注意到 2个队列,`incoming`和`outgoing`, 他们用于缓冲 IncomingMessage 实例和对应的 ServerResponse 实例。 166 | 通过 `IncomingMessage`实例构建相应的 `ServerResponse` 实例, 并且通过 `res.assignSocket(socket);` ,绑定了 167 | 三元组 ``。 168 | 169 | 170 | 最后,发送 request 事件,参数为req, res。回到 hello world 中,监听者拿到 req 和 res, 向 response 流中写入HTTP头 171 | 和内容发送出去。 172 | 173 | 174 | ### 总结 175 | 对象池也是内存池的一种衍生,需要在内存和性能方面折中考量。 176 | 177 | 上面只是梳理了一个主线,其他异常处理,安全等方面剖析后面的章节会一一解读。 178 | 179 | 180 | ### 参考 181 | * https://docs.google.com/document/d/1A3cxhZg2aktJeSGt0P-8KrA4WyG1c8LlPomQflaQs8s/edit 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /chapter5/chapter5-1.md: -------------------------------------------------------------------------------- 1 | 2 | ## 事件循环 3 | 4 | > "Event Loop是一个程序结构,用于等待和发送消息和事件。(a programming construct that waits for and dispatches events or messages in a program.)" 5 | 6 | ![](5fee18eegw1ewjpoxmdf5j20k80b1win.jpg) 7 | 8 | 9 | ### 事件循环 10 | 事件循环的职责,就是不断得等待事件的发生,然后将这个事件的所有处理器,以它们订阅这个事件的时间顺序,依次执行。当这个事件的所有处理器都被执行完毕之后,事件循环就会开始继续等待下一个事件的触发,不断往复。 11 | 12 | 当同时并发地处理多个请求时,以上的概念也是正确的,可以这样理解:在单个的线程中,事件处理器是一个一个按顺序执行的。 13 | 14 | 即如果某个事件绑定了两个处理器,那么第二个处理器会在第一个处理器执行完毕后,才开始执行。在这个事件的所有处理器都执行完毕之前,事件循环不会去检查是否有新的事件触发。在单个线程中,一切都是有顺序地一个一个地执行的! 15 | 16 | ### Node.js 中的事件循环 17 | 18 | Node采用V8作为JavaScript的执行引擎,同时使用libuv实现事件驱动式异步I/O。其事件循环就是采用了libuv的默认事件循环。 19 | 在src/node.cc中, 20 | ```c++ 21 | Environment* env = CreateEnvironment( 22 | node_isolate, 23 | uv_default_loop(), 24 | context, 25 | argc, 26 | argv, 27 | exec_argc, 28 | exec_argv); 29 | ``` 30 | 这段代码建立了一个node执行环境,可以看到第三行的uv_default_loop(),这是libuv库中的一个函数,它会初始化uv库本身以及其中的default_loop_struct,并返回一个指向它的指针default_loop_ptr。 之后,Node会载入执行环境并完成一些设置操作,然后启动event loop: 31 | ```c++ 32 | bool more; 33 | do { 34 | more = uv_run(env->event_loop(), UV_RUN_ONCE); 35 | if (more == false) { 36 | EmitBeforeExit(env); 37 | // Emit `beforeExit` if the loop became alive either after emitting 38 | // event, or after running some callbacks. 39 | more = uv_loop_alive(env->event_loop()); 40 | if (uv_run(env->event_loop(), UV_RUN_NOWAIT) != 0) 41 | more = true; 42 | } 43 | } while (more == true); 44 | code = EmitExit(env); 45 | RunAtExit(env); 46 | ``` 47 | 48 | more用来标识是否进行下一轮循环。 env->event_loop()会返回之前保存在env中的default_loop_ptr,uv_run函数将以指定的UV_RUN_ONCE模式启动libuv的event loop。在这种模式下,uv_run会至少处理一个事件:这意味着,如果当前事件队列中没有需要处理的I/O事件,uv_run会阻塞住,直到有I/O事件需要处理,或者下一个定时器时间到。如果当前没有I/O事件也没有定时器事件,则uv_run返回false。 49 | 50 | 接下来Node会根据more的情况决定下一步操作: 51 | 52 | - 如果more为true,则继续运行下一轮loop。 53 | 54 | - 如果more为false,说明已经没有等待处理的事件了,EmitBeforeExit(env);触发进程的'beforeExit'事件,检查并处理相应的处理函数,完成后直接跳出循环。 55 | 56 | 最后触发'exit'事件,执行相应的回调函数,Node运行结束,后面会进行一些资源释放操作。 57 | 58 | 在libuv中,event loop会在每次循环的开始更新自己的time从而实现计时功能,而I/O事件则分为两类: 59 | 60 | - Network I/O是使用系统提供的非阻塞式I/O解决方案,例如在Linux上使用epoll,windows上使用IOCP。 61 | 62 | - 文件操作和DNS操作没有(很好的)系统解决方案,因此libuv自建了线程池,在其中进行阻塞式I/O。 63 | 64 | 另外我们也可以将自定义的函数抛到线程池中运行,在运行结束后主线程会执行相应的回调函数,不过Node并没有将这一项功能加入到JavaScript中,也就是说只用原生Node是无法在JavaScript中开启新的线程进行并行执行的。 65 | 66 | ### process.nextTick 67 | ![](settimeout.jpeg) 68 | 带着这个问题,我们看看 JS 层的 nextTick 是怎么被驱动的。 69 | 70 | 在入口点 `src/node.js`, `processNextTick` 方法构建了 `process.nextTick` API。 71 | 72 | `process._tickCallback ` 作为 nextTick 的回调函数,挂到了 `process` 对象上,由 C++ 层回调使用。 73 | 74 | ```js 75 | startup.processNextTick = function() { 76 | var nextTickQueue = []; 77 | var pendingUnhandledRejections = []; 78 | var microtasksScheduled = false; 79 | 80 | // Used to run V8's micro task queue. 81 | var _runMicrotasks = {}; 82 | 83 | // *Must* match Environment::TickInfo::Fields in src/env.h. 84 | var kIndex = 0; 85 | var kLength = 1; 86 | 87 | process.nextTick = nextTick; 88 | // Needs to be accessible from beyond this scope. 89 | process._tickCallback = _tickCallback; 90 | process._tickDomainCallback = _tickDomainCallback; 91 | 92 | // This tickInfo thing is used so that the C++ code in src/node.cc 93 | // can have easy access to our nextTick state, and avoid unnecessary 94 | // calls into JS land. 95 | const tickInfo = process._setupNextTick(_tickCallback, _runMicrotasks); 96 | // 省略... 97 | } 98 | ``` 99 | 通过 `process._setupNextTick` 注册 `_tickCallback` 到 `env` 的 `tick_callback_function` 上。 100 | 101 | 102 | 在 `src/async_wrap.cc` 文件中,我们发现对其的调用如下: 103 | ```js 104 | Local AsyncWrap::MakeCallback(const Local cb, 105 | int argc, 106 | Local* argv) { 107 | // ... 108 | Environment::TickInfo* tick_info = env()->tick_info(); 109 | 110 | if (tick_info->in_tick()) { 111 | return ret; 112 | } 113 | 114 | if (tick_info->length() == 0) { 115 | env()->isolate()->RunMicrotasks(); 116 | } 117 | 118 | if (tick_info->length() == 0) { 119 | tick_info->set_index(0); 120 | return ret; 121 | } 122 | 123 | tick_info->set_in_tick(true); 124 | 125 | env()->tick_callback_function()->Call(process, 0, nullptr); 126 | 127 | tick_info->set_in_tick(false); 128 | // ... 129 | ``` 130 | 131 | 132 | 当无 `nextTick`任务时,`env()->isolate()->RunMicrotasks();`会驱动 `Promise` 任务执行。 133 | 134 | 否则会调用 `tick_callback_function` ,也就是 `_tickCallback`。 135 | 136 | 看到这里我也有个疑问,如果没有异步 IO 呢,怎么驱动呢? 137 | 138 | 我们来到 `lib/module.js`, 如下 139 | 140 | ```js 141 | // bootstrap main module. 142 | Module.runMain = function() { 143 | // Load the main module--the command line argument. 144 | Module._load(process.argv[1], null, true); 145 | // Handle any nextTicks added in the first tick of the program 146 | process._tickCallback(); 147 | }; 148 | ``` 149 | 150 | `Module._load` 加载主脚本后,就调用 `_tickCallback`, 处理第一次的 tick 了。 151 | 152 | 所以上面的疑问有了答案,`nextTick` 主要在 `uv__io_poll` 驱动。为什么说主要呢?因为还 153 | 可能在 Timer模块驱动,具体细节留给读者去研究啦。 154 | 155 | 156 | ### 总结 157 | 158 | 159 | ### 参考 160 | * http://acemood.github.io/2016/02/01/event-loop-in-javascript/ 161 | 162 | 163 | 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /chapter2/chapter2-0.md: -------------------------------------------------------------------------------- 1 | 2 | ## V8 concept 3 | ### 架构图 4 | ![](e09d7b330d9e754f7ff1282a1af55295.png) 5 | 6 | 现在 JS 引擎的执行过程大致是:源代码 --->抽象语法树 --->字节码 --->JIT--->本地代码。 7 | 8 | V8 更加直接的将抽象语法树通过 JIT 技术转换成本地代码,放弃了在字节码阶段可以进行的一些性能优化,但保证了执行速度。 9 | 在 V8 生成本地代码后,也会通过 Profiler 采集一些信息,来优化本地代码。虽然,少了生成字节码这一阶段的性能优化, 10 | 但极大减少了转换时间。 11 | 12 | > PS: TurboFan 将逐步取代 Crankshaft 13 | 14 | 在使用 v8 引擎之前,先来了解一下几个基本概念:句柄(handle),作用域(scope),上下文环境(可以简单地理解为运行环境)。 15 | 16 | ### Isolate 17 | > An isolate is a VM instance with its own heap. It represents an isolated instance of the V8 engine. 18 | > V8 isolates have completely separate states. Objects from one isolate must not be used in other isolates. 19 | 20 | 一个 Isolate 是一个独立的虚拟机。对应一个或多个线程。但同一时刻 只能被一个线程进入。所有的 Isolate 彼此之间是完全隔离的, 它们不能够有任何共享的资源。如果不显式创建 Isolate, 会自动创建一个默认的 Isolate。 21 | 22 | 后面提到的 Context、Scope、Handle 的概念都是一个 Isolate 内部的, 如下图: 23 | ![](Context.png) 24 | 25 | ### Handle 概念 26 | 在 V8 中,内存分配都是在 V8 的 Heap 中进行分配的,JavaScript 的值和对象也都存放在 V8 的 Heap 中。这个 Heap 由 V8 独立的去维护,失去引 27 | 用的对象将会被 V8 的 GC 掉并可以重新分配给其他对象。而 Handle 即是对 Heap 中对象的引用。V8 为了对内存分配进行管理,GC 需要对 V8 中的 28 | 所有对象进行跟踪,而对象都是用 Handle 方式引用的,所以 GC 需要对 Handle 进行管理,这样 GC 就能知道 Heap 中一个对象的引用情况,当一个对象的 Handle 引用发生改变的时候,GC 即可对该对象进行回收或者移动。因此,V8 编程中必须使用 Handle 去引用一个对象,而不是直接通过 C 29 | ++ 的方式去获取对象的引用,直接通过 C++ 的方式去引用一个对象,会使得该对象无法被 V8 管理。 30 | 31 | Handle 分为 Local 和 Persistent 两种。 32 | 33 | 从字面上就能知道,Local 是局部的,它同时被 HandleScope 进行管理。 34 | persistent,类似与全局的,不受 HandleScope 的管理,其作用域可以延伸到不同的函数,而 Local 是局部的,作用域比较小。 35 | Persistent Handle 对象需要 Persistent::New, Persistent::Dispose 配对使用,类似于 C++ 中 new 和 delete。 36 | 37 | Persistent::MakeWeak 可以用来弱化一个 Persistent Handle,如果一个对象的唯一引用 Handle 是一个 Persistent,则可以使用 MakeWeak 方法来弱化该引用,该方法可以触发 GC 对被引用对象的回收。 38 | 39 | ### Scope 40 | 从概念上理解,作用域可以看成是一个句柄的容器,在一个作用域里面可以有很多很多个句柄(也就是说,一个 scope 里面可以包含很多很多个 41 | v8 引擎相关的对象),句柄指向的对象是可以一个一个单独地释放的,但是很多时候(真正开始写业务代码的时候),一个一个地释放句柄过于 42 | 繁琐,取而代之的是,可以释放一个 scope,那么包含在这个 scope 中的所有 handle 就都会被统一释放掉了。 43 | 44 | Scope 在 v8.h 中有这么几个:HandleScope,Context::Scope。 45 | 46 | HandleScope 是用来管理 Handle 的,而 Context::Scope 仅仅用来管理 Context 对象。 47 | 48 | 代码像下面这样: 49 | ```c++ 50 | // 在此函数中的 Handle 都会被 handleScope 管理 51 | HandleScope handleScope; 52 | // 创建一个 js 执行环境 Context 53 | Handle context = Context::New(); 54 | Context::Scope contextScope(context); 55 | // 其它代码 56 | ``` 57 | 一般情况下,函数的开始部分都放一个 HandleScope,这样此函数中的 Handle 就不需要再理会释放资源了。 58 | 而 Context::Scope 仅仅做了:在构造中调用 context->Enter(),而在析构函数中调用 context->Leave()。 59 | 60 | 61 | ### Context 概念  62 | 从概念上讲,这个上下文环境也可以理解为运行环境。在执行 javascript 脚本的时候,总要有一些环境变量或者全局函数。 63 | 我们如果要在自己的 c++ 代码中嵌入 v8 引擎,自然希望提供一些 c++ 编写的函数或者模块,让其他用户从脚本中直接调用,这样才会体现出 javascript 的强大。 64 | 我们可以用 c++ 编写全局函数或者类,让其他人通过 javascript 进行调用,这样,就无形中扩展了 javascript 的功能。  65 | 66 | Context 可以嵌套,即当前函数有一个 Context,调用其它函数时如果又有一个 Context,则在被调用的函数中 javascript 是以最近的 67 | Context 为准的,当退出这个函数时,又恢复到了原来的 Context。 68 | 69 | 我们可以往不同的 Context 里 “导入” 不同的全局变量及函数,互不影响。据说设计 Context 的最初目的是为了让浏览器在解析 HTML 的 iframe 70 | 时,让每个 iframe 都有独立的 javascript 执行环境,即一个 iframe 对应一个 Context。 71 | 72 | #### 同作用域下不同的执行上下文 73 | 74 | ![](c3ad9f4a15cb36af631932a52dec3e96.png) 75 | 76 | ### 关系 77 | ![](1354452360_3578.png) 78 | 79 | 从这张图可以比较清楚的看到 Handle,HandleScope,以及被 Handle 引用的对象之间的关系。从图中可以看到,V8 的对象都是存在 V8 的 Heap 中,而 Handle 则是对该对象的引用。 80 | 81 | ### 垃圾回收 82 | 垃圾回收器是一把十足的双刃剑。好处是简化程序的内存管理,内存管理无需程序员来操作,由此也减少了长时间运转的程序的内存泄漏。然而无法预期的停顿,影响了交互体验。 83 | 84 | 85 | #### 基本概念 86 | 垃圾回收器解决基本问题就是,识别需要回收的内存。一旦辨别完毕,这些内存区域即可在未来的分配中重用,或者是返还给操作系统。一个对象当它不是处于活跃状态的时候它就死了。一个对象处于活跃状态,当且仅当它被一个根对象或另一个活跃对象指向。根对象被定义为处于活跃状态,是浏览器或 V8 所引用的对象。比如说全局对象属于根对象,因为它们始终可被访问;浏览器对象,如 DOM 元素,也属于根对象,尽管在某些场合下它们只是弱引用。 87 | 88 | 89 | #### 堆的构成 90 | 在深入研究垃圾回收器的内部工作原理之前,首先来看看堆是如何组织的。V8 将堆分为了几个不同的区域: 91 | ![2015-11-17 下午 3.09.08](http://alinode-assets.oss-cn-hangzhou.aliyuncs.com/2336435d-bdd4-4d86-8e28-b253e7d7ad6a.png) 92 | 93 | **新生区**:大多数对象开始时被分配在这里。新生区是一个很小的区域,垃圾回收在这个区域非常频繁,与其他区域相独立。 94 | 95 | **老生指针区**:包含大多数可能存在指向其他对象的指针的对象。大多数在新生区存活一段时间之后的对象都会被挪到这里。 96 | 97 | **老生数据区**:这里存放只包含原始数据的对象(这些对象没有指向其他对象的指针)。字符串、封箱的数字以及未封箱的双精度数字数组,在新生区经历一次 Scavenge 后会被移动到这里。 98 | 99 | **大对象区**:这里存放体积超过 1MB 大小的对象。每个对象有自己 mmap 产生的内存。垃圾回收器从不移动大对象。 100 | 101 | **Code 区**:代码对象,也就是包含 JIT 之后指令的对象,会被分配到这里。 102 | 103 | **Cell 区、属性 Cell 区、Map 区**:这些区域存放 Cell、属性 Cell 和 Map,每个区域因为都是存放相同大小的元素,因此内存结构很简单。 104 | 105 | > 如上图:在 node-v4.x 之后,区域进行了合并为:新生区,老生区,大对象区,Map 区,Code 区 106 | 107 | 有了这些背景知识,我们可以来深入垃圾回收器了。 108 | 109 | #### 识别指针 110 | 垃圾回收器面临的第一个问题是,如何才能在堆中区分指针和数据,因为指针指向着活跃的对象。大多数垃圾回收算法会将对象在内存中挪动(以便减少内存碎片,使内存紧凑),因此即使不区分指针和数据,我们也常常需要对指针进行改写。 111 | V8 采用了标记指针法:这种方法需要在每个指针的末位预留一位来标记这个字代表的是指针或数据。 112 | 113 | 114 | #### 对象的晋升 115 | 当一个对象经过多次新生代的清理依旧幸存,这说明它的生存周期较长,也就会被移动到老生代,这称为对象的晋升。具体移动的标准有两种: 116 | - 对象从 From 空间复制到 To 空间时,会检查它的内存地址来判断这个对象是否已经活过一次新生代的清理,如果是,则复制到老生代中,否则复制到 To 空间中 117 | - 对象从 From 空间复制到 To 空间时,如果 To 空间已经被使用了超过 25%,那么这个对象直接被复制到老生代。 118 | 119 | 120 | #### 写屏障 121 | 如果新生区中某个对象,只有一个指向它的指针,而这个指针恰好是在老生区的对象当中,我们如何才能知道新生区中那个对象是活跃的呢? 为了解决这个问题,实际上在写缓冲区中有一个列表 `store-buffer{.cc,.h,-inl.h}`,列表中记录了所有老生区对象指向新生区的情况。新对象诞生的时候,并不会有指向它的指针,而当有老生区中的对象出现指向新生区对象的指针时,我们便记录下来这样的跨区指向。由于这种记录行为总是发生在写操作时,它被称为写屏障. 122 | 123 | #### 垃圾回收三部曲 124 | `Stop-the-World` 的 GC 包括三个主要步骤: 125 | 1. 枚举根节点引用; 126 | 2. 发现并标记活对象; 127 | 3. 垃圾内存清理 128 | 129 | 分代回收在 V8 中分为 `Scavenge`, `Mark-Sweep`。 130 | * `Scavenge`: 当分配指针达到了新生区的末尾,就会有一次清理。 131 | * `Mark-Sweep`: 对于活跃超过 2 个小周期的对象,则需将其移动至老生区, 当老生区有足够多的对象时才会触发。 132 | 133 | ### 总结 134 | 135 | 如果你还想了解更多垃圾回收上的东西,我建议你读读 Richard Jones 和 Rafael Lins 写的《Garbage Collection》,这是一个绝好的参考,涵盖了大量你需要了解的内容。你可能还对《Garbage First Garbage-Collection》感兴趣,这是一篇描述 JVM 所使用的垃圾回收算法的论文。 136 | 137 | 138 | ### 参考 139 | * https://developers.google.com/v8/get_started 140 | * https://developers.google.com/v8/embed 141 | * http://newhtml.net/v8-garbage-collection/ 142 | -------------------------------------------------------------------------------- /chapter10/chapter10-2.md: -------------------------------------------------------------------------------- 1 | ## HTTP 2/2 2 | 3 | http 模块提供了两个函数 http.request 和 http.get,功能是作为客户端向 HTTP服务器发起请求。 4 | 5 | 这通常来实现自己的爬虫程序, 笔者自己写的一个爬取知乎的一个例子:https://github.com/yjhjstz/iZhihu 6 | 7 | 8 | ### GET 例子 9 | ```js 10 | const http = require("http") 11 | http.get('http://www.baidu.com', (res) => { 12 | console.log(`Got response: ${res.statusCode}`); 13 | // consume response body 14 | res.resume(); 15 | }).on('error', (e) => { 16 | console.log(`Got error: ${e.message}`); 17 | }); 18 | ``` 19 | 上面的程序会返回一个200的状态码! 20 | 21 | ### HTTP Client 22 | Node.js 中,http.get 通过创建一个 `ClientRequest`的对象,建立与服务端的连接通信。 23 | ```js 24 | // lib/_http_client.js 25 | function ClientRequest(options, cb) { 26 | var self = this; 27 | OutgoingMessage.call(self); 28 | 29 | // ... 30 | const defaultPort = options.defaultPort || 31 | self.agent && self.agent.defaultPort; 32 | 33 | var port = options.port = options.port || defaultPort || 80; 34 | var host = options.host = options.hostname || options.host || 'localhost'; 35 | 36 | if (options.setHost === undefined) { 37 | var setHost = true; 38 | } 39 | 40 | self.socketPath = options.socketPath; 41 | 42 | var method = self.method = (options.method || 'GET').toUpperCase(); 43 | if (!common._checkIsHttpToken(method)) { 44 | throw new TypeError('Method must be a valid HTTP token'); 45 | } 46 | self.path = options.path || '/'; 47 | if (cb) { 48 | self.once('response', cb); 49 | } 50 | 51 | // ... 52 | 53 | var called = false; 54 | if (self.socketPath) { 55 | // ... 56 | } else if (self.agent) { 57 | // ... 58 | } else { 59 | // No agent, default to Connection:close. 60 | self._last = true; 61 | self.shouldKeepAlive = false; 62 | if (typeof options.createConnection === 'function') { 63 | const newSocket = options.createConnection(options, oncreate); 64 | if (newSocket && !called) { 65 | called = true; 66 | self.onSocket(newSocket); 67 | } else { 68 | return; 69 | } 70 | } else { 71 | debug('CLIENT use net.createConnection', options); 72 | self.onSocket(net.createConnection(options)); 73 | } 74 | } 75 | 76 | function oncreate(err, socket) { 77 | // ... 78 | } 79 | 80 | self._deferToConnect(null, null, function() { 81 | self._flush(); 82 | self = null; 83 | }); 84 | } 85 | 86 | util.inherits(ClientRequest, OutgoingMessage); 87 | ``` 88 | callback 通过 `self.once('response', cb);`, 监听了 response 事件。之后如果没有设置代理服务,则默认使用 89 | net 模块创建与服务器的连接。那么 response 事件是哪里发送的呢? 90 | 91 | 下面我们看到比较重要的 `onSocket` 函数。 92 | ```js 93 | ClientRequest.prototype.onSocket = function(socket) { 94 | process.nextTick(onSocketNT, this, socket); 95 | }; 96 | 97 | function onSocketNT(req, socket) { 98 | if (req.aborted) { 99 | // If we were aborted while waiting for a socket, skip the whole thing. 100 | socket.emit('free'); 101 | } else { 102 | tickOnSocket(req, socket); 103 | } 104 | } 105 | ``` 106 | 107 | 这边 onSocket 必须是一个异步函数,大家可以仔细体会下! 同时 onSocketNT 会异常做了处理,当请求失败时,则发送 free 事件。 108 | 否则来到 `tickOnSocket`。 109 | 110 | ```js 111 | function tickOnSocket(req, socket) { 112 | var parser = parsers.alloc(); 113 | req.socket = socket; 114 | req.connection = socket; 115 | parser.reinitialize(HTTPParser.RESPONSE); 116 | parser.socket = socket; 117 | parser.incoming = null; 118 | parser.outgoing = req; 119 | req.parser = parser; 120 | 121 | socket.parser = parser; 122 | socket._httpMessage = req; 123 | 124 | // Setup "drain" propagation. 125 | httpSocketSetup(socket); 126 | 127 | // Propagate headers limit from request object to parser 128 | if (typeof req.maxHeadersCount === 'number') { 129 | parser.maxHeaderPairs = req.maxHeadersCount << 1; 130 | } else { 131 | // Set default value because parser may be reused from FreeList 132 | parser.maxHeaderPairs = 2000; 133 | } 134 | 135 | parser.onIncoming = parserOnIncomingClient; 136 | socket.removeListener('error', freeSocketErrorListener); 137 | socket.on('error', socketErrorListener); 138 | socket.on('data', socketOnData); 139 | socket.on('end', socketOnEnd); 140 | socket.on('close', socketCloseListener); 141 | req.emit('socket', socket); 142 | } 143 | ``` 144 | 145 | 同 HTTP Server 类似,从池中申请一个解析器,用于解析 HTTP 协议, 到这一步说明连接已经建立,所以重新设置 error 事件的回调。 146 | 147 | 同时设置 数据回调等,然后发送 `socket`事件, 来到 `parserOnIncomingClient`. 148 | 149 | ```js 150 | // client 151 | function parserOnIncomingClient(res, shouldKeepAlive) { 152 | var socket = this.socket; 153 | var req = socket._httpMessage; 154 | 155 | 156 | // propagate "domain" setting... 157 | if (req.domain && !res.domain) { 158 | debug('setting "res.domain"'); 159 | res.domain = req.domain; 160 | } 161 | 162 | debug('AGENT incoming response!'); 163 | 164 | if (req.res) { 165 | // We already have a response object, this means the server 166 | // sent a double response. 167 | socket.destroy(); 168 | return; 169 | } 170 | req.res = res; 171 | 172 | var isHeadResponse = req.method === 'HEAD'; 173 | 174 | // ... 175 | req.res = res; 176 | res.req = req; 177 | 178 | // add our listener first, so that we guarantee socket cleanup 179 | res.on('end', responseOnEnd); 180 | var handled = req.emit('response', res); 181 | 182 | // If the user did not listen for the 'response' event, then they 183 | // can't possibly read the data, so we ._dump() it into the void 184 | // so that the socket doesn't hang there in a paused state. 185 | if (!handled) 186 | res._dump(); 187 | 188 | return isHeadResponse; 189 | } 190 | ``` 191 | 192 | 在这里发送 response 事件,参数对象 `res` 上也挂上了 `req` 对象。这样 req 和 res 就相互引用。 193 | 194 | 用户的 callback 终于得到回调。 195 | 196 | ### 总结 197 | 上面只是梳理了一个http client 主线, 实际我们很少使用该模块,而是使用第三方的 npm 包,比如 198 | * urllib (轻量级) 199 | * request 200 | 201 | 202 | ### 参考 203 | *https://nodejs.org/api/http.html 204 | *https://github.com/nodejs/node/blob/master/lib/_http_client.js 205 | *https://github.com/nodejs/http-parser 206 | 207 | 208 | -------------------------------------------------------------------------------- /chapter2/chapter2-1.md: -------------------------------------------------------------------------------- 1 | ## C++ 和 JS 交互 2 | 本章主要来讲讲如何通过 V8 来实现 JS 调用 C++。JS 调用 C++,分为 JS 调用 C++ 函数(全局),和调用 C++ 类。 3 | 4 | 5 | ### 数据及模板 6 | 由于 C++ 原生数据类型与 JavaScript 中数据类型有很大差异,因此 V8 提供了 Value 类,从 JavaScript 到 C++,从 C++ 到 JavaScript 都会用到这个类及其子类,比如: 7 | ```c++ 8 | Handle Add(const Arguments& args){ 9 | int a = args[0]->Uint32Value(); 10 | int b = args[1]->Uint32Value(); 11 | 12 | return Integer::New(a+b); 13 | } 14 | ``` 15 | Integer 即为 Value 的一个子类。 16 | 17 | V8 中,有两个模板 (Template) 类 (并非 C++ 中的模板类): 18 | - 对象模板 (ObjectTemplate) 19 | - 函数模板 (FunctionTemplate) 20 | 这两个模板类用以定义 JavaScript 对象和 JavaScript 函数。我们在后续的小节部分将会接触到模板类的实例。通过使用 21 | ObjectTemplate,可以将 C++ 中的对象暴露给脚本环境,类似的,FunctionTemplate 用以将 C++ 22 | 函数暴露给脚本环境,以供脚本使用。 23 | 24 | 25 | ### JS 使用 C++ 变量 26 | 在 JavaScript 与 V8 间共享变量事实上是非常容易的,基本模板如下: 27 | ```c++ 28 | static char sname[512] = {0}; 29 | 30 | static Handle NameGetter(Local name, const AccessorInfo& info) { 31 | return String::New((char*)&sname,strlen((char*)&sname)); 32 | } 33 | 34 | static void NameSetter(Local name, Local value, const AccessorInfo& info) { 35 | Local str = value->ToString(); 36 | str->WriteAscii((char*)&sname); 37 | } 38 | ``` 39 | 定义了 NameGetter, NameSetter 之后,在 main 函数中,将其注册在 global 上: 40 | ```c++ 41 | // Create a template for the global object. 42 | Handle global = ObjectTemplate::New(); 43 | //public the name variable to script 44 | global->SetAccessor(String::New("name"), NameGetter, NameSetter); 45 | 46 | ``` 47 | 48 | ### JS 调用 C++ 函数 49 | 在 JavaScript 中调用 C++ 函数是脚本化最常见的方式,通过使用 C++ 函数,可以极大程度的增强 JavaScript 脚本的能力,如文件读写,网络 / 数据库访问,图形 / 图像处理等等,类似于 JAVA 的 jni 技术。 50 | 51 | 在 C++ 代码中,定义以下原型的函数: 52 | ```c++ 53 | Handle func(const Arguments& args){//return something} 54 | ``` 55 | 然后,再将其公开给脚本: 56 | `global->Set(String::New("func"),FunctionTemplate::New(func));` 57 | 58 | ### JS 使用 C++ 类 59 | 如果从面向对象的视角来分析,最合理的方式是将 C++ 类公开给 JavaScript,这样可以将 JavaScript 60 | 内置的对象数量大大增加,从而尽可能少的使用宿主语言,而更大的利用动态语言的灵活性和扩展性。事实上,C++ 61 | 语言概念众多,内容繁复,学习曲线较 JavaScript 远为陡峭。最好的应用场景是:既有脚本语言的灵活性, 62 | 又有 C/C++ 等系统语言的效率。使用 V8 引擎,可以很方便的将 C++ 类” 包装” 成可供 JavaScript 使用的资源。 63 | 64 | 我们这里举一个较为简单的例子,定义一个 Person 类,然后将这个类包装并暴露给 JavaScript 脚本,在脚本中新建 Person 类的对象,使用 Person 对象的方法。 65 | 首先,我们在 C++ 中定义好类 Person: 66 | ```c++ 67 | class Person { 68 | private: 69 | unsigned int age; 70 | char name[512]; 71 | 72 | public: 73 | Person(unsigned int age, char *name) { 74 | this->age = age; 75 | strncpy(this->name, name, sizeof(this->name)); 76 | } 77 | 78 | unsigned int getAge() { 79 | return this->age; 80 | } 81 | 82 | void setAge(unsigned int nage) { 83 | this->age = nage; 84 | } 85 | 86 | char *getName() { 87 | return this->name; 88 | } 89 | 90 | void setName(char *nname) { 91 | strncpy(this->name, nname, sizeof(this->name)); 92 | } 93 | }; 94 | ``` 95 | Person 类的结构很简单,只包含两个字段 age 和 name,并定义了各自的 getter/setter. 然后我们来定义构造器的包装: 96 | ```c++ 97 | Handle PersonConstructor(const Arguments& args){ 98 | Handle object = args.This(); 99 | HandleScope handle_scope; 100 | int age = args[0]->Uint32Value(); 101 | 102 | String::Utf8Value str(args[1]); 103 | char* name = ToCString(str); 104 | 105 | Person *person = new Person(age, name); 106 | object->SetInternalField(0, External::New(person)); 107 | return object; 108 | } 109 | ``` 110 | 从函数原型上可以看出,构造器的包装与上一小节中,函数的包装是一致的,因为构造函数在 V8 看来,也是一个函数。需要注意的是, 111 | 从 args 中获取参数并转换为合适的类型之后,我们根据此参数来调用 Person 类实际的构造函数,并将其设置在 object 112 | 的内部字段中。紧接着,我们需要包装 Person 类的 getter/setter: 113 | ```c++ 114 | Handle PersonGetAge(const Arguments& args){ 115 | Local self = args.Holder(); 116 | Local wrap = Local::Cast(self->GetInternalField(0)); 117 | 118 | void *ptr = wrap->Value(); 119 | 120 | return Integer::New(static_cast(ptr)->getAge()); 121 | } 122 | 123 | Handle PersonSetAge(const Arguments& args) { 124 | Local self = args.Holder(); 125 | Local wrap = Local::Cast(self->GetInternalField(0)); 126 | 127 | void* ptr = wrap->Value(); 128 | 129 | static_cast(ptr)->setAge(args[0]->Uint32Value()); 130 | return Undefined(); 131 | } 132 | ``` 133 | 而 getName 和 setName 的与上例类似。在对函数包装完成之后,需要将 Person 类暴露给脚本环境: 134 | 首先,创建一个新的函数模板,将其与字符串”Person” 绑定,并放入 global: 135 | ```c++ 136 | Handle person_template = FunctionTemplate::New(PersonConstructor); 137 | person_template->SetClassName(String::New("Person")); 138 | global->Set(String::New("Person"), person_template); 139 | ``` 140 | 然后定义原型模板: 141 | ```c++ 142 | Handle person_proto = person_template->PrototypeTemplate(); 143 | 144 | person_proto->Set("getAge", FunctionTemplate::New(PersonGetAge)); 145 | person_proto->Set("setAge", FunctionTemplate::New(PersonSetAge)); 146 | 147 | person_proto->Set("getName", FunctionTemplate::New(PersonGetName)); 148 | person_proto->Set("setName", FunctionTemplate::New(PersonSetName)); 149 | ``` 150 | 最后设置实例模板: 151 | ```c++ 152 | Handle person_inst = person_template->InstanceTemplate(); 153 | person_inst->SetInternalFieldCount(1); 154 | ``` 155 | 156 | ### C++ 调用 JS 函数 157 | 我们直接看下 src/timer_wrap.cc 的例子,V8 编译执行 timer.js, 构造了 Timer 对象。 158 | 159 | ```c++ 160 | static void OnTimeout(uv_timer_t* handle) { 161 | TimerWrap* wrap = static_cast(handle->data); 162 | Environment* env = wrap->env(); 163 | HandleScope handle_scope(env->isolate()); 164 | Context::Scope context_scope(env->context()); 165 | wrap->MakeCallback(kOnTimeout, 0, nullptr); 166 | } 167 | 168 | inline v8::Local AsyncWrap::MakeCallback(uint32_t index, int argc, v8::Local* argv) { 169 | v8::Local cb_v = object()->Get(index); 170 | CHECK(cb_v->IsFunction()); 171 | return MakeCallback(cb_v.As(), argc, argv); 172 | } 173 | ``` 174 | `TimerWrap` 对象通过数组的索引寻址,找到 Timer 对象索引 0 的对象,而对其赋值的是在 lib/timer.js 里面的 175 | `list._timer[kOnTimeout] = listOnTimeout;` 。这边找到的对象是个 `Function`, 176 | 后面忽略 domains 异常处理等,就是简单的调用 Function 对象的 Call 方法, 并且传人上文提到的 Context 和参数。 177 | 178 | `Local ret = callback->Call(recv, argc, argv);` 179 | 180 | 这就实现了 C++ 对 JS 函数的调用。 181 | 182 | ### 总结 183 | 184 | 185 | ### 参考 186 | - [1]. https://www.ibm.com/developerworks/cn/opensource/os-cn-v8engine/ 187 | - [2]. https://developers.google.com/v8/embed 188 | -------------------------------------------------------------------------------- /chapter4/chapter4-1.md: -------------------------------------------------------------------------------- 1 | 2 | ## cluster 3 | 4 | 5 | ### 背景 6 | 众所周知,Node.js是单线程的,一个单独的Node.js进程无法充分利用多核。Node.js从v0.8开始,新增cluster模块,让Node.js开发Web服务时,很方便的做到充分利用多核机器。 7 | 8 | 充分利用多核的思路是:使用多个进程处理业务。cluster模块封装了创建子进程、进程间通信、服务负载均衡。有两类进程,master进程和worker进程,master进程是主控进程,它负责启动worker进程,worker是子进程、干活的进程。 9 | 10 | ### 竞争模型 11 | 12 | 最初的 Node.js 多进程模型就是这样实现的,master 进程创建 socket,绑定到某个地址以及端口后,自身不调用 listen 来监听连接以及 accept 连接,而是将该 socket 的 fd 传递到 fork 出来的 worker 进程,worker 接收到 fd 后再调用 listen,accept 新的连接。但实际一个新到来的连接最终只能被某一个 worker 进程 accept 再做处理,至于是哪个 worker 能够 accept 到,开发者完全无法预知以及干预。这势必就导致了当一个新连接到来时,多个 worker 进程会产生竞争,最终由胜出的 worker 获取连接。 13 | 14 | 相信到这里大家也应该知道这种多进程模型比较明显的问题了 15 | 16 | * 多个进程之间会竞争 accept 一个连接,产生惊群现象,效率比较低。 17 | * 由于无法控制一个新的连接由哪个进程来处理,必然导致各 worker 进程之间的负载非常不均衡。 18 | 19 | 20 | 21 | ### round-robin (轮询) 22 | 23 | 上面的多进程模型存在诸多问题,于是就出现了基于round-robin的另一种模型。 24 | 主要思路是master进程创建socket,绑定好地址以及端口后再进行监听。该socket的fd不传递到各个worker进程,当master进程获取到新的连接时,再决定将accept到的客户端socket fd传递给指定的worker处理。我这里使用了指定, 所以如何传递以及传递给哪个worker完全是可控的,round-robin只是其中的某种算法而已,当然可以换成其他的。 25 | 26 | Master是如何将接收的请求传递至worker中进行处理然后响应的? 27 | 28 | Cluster 模块通过监听该内部TCP服务器的connection事件,在监听器函数里,有负载均衡地挑选出一个worker,向其发送newconn内部消息(消息体对象中包含cmd: 'NODE_CLUSTER'属性)以及一个客户端句柄(即connection事件处理函数的第二个参数),相关代码如下: 29 | ```js 30 | // lib/cluster.js 31 | // ... 32 | 33 | function RoundRobinHandle(key, address, port, addressType, backlog, fd) { 34 | // ... 35 | this.server = net.createServer(assert.fail); 36 | // ... 37 | 38 | var self = this; 39 | this.server.once('listening', function() { 40 | // ... 41 | self.handle.onconnection = self.distribute.bind(self); 42 | }); 43 | } 44 | 45 | RoundRobinHandle.prototype.distribute = function(err, handle) { 46 | this.handles.push(handle); 47 | var worker = this.free.shift(); 48 | if (worker) this.handoff(worker); 49 | }; 50 | 51 | RoundRobinHandle.prototype.handoff = function(worker) { 52 | // ... 53 | var message = { act: 'newconn', key: this.key }; 54 | var self = this; 55 | sendHelper(worker.process, message, handle, function(reply) { 56 | // ... 57 | }); 58 | }; 59 | ``` 60 | Worker进程在接收到了newconn内部消息后,根据传递过来的句柄,调用实际的业务逻辑处理并返回: 61 | ```js 62 | // lib/cluster.js 63 | // ... 64 | 65 | // 该方法会在Node.js初始化时由 src/node.js 调用 66 | cluster._setupWorker = function() { 67 | // ... 68 | process.on('internalMessage', internal(worker, onmessage)); 69 | 70 | // ... 71 | function onmessage(message, handle) { 72 | if (message.act === 'newconn') 73 | onconnection(message, handle); 74 | // ... 75 | } 76 | }; 77 | 78 | function onconnection(message, handle) { 79 | // ... 80 | var accepted = server !== undefined; 81 | // ... 82 | if (accepted) server.onconnection(0, handle); 83 | } 84 | ``` 85 | 至此,也总结一下: 86 | 87 | * 所有请求先统一经过内部TCP服务器。 88 | * 在内部TCP服务器的请求处理逻辑中,有负载均衡地挑选出一个worker进程,向其发送一个newconn内部消息,随消息发送客户端句柄。 89 | * Worker进程接收到此内部消息,根据客户端句柄创建net.Socket实例,执行具体业务逻辑,返回。 90 | 91 | ### listen 端口复用 92 | 为了得到这个问题的解答,我们先从worker进程的初始化看起,master进程在fork工作进程时,会为其附上环境变量NODE_UNIQUE_ID,是一个从零开始的递增数: 93 | 94 | ```js 95 | // lib/cluster.js 96 | // ... 97 | 98 | function createWorkerProcess(id, env) { 99 | // ... 100 | workerEnv.NODE_UNIQUE_ID = '' + id; 101 | 102 | // ... 103 | return fork(cluster.settings.exec, cluster.settings.args, { 104 | env: workerEnv, 105 | silent: cluster.settings.silent, 106 | execArgv: execArgv, 107 | gid: cluster.settings.gid, 108 | uid: cluster.settings.uid 109 | }); 110 | } 111 | ``` 112 | 随后Node.js在初始化时,会根据该环境变量,来判断该进程是否为cluster模块fork出的工作进程,若是,则执行workerInit()函数来初始化环境,否则执行masterInit()函数。 113 | 114 | 在workerInit()函数中,定义了cluster._getServer方法,这个方法在任何net.Server实例的listen方法中,会被调用: 115 | 116 | ```js 117 | // lib/net.js 118 | // ... 119 | 120 | function listen(self, address, port, addressType, backlog, fd, exclusive) { 121 | exclusive = !!exclusive; 122 | 123 | if (!cluster) cluster = require('cluster'); 124 | 125 | if (cluster.isMaster || exclusive) { 126 | self._listen2(address, port, addressType, backlog, fd); 127 | return; 128 | } 129 | 130 | cluster._getServer(self, { 131 | address: address, 132 | port: port, 133 | addressType: addressType, 134 | fd: fd, 135 | flags: 0 136 | }, cb); 137 | 138 | function cb(err, handle) { 139 | // ... 140 | 141 | self._handle = handle; 142 | self._listen2(address, port, addressType, backlog, fd); 143 | } 144 | } 145 | ``` 146 | 你可能已经猜到,答案就在这个cluster._getServer函数的代码中。它主要干了两件事: 147 | * 向master进程注册该worker,若master进程是第一次接收到监听此端口/描述符下的worker,则起一个内部TCP服务器,来承担监听该端口/描述符的职责,随后在master中记录下该worker。 148 | * Hack掉worker进程中的net.Server实例的listen方法里监听端口/描述符的部分,使其不再承担该职责。 149 | 150 | 对于第一件事,由于master在接收,传递请求给worker时,会符合一定的负载均衡规则(在非Windows平台下默认为轮询),这些逻辑被封装在RoundRobinHandle类中。故,初始化内部TCP服务器等操作也在此处: 151 | ```js 152 | // lib/cluster.js 153 | // ... 154 | 155 | function RoundRobinHandle(key, address, port, addressType, backlog, fd) { 156 | // ... 157 | this.handles = []; 158 | this.handle = null; 159 | this.server = net.createServer(assert.fail); 160 | 161 | if (fd >= 0) 162 | this.server.listen({ fd: fd }); 163 | else if (port >= 0) 164 | this.server.listen(port, address); 165 | else 166 | this.server.listen(address); // UNIX socket path. 167 | 168 | /// ... 169 | } 170 | ``` 171 | 对于第二件事,由于net.Server实例的listen方法,最终会调用自身_handle属性下listen方法来完成监听动作,故在代码中修改之: 172 | ```js 173 | // lib/cluster.js 174 | // ... 175 | 176 | function rr(message, cb) { 177 | // ... 178 | // 此处的listen函数不再做任何监听动作 179 | function listen(backlog) { 180 | return 0; 181 | } 182 | 183 | function close() { 184 | // ... 185 | } 186 | function ref() {} 187 | function unref() {} 188 | 189 | var handle = { 190 | close: close, 191 | listen: listen, 192 | ref: ref, 193 | unref: unref, 194 | }; 195 | // ... 196 | handles[key] = handle; 197 | cb(0, handle); // 传入这个cb中的handle将会被赋值给net.Server实例中的_handle属性 198 | } 199 | 200 | // lib/net.js 201 | // ... 202 | function listen(self, address, port, addressType, backlog, fd, exclusive) { 203 | // ... 204 | 205 | if (cluster.isMaster || exclusive) { 206 | self._listen2(address, port, addressType, backlog, fd); 207 | return; // 仅在worker环境下改变 208 | } 209 | 210 | cluster._getServer(self, { 211 | address: address, 212 | port: port, 213 | addressType: addressType, 214 | fd: fd, 215 | flags: 0 216 | }, cb); 217 | 218 | function cb(err, handle) { 219 | // ... 220 | self._handle = handle; 221 | // ... 222 | } 223 | } 224 | ``` 225 | 226 | 至此,总结下: 227 | * 端口仅由master进程中的内部TCP服务器监听了一次。 228 | * 不会出现端口被重复监听报错,是由于,worker进程中,最后执行监听端口操作的方法,已被cluster模块主动覆盖。 229 | 230 | 231 | 232 | ### 总结 233 | 234 | 235 | ### 参考 236 | -------------------------------------------------------------------------------- /chapter14/chapter14-0.md: -------------------------------------------------------------------------------- 1 | # V8 bailout reasons 2 | v8 bailout reasons 的例子, 解释和建议. 帮助`alinode`的用户根据 CPU-Profiler 的提示进行优化。 3 | 4 | ## 索引 5 | ### [Bailout reasons](#bailout-reasons-1) 6 | 7 | * [Assignment to parameter in arguments object](#assignment-to-parameter-in-arguments-object) 8 | * [Bad value context for arguments value](#bad-value-context-for-arguments-value) 9 | * [ForInStatement with non-local each variable](#forinstatement-with-non-local-each-variable) 10 | * [Object literal with complex property](#object-literal-with-complex-property) 11 | * [ForInStatement is not fast case](#forinstatement-is-not-fast-case) 12 | * [Reference to a variable which requires dynamic lookup](#reference-to-a-variable-which-requires-dynamic-lookup) 13 | * [TryCatchStatement](#trycatchstatement) 14 | * [TryFinallyStatement](#tryfinallystatement) 15 | * [Unsupported phi use of arguments](#unsupported-phi-use-of-arguments) 16 | * [Yield](#yield) 17 | 18 | 19 | ## Bailout reasons 20 | ### Assignment to parameter in arguments object 21 | 22 | * 简单例子 23 | 24 | ```js 25 | // sloppy mode only 26 | function test(a) { 27 | if (arguments.length < 2) { 28 | a = 0; 29 | } 30 | } 31 | ``` 32 | 33 | * Why 34 | * 只会在函数中重新赋值参数发生。 35 | 36 | * Advices 37 | * 你不能给变量 a 重新赋值. 38 | * 最好使用 strict mode . 39 | * V8 最新的 TurboFan 会有优化 [#1][1]. 40 | 41 | 42 | ### Bad value context for arguments value 43 | 44 | * 简单例子 45 | 46 | ```js 47 | // strict & sloppy modes 48 | function test1() { 49 | arguments[0] = 0; 50 | } 51 | 52 | // strict & sloppy modes 53 | function test2() { 54 | arguments.length = 0; 55 | } 56 | 57 | // strict & sloppy modes 58 | function test3() { 59 | return arguments; 60 | } 61 | 62 | // strict & sloppy modes 63 | function test4() { 64 | var args = [].slice.call(arguments); 65 | } 66 | 67 | // strict & sloppy modes 68 | function test5() { 69 | var a = arguments; 70 | return function() { 71 | return a; 72 | }; 73 | } 74 | ``` 75 | 76 | * Why 77 | * 要求再具体化 `arguments` 数组. 78 | 79 | * Advices 80 | * 可以读读: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments 81 | * 你可以循环 `arguments` 创建一个新的数组 [Unsupported phi use of arguments](#unsupported-phi-use-of-arguments) 82 | * V8 最新的 TurboFan 会有优化 [#1][1]. 83 | 84 | * 外部链接 85 | * https://github.com/bevry/taskgroup/issues/12 86 | * [更多][7] 87 | 88 | ### ForInStatement with non-local each variable 89 | 90 | * 简单例子 91 | 92 | ```js 93 | // strict & sloppy modes 94 | function test1() { 95 | var obj = {}; 96 | for(key in obj); 97 | } 98 | 99 | // strict & sloppy modes 100 | function key() { 101 | return 'a'; 102 | } 103 | function test2() { 104 | var obj = {}; 105 | for(key() in obj); 106 | } 107 | ``` 108 | 109 | * Why 110 | * https://github.com/yjhjstz/v8-git-mirror/blob/master/src/hydrogen.cc#L5254 111 | 112 | * Advices 113 | * 只有纯局部变量可以用于 for...in 114 | * https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#5-for-in 115 | 116 | * 外面链接 117 | * https://github.com/mbostock/d3/pull/2686 118 | 119 | 120 | ### Object literal with complex property 121 | 122 | * 简单例子 123 | 124 | ```js 125 | // strict & sloppy modes 126 | function test() { 127 | return { 128 | __proto__: 3 129 | }; 130 | } 131 | ``` 132 | 133 | * Why 134 | 135 | * Advices 136 | * 简化 Object。 137 | 138 | ### ForInStatement is not fast case 139 | 140 | * 简单例子 141 | ```js 142 | for (var prop in obj) { 143 | /* lots of code */ 144 | } 145 | ``` 146 | 147 | * Why 148 | * for 循环中包含太多的代码。 149 | 150 | * Advices 151 | * for 循环中的提取代码提取为函数。 152 | 153 | 154 | ### Reference to a variable which requires dynamic lookup 155 | 156 | * 简单例子 157 | ```js 158 | // sloppy mode only 159 | function test() { 160 | with ({x:1}) { 161 | return x; 162 | } 163 | } 164 | ``` 165 | 166 | * Why 167 | * 编译时编译定位失败,Crankshaft需要重新动态查找。[#3][3] 168 | 169 | * Advices 170 | * TurboFan可以优化。 171 | 172 | 173 | ### TryCatchStatement 174 | 175 | * 简单例子 176 | 177 | ```js 178 | // strict & sloppy modes OR // sloppy mode only 179 | function func() { 180 | return 3; 181 | try {} catch(e) {} 182 | } 183 | ``` 184 | 185 | * Why 186 | * try/catch 使得控制流不稳定,很难在运行时优化。 187 | * Advices 188 | * 不要在负载重的函数中使用try/catch. 189 | * 可以重构为 `try { func() } catch` 190 | 191 | 192 | ### TryFinallyStatement 193 | 194 | * 简单例子 195 | 196 | ```js 197 | // strict & sloppy modes OR // sloppy mode only 198 | function func() { 199 | return 3; 200 | try {} finally {} 201 | } 202 | ``` 203 | 204 | * Why 205 | * See [TryCatchStatement](#trycatchstatement) 206 | 207 | * Advices 208 | * See [TryCatchStatement](#trycatchstatement) 209 | 210 | 211 | 212 | ### Unsupported phi use of arguments 213 | 214 | * 简单例子 215 | 216 | ```js 217 | // strict & sloppy modes 218 | function test1() { 219 | var _arguments = arguments; 220 | if (0 === 0) { // anything evaluating to true, except a number or `true` 221 | _arguments = [0]; // Unsupported phi use of arguments 222 | } 223 | } 224 | 225 | // strict & sloppy modes 226 | function test2() { 227 | var _arguments = arguments; 228 | for (var i = 0; i < 1; i++) { 229 | _arguments = [0]; // Unsupported phi use of arguments 230 | } 231 | } 232 | 233 | // strict & sloppy modes 234 | function test3() { 235 | var _arguments = arguments; 236 | var again = true; 237 | while (again) { 238 | _arguments = [0]; // Unsupported phi use of arguments 239 | again = false; 240 | } 241 | } 242 | ``` 243 | 244 | * Why 245 | * Crankshaft 无法知道 `_arguments`是 object 或 array. 246 | * [深入了解](http://mrale.ph/blog/2015/11/02/crankshaft-vs-arguments-object.html) 247 | 248 | * Advices 249 | * 最好操作 `arguments` 的拷贝. 250 | * TurboFan 可以优化 [#1][1]. 251 | 252 | 253 | 254 | ### Yield 255 | 256 | * 简单例子 257 | 258 | ```js 259 | // strict & sloppy modes 260 | function* test() { 261 | yield 0; 262 | } 263 | ``` 264 | 265 | * Why 266 | * generator 状态保持、恢复通过拷贝函数栈帧实现,但在优化编译器中并不适用。 267 | 268 | * Advices 269 | * 暂时不用考虑,TurboFan 可以优化。 270 | 271 | * 外部链接: 272 | * https://groups.google.com/forum/#!topic/v8-users/KnnUb-u4rA8 273 | 274 | --- 275 | 276 | [1]: https://chromium.googlesource.com/v8/v8/+/d3f074b23195a2426d14298dca30c4cf9183f203%5E%21/src/bailout-reason.h 277 | [2]: https://codereview.chromium.org/1272673003 278 | [3]: https://groups.google.com/forum/#!msg/google-chrome-developer-tools/Y0J2XQ9iiqU/H60qqZNlQa8J 279 | [4]: https://github.com/GoogleChrome/devtools-docs/issues/53#issuecomment-37269998 280 | [5]: https://github.com/GoogleChrome/devtools-docs/issues/53#issuecomment-140030617 281 | [6]: https://github.com/GoogleChrome/devtools-docs/issues/53#issuecomment-145192013 282 | [7]: https://github.com/GoogleChrome/devtools-docs/issues/53#issuecomment-147569505 283 | 284 | 285 | 286 | ### Resources 287 | 288 | - [All bailout reasons in Chromium codebase](https://code.google.com/p/chromium/codesearch#chromium/src/v8/src/bailout-reason.h) 289 | - [Bad value context for arguments value](https://gist.github.com/Hypercubed/89808f3051101a1a97f3) 290 | - [I-want-to-optimize-my-JS-application-on-V8 checklist](http://mrale.ph/blog/2011/12/18/v8-optimization-checklist.html) 291 | - [JavaScript: Performance loss on incorrect arguments using](http://techblog.dorogin.com/2015/05/performance-loss-on-incorrect-arguments-using.html) 292 | - [Optimization killers](https://github.com/petkaantonov/bluebird/wiki/Optimization-killers) 293 | - [OptimizationKillers](https://github.com/zhangchiqing/OptimizationKillers) 294 | - [Performance Tips for JavaScript in V8](http://www.html5rocks.com/en/tutorials/speed/v8/) 295 | - [thlorenz/v8-perf](https://github.com/thlorenz/v8-perf/blob/master/compiler.md) 296 | 297 | 298 | 299 | -------------------------------------------------------------------------------- /chapter9/chapter9-1.md: -------------------------------------------------------------------------------- 1 | 2 | ## 网络 (Net) 3 | 4 | ### 网络模型 5 | ISO制定的OSI参考模型的过于庞大、复杂招致了许多批评。与此对照,由技术人员自己开发的TCP/ 6 | IP协议栈获得了更为广泛的应用。如图所示,是TCP/IP参考模型和OSI参考模型的对比示意图。 7 | 8 | ![NET](http://img.blog.csdn.net/20160324203556764) 9 | 10 | ### UDP vs TCP 11 | * TCP(Transmission Control Protocol):传输控制协议 12 | * UDP(User Datagram Protocol):用户数据报协议 13 | 14 | 主要在于连接性(Connectivity)、可靠性(Reliability)、有序性(Ordering)、有界性(Boundary)、拥塞控制(Congestion or Flow control)、传输速度(Speed)、量级(Heavy/Light weight)、头部大小(Header size)等差异。 15 | 16 | #### 主要差异: 17 | * TCP是面向连接(Connection oriented)的协议,UDP是无连接(Connection less)协议; 18 | * TCP用三次握手建立连接:1) Client向server发送SYN;2) Server接收到SYN,回复Client一个SYN-ACK;3)Client接收到SYN_ACK,回复Server一个ACK。到此,连接建成。UDP发送数据前不需要建立连接。 19 | ![](http://p.blog.csdn.net/images/p_blog_csdn_net/ruimingde/492389/o_tcp三次握手.bmp) 20 | 21 | 22 | * TCP可靠,UDP不可靠; 23 | * TCP丢包会自动重传,UDP不会。 24 | 25 | 26 | * TCP有序,UDP无序; 27 | * 消息在传输过程中可能会乱序,后发送的消息可能会先到达,TCP会对其进行重排序,UDP不会。 28 | 29 | 从程序实现的角度来看,可以用下图来进行描述。 30 | 31 | ![](http://img.my.csdn.net/uploads/201303/15/1363304870_3150.jpg) 32 | 33 | 从上图也能清晰的看出,TCP通信需要服务器端侦听listen、接收客户端连接请求accept,等待客户端connect建立连接后才能进行数据包的收发(recv/send)工作。而UDP则服务器和客户端的概念不明显,服务器端即接收端需要绑定端口,等待客户端的数据的到来。后续便可以进行数据的收发(recvfrom/sendto)工作。 34 | 35 | ### Socket 抽象 36 | Socket 是对 TCP/IP 协议族的一种封装,是应用层与TCP/IP协议族通信的中间软件抽象层。它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。 37 | 38 | Socket 还可以认为是一种网络间不同计算机上的进程通信的一种方法,利用三元组(ip地址,协议,端口)就可以唯一标识网络中的进程,网络中的进程通信可以利用这个标志与其它进程进行交互。 39 | 40 | Socket 起源于 Unix ,Unix/Linux 基本哲学之一就是“一切皆文件”,都可以用“打开(open) –> 读写(write/read) –> 关闭(close)”模式来进行操作。因此 Socket 也被处理为一种特殊的文件。 41 | 42 | #### C++层绑定 43 | TCP的绑定导出: 44 | ```c++ 45 | void TCPWrap::Initialize(Local target, 46 | Local unused, 47 | Local context) { 48 | Environment* env = Environment::GetCurrent(context); 49 | 50 | Local t = env->NewFunctionTemplate(New); 51 | t->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "TCP")); 52 | t->InstanceTemplate()->SetInternalFieldCount(1); 53 | 54 | // Init properties 55 | t->InstanceTemplate()->Set(String::NewFromUtf8(env->isolate(), "reading"), 56 | Boolean::New(env->isolate(), false)); 57 | t->InstanceTemplate()->Set(String::NewFromUtf8(env->isolate(), "owner"), 58 | Null(env->isolate())); 59 | t->InstanceTemplate()->Set(String::NewFromUtf8(env->isolate(), "onread"), 60 | Null(env->isolate())); 61 | t->InstanceTemplate()->Set(String::NewFromUtf8(env->isolate(), 62 | "onconnection"), 63 | Null(env->isolate())); 64 | 65 | 66 | env->SetProtoMethod(t, "close", HandleWrap::Close); 67 | 68 | env->SetProtoMethod(t, "ref", HandleWrap::Ref); 69 | env->SetProtoMethod(t, "unref", HandleWrap::Unref); 70 | 71 | StreamWrap::AddMethods(env, t, StreamBase::kFlagHasWritev); 72 | 73 | env->SetProtoMethod(t, "open", Open); 74 | env->SetProtoMethod(t, "bind", Bind); 75 | env->SetProtoMethod(t, "listen", Listen); 76 | env->SetProtoMethod(t, "connect", Connect); 77 | env->SetProtoMethod(t, "bind6", Bind6); 78 | env->SetProtoMethod(t, "connect6", Connect6); 79 | env->SetProtoMethod(t, "getsockname", 80 | GetSockOrPeerName); 81 | env->SetProtoMethod(t, "getpeername", 82 | GetSockOrPeerName); 83 | env->SetProtoMethod(t, "setNoDelay", SetNoDelay); 84 | env->SetProtoMethod(t, "setKeepAlive", SetKeepAlive); 85 | 86 | #ifdef _WIN32 87 | env->SetProtoMethod(t, "setSimultaneousAccepts", SetSimultaneousAccepts); 88 | #endif 89 | 90 | target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "TCP"), t->GetFunction()); 91 | env->set_tcp_constructor_template(t); 92 | 93 | // Create FunctionTemplate for TCPConnectWrap. 94 | Local cwt = 95 | FunctionTemplate::New(env->isolate(), NewTCPConnectWrap); 96 | cwt->InstanceTemplate()->SetInternalFieldCount(1); 97 | cwt->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "TCPConnectWrap")); 98 | target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "TCPConnectWrap"), 99 | cwt->GetFunction()); 100 | } 101 | ``` 102 | `TCPWrap`导出了 TCP 类,TCPConnectWrap 类,并且我们看到对 IPV6协议族的支持:`bind6`, `connect6`。 103 | 104 | #### TCP Socket 105 | Node.js 的 Net模块也对 TCP socket 进行了抽象封装: 106 | ```js 107 | function Socket(options) { 108 | if (!(this instanceof Socket)) return new Socket(options); 109 | 110 | this._connecting = false; 111 | this._hadError = false; 112 | this._handle = null; 113 | this._parent = null; 114 | this._host = null; 115 | 116 | if (typeof options === 'number') 117 | options = { fd: options }; // Legacy interface. 118 | else if (options === undefined) 119 | options = {}; 120 | 121 | stream.Duplex.call(this, options); 122 | 123 | if (options.handle) { 124 | this._handle = options.handle; // private 125 | } else if (options.fd !== undefined) { 126 | this._handle = createHandle(options.fd); 127 | this._handle.open(options.fd); 128 | if ((options.fd == 1 || options.fd == 2) && 129 | (this._handle instanceof Pipe) && 130 | process.platform === 'win32') { 131 | // Make stdout and stderr blocking on Windows 132 | var err = this._handle.setBlocking(true); 133 | if (err) 134 | throw errnoException(err, 'setBlocking'); 135 | } 136 | this.readable = options.readable !== false; 137 | this.writable = options.writable !== false; 138 | } else { 139 | // these will be set once there is a connection 140 | this.readable = this.writable = false; 141 | } 142 | 143 | // shut down the socket when we're finished with it. 144 | this.on('finish', onSocketFinish); 145 | this.on('_socketEnd', onSocketEnd); 146 | 147 | initSocketHandle(this); 148 | 149 | // ... 150 | } 151 | util.inherits(Socket, stream.Duplex); 152 | ``` 153 | 首先 `Socket` 是一个全双工的 Stream,所以继承了 Duplex。通过 `createHandle` 创建套接字并赋值到 154 | `this._handle`上。 155 | 156 | 同时监听 `finish`, `_socketEnd`事件, 157 | 158 | 159 | 160 | ### 粘包 161 | > 一般所谓的TCP粘包是在一次接收数据不能完全地体现一个完整的消息数据。TCP通讯为何存在粘包呢?主要原因是TCP是以流的方式来处理数据,再加上网络上MTU的值往往小于在应用处理的消息数据,所以就会引发一次接收的数据无法满足消息的需要,导致粘包的存在。处理粘包的唯一方法就是制定应用层的数据通讯协议,通过协议来规范现有接收的数据是否满足消息数据的需要。 162 | 163 | #### 情况分析 164 | 165 | TCP粘包通常在流传输中出现,UDP则不会出现粘包,因为UDP有消息边界。使用TCP协议发送数据段需要等待缓冲区满了才将数据发送出去,当满的时候有可能不是一条消息而是几条消息存在于缓冲区内,为了优化性能(Nagle算法),TCP会将这几个小数据包合并为一个大的数据包,造成粘包;另外在接收数据端,如果没能及时接收缓冲区的包,也会造成缓冲区多包合并接收,这也是粘包。 166 | 167 | #### 解决办法 168 | 169 | * 自定义应用层协议; 170 | * 不使用Nagle算法, 使用提供的 API:`socket.setNoDelay`。 171 | 172 | 173 | ### UDP 174 | #### 组播 175 | * https://en.wikipedia.org/wiki/Multicast#IP_multicast%EF%BC%89%EF%BC%8C%E5%85%B7 176 | 177 | #### UDP Socket 178 | ```js 179 | function Socket(type, listener) { 180 | EventEmitter.call(this); 181 | 182 | if (typeof type === 'object') { 183 | var options = type; 184 | type = options.type; 185 | } 186 | 187 | var handle = newHandle(type); 188 | handle.owner = this; 189 | 190 | this._handle = handle; 191 | this._receiving = false; 192 | this._bindState = BIND_STATE_UNBOUND; 193 | this.type = type; 194 | this.fd = null; // compatibility hack 195 | 196 | // If true - UV_UDP_REUSEADDR flag will be set 197 | this._reuseAddr = options && options.reuseAddr; 198 | 199 | if (typeof listener === 'function') 200 | this.on('message', listener); 201 | } 202 | util.inherits(Socket, EventEmitter); 203 | ``` 204 | 205 | UDP 继承了 `EventEmitter`, 同样也支持 IPV4和 IPV6协议, 由`type`区分, 206 | `this._reuseAddr` 标识是否要使用选项:`SO_REUSEADDR`。 207 | 208 | SO_REUSEADDR允许完全重复的捆绑:当一个IP地址和端口绑定到某个套接口上时,还允许此IP地址和端口捆绑到另一个套接口上。一般来说,这个特性仅在支持多播的系统上才有,而且只对UDP套接口而言(TCP不支持多播)。 209 | 210 | ### 总结 211 | 从笔者的经验看,尽量不要尝试去使用 `UDP`,除非你知道丢包了对于应用是没有影响的,否则排查网络丢包会使人崩溃的! 212 | 213 | 214 | ### 参考 215 | * https://en.wikipedia.org/wiki/Nagle's_algorithm 216 | -------------------------------------------------------------------------------- /chapter11/chapter11-5.md: -------------------------------------------------------------------------------- 1 | ## 文件io 2 | 3 | 上一章节在讲述了线程池的模型,读者对一次异步 IO 发起到结束有了一个大致的认识。 4 | 5 | fs 模块还提供了同步接口,如 `readFileSync`, 这在异步模型的 node.js 的 6 | 核心模块中是极为少见的。 7 | 8 | 9 | ### 请求对象 10 | 11 | * 异步读文件接口定义: 12 | `fs.readFile = function(path, options, callback_) ` 13 | * 同步读文件接口定义: 14 | `fs.readFileSync = function(path, options) ` 15 | 16 | 两者明显的差异在于第三个参数 `callback_`, 异步会提交请求然后等待回调,同步则阻塞直到返回。 17 | 18 | 让我们来看看第三个参数对实现的影响。 19 | 20 | ```js 21 | // fs.js 22 | // ... 23 | var context = new ReadFileContext(callback, encoding); 24 | var req = new FSReqWrap(); 25 | req.context = context; 26 | req.oncomplete = readFileAfterOpen; 27 | ``` 28 | 29 | 异步的实现中会创建一个请求对象,并且绑定回调和上下文环境。 该请求对象由 C++ 绑定导出。 30 | 31 | 32 | #### FSReqWrap (node.js) 33 | FSReqWrap 是由 `src/node_file.cc` 实现并导出,提供给 javascript 使用。 34 | 35 | ```c++ 36 | class FSReqWrap: public ReqWrap { 37 | public: 38 | enum Ownership { COPY, MOVE }; 39 | 40 | inline static FSReqWrap* New(Environment* env, 41 | Local req, 42 | const char* syscall, 43 | const char* data = nullptr, 44 | Ownership ownership = COPY); 45 | 46 | inline void Dispose(); 47 | 48 | //... 49 | }; 50 | 51 | ``` 52 | FSReqWrap 继承 ReqWrap, ReqWrap 是个模板类,`T req_;` 存储了不同类型的请求,在这里 53 | 模板编译后,`uv_fs_t req_;`, req_ 存储了 uv_fs_t 请求对象。 这源自于一次性能优化的提交。 54 | > fs: improve `readFile` performance 55 | 56 | > This commit improves `readFile` performance by 57 | > reducing number of closure allocations and using 58 | > `FSReqWrap` directly. 59 | 60 | 具体了解, https://github.com/iojs/io.js/pull/718 。 61 | 62 | 63 | 在js 层发起请求后,会来到C++绑定层, 64 | ```c++ 65 | #define ASYNC_DEST_CALL(func, req, dest, ...) \ 66 | Environment* env = Environment::GetCurrent(args); \ 67 | CHECK(req->IsObject()); \ 68 | FSReqWrap* req_wrap = FSReqWrap::New(env, req.As(), #func, dest); \ 69 | int err = uv_fs_ ## func(env->event_loop(), \ 70 | &req_wrap->req_, \ 71 | __VA_ARGS__, \ 72 | After); \ 73 | req_wrap->Dispatched(); \ 74 | if (err < 0) { \ 75 | uv_fs_t* uv_req = &req_wrap->req_; \ 76 | uv_req->result = err; \ 77 | uv_req->path = nullptr; \ 78 | After(uv_req); \ 79 | req_wrap = nullptr; \ 80 | } else { \ 81 | args.GetReturnValue().Set(req_wrap->persistent()); \ 82 | } 83 | 84 | #define ASYNC_CALL(func, req, ...) \ 85 | ASYNC_DEST_CALL(func, req, nullptr, __VA_ARGS__) \ 86 | ``` 87 | 88 | 这里才会生成 libuv 所需的请求对象, 对于读请求调用 `uv_fs_read`, 提交请求,指定回调函数为 `After`。 89 | 90 | #### uv_fs_t (libuv) 91 | 看一下libuv的异步读文件代码,deps/uv/src/unix/fs.c: 92 | 93 | ```c++ 94 | /* uv_fs_t is a subclass of uv_req_t. */ 95 | struct uv_fs_s { 96 | UV_REQ_FIELDS 97 | uv_fs_type fs_type; 98 | uv_loop_t* loop; 99 | uv_fs_cb cb; 100 | ssize_t result; 101 | void* ptr; 102 | const char* path; 103 | uv_stat_t statbuf; /* Stores the result of uv_fs_stat() and uv_fs_fstat(). */ 104 | UV_FS_PRIVATE_FIELDS 105 | }; 106 | ``` 107 | 108 | ```c++ 109 | #define INIT(subtype) \ 110 | do { \ 111 | req->type = UV_FS; \ 112 | if (cb != NULL) \ 113 | uv__req_init(loop, req, UV_FS); \ 114 | req->fs_type = UV_FS_ ## subtype; \ 115 | req->result = 0; \ 116 | req->ptr = NULL; \ 117 | req->loop = loop; \ 118 | req->path = NULL; \ 119 | req->new_path = NULL; \ 120 | req->cb = cb; \ 121 | } \ 122 | while (0) 123 | ``` 124 | 125 | 可以看到一次异步文件读操作在libuv层被封装到一个uv_fs_t的结构体,req->cb是来自上层的回调函数(node C++层:src/node_file.cc 的After函数)。 126 | 127 | 异步io请求最后调用uv__work_submit,把异步io请求提交给线程池。这里有两个函数: 128 | 129 | * `uv__fs_work`:这个是文件io的处理函数,可以看到当cb为NULL的时候,即非异步模式,`uv__fs_work`在当前线程(事件循环所在线程)直接被调用。如果cb != NULL,即文件io为异步模式,此时把`uv__fs_work`和`uv__fs_done`提交给线程池。 130 | 131 | * `uv__fs_done`:这个是异步文件io结束后的回调函数。在uv__fs_done里面会回调上层C++模块的cb函数(即req->cb)。 132 | 133 | 需要特别注意的是:** 此时io操作的主体 `uv__fs_work`函数是在线程池里执行的。 134 | 但是`uv__fs_done` 135 | 必须在事件循环的线程里被回调,因为这个函数最终会回调到用户js代码的回调函数,而js代码里的所有代码必须在同个线程里面。** 136 | 137 | #### 线程池的请求对象 —— struct uv__work 138 | 先看下 `uv__work`的定义: 139 | ```c++ 140 | struct uv__work { 141 | void (*work)(struct uv__work *w); 142 | void (*done)(struct uv__work *w, int status); 143 | struct uv_loop_s* loop; 144 | void* wq[2]; 145 | }; 146 | ``` 147 | 再看看`uv__work_submit`做了什么: 148 | ```c++ 149 | static void post(QUEUE* q) { 150 | uv_mutex_lock(&mutex); 151 | QUEUE_INSERT_TAIL(&wq, q); 152 | if (idle_threads > 0) 153 | uv_cond_signal(&cond); 154 | uv_mutex_unlock(&mutex); 155 | } 156 | 157 | void uv__work_submit(uv_loop_t* loop, 158 | struct uv__work* w, 159 | void (*work)(struct uv__work* w), 160 | void (*done)(struct uv__work* w, int status)) { 161 | uv_once(&once, init_once); 162 | w->loop = loop; 163 | w->work = work; 164 | w->done = done; 165 | post(&w->wq); 166 | } 167 | ``` 168 | 169 | `uv__work_submit` 把传进来的`uv__fs_work`、`uv__fs_done`封装到`uv__work`结构体里面,这个结构体表示一个线程操作的请求。通过post把请求提交给线程池。 170 | 171 | 看到post函数里面的QUEUE_INSERT_TAIL,把该`uv__work`对象加进`wq`链表里面。wq是一个全局静态变量。也就是说,进程空间里的所有线程共用同一个wq链表。 172 | 173 | 174 | 看到post函数通过uv_cond_signal向相应的条件变量——cond发送信号,处在uv_cond_wait挂起等待的工作线程当中的某个线程被激活。 175 | 176 | worker线程往下执行,从wq取出w,执行w->work()。 177 | 178 | 工作线程完成任务后,调用uv_async_send通知主线程统一的io观察者,执行 callback。 179 | 180 | 181 | ### 回调 182 | 在一次异步文件 I/O 操作中,回调函数是整个流程的最后一环,用以将底层 I/O 操作的结果传递给上层应用。具体流程如下: 183 | 184 | 1. **工作线程完成任务** 185 | 工作线程从全局工作队列中取出一个 `uv__work` 请求,执行其中封装的 `uv__fs_work` 函数,该函数完成实际的文件 I/O 操作(例如文件读取)。 186 | 187 | 2. **通知主线程** 188 | 完成 I/O 操作后,工作线程调用 `uv_async_send`,向主线程发送信号。这一机制确保了回调函数不会在工作线程中执行,而是延迟到主线程中被调度,从而保证了 JavaScript 层代码的单线程执行环境。 189 | 190 | 3. **执行回调函数** 191 | 主线程在检测到异步事件后,会调用 `uv__fs_done` 回调函数。该函数负责将 `uv_fs_t` 请求对象中的操作结果(如读取的数据、错误码等)传递给上层 C++ 模块中的 `After` 函数。随后,`After` 函数会调用用户在 JavaScript 层传入的回调函数,将操作结果反馈给用户。 192 | 193 | 4. **事件循环与回调协调** 194 | 由于 Node.js 的 JavaScript 代码运行在单线程的事件循环中,因此所有用户回调都必须在主线程中执行。这一设计确保了回调函数能够安全地访问共享状态,而无需担心线程安全问题,同时也使得异步 I/O 操作与事件循环的协同工作成为可能。 195 | 196 | 这种回调机制将底层的异步 I/O 操作与上层的 JavaScript 回调解耦合,通过工作线程与主线程之间的信号通知,实现了高效且安全的异步操作流程。 197 | 198 | ### 总结 199 | 通过对各层请求对象的梳理,也详细梳理出了一次 read 请求的脉络, 使读者有了一个理性的认识。 200 | 201 | ### 参考 202 | 203 | - https://github.com/nodejs/node/blob/master/src/node_file.cc 204 | - https://github.com/libuv/libuv 205 | - https://github.com/iojs/io.js/pull/718 206 | - https://github.com/libuv/libuv/blob/v1.x/src/unix/fs.c 207 | 208 | -------------------------------------------------------------------------------- /chapter8/chapter8-1.md: -------------------------------------------------------------------------------- 1 | 2 | ## Stream 流 3 | 4 | 5 | 从[早先的unix](http://www.youtube.com/watch?v=tc4ROCJYbm0)开始,stream便开始进入了人们的视野,在过去的几十年的时间里,它被证明是一种可依赖的编程方式,它可以将一个大型的系统拆成一些很小的部分,并且让这些部分之间完美地进行合作。在unix中,我们可以使用`|`符号来实现流。在node中,node内置的[stream模块](http://nodejs.org/docs/latest/api/stream.html)已经被多个核心模块使用,同时也可以被用户自定义的模块使用。和unix类似,node中的流模块的基本操作符叫做`.pipe()`,同时你也可以使用一个后压机制来应对那些对数据消耗较慢的对象。 6 | 7 | 8 | 9 | ### 为什么应该使用流 10 | 11 | 在node中,I/O都是异步的,所以在和硬盘以及网络的交互过程中会涉及到传递回调函数的过程。你之前可能会写出这样的代码: 12 | ```js 13 | var http = require('http'); 14 | var fs = require('fs'); 15 | 16 | var server = http.createServer(function (req, res) { 17 | fs.readFile(__dirname + 'data.txt', function (err, data) { 18 | res.end(data); 19 | }); 20 | }); 21 | server.listen(8000); 22 | ``` 23 | 上面的这段代码并没有什么问题,但是在每次请求时,我们都会把整个`data.txt`文件读入到内存中,然后再把结果返回给客户端。想想看,如果`data.txt`文件非常大,在响应大量用户的并发请求时,程序可能会消耗大量的内存,这样很可能会造成用户连接缓慢的问题。 24 | 25 | 其次,上面的代码可能会造成很不好的用户体验,因为用户在接收到任何的内容之前首先需要等待程序将文件内容完全读入到内存中。 26 | 27 | 所幸的是,`(req,res)`参数都是流对象,这意味着我们可以使用一种更好的方法来实现上面的需求: 28 | ```js 29 | var http = require('http'); 30 | var fs = require('fs'); 31 | 32 | var server = http.createServer(function (req, res) { 33 | var stream = fs.createReadStream(__dirname + '/data.txt'); 34 | stream.pipe(res); 35 | }); 36 | server.listen(8000); 37 | ``` 38 | 在这里,`.pipe()`方法会自动帮助我们监听`data`和`end`事件。上面的这段代码不仅简洁,而且`data.txt`文件中每一小段数据都将源源不断的发送到客户端。 39 | 40 | 除此之外,使用`.pipe()`方法还有别的好处,比如说它可以自动控制后端压力,以便在客户端连接缓慢的时候node可以将尽可能少的缓存放到内存中。 41 | 42 | 想要将数据进行压缩?我们可以使用相应的流模块完成这项工作! 43 | ```js 44 | var http = require('http'); 45 | var fs = require('fs'); 46 | var oppressor = require('oppressor'); 47 | 48 | var server = http.createServer(function (req, res) { 49 | var stream = fs.createReadStream(__dirname + '/data.txt'); 50 | stream.pipe(oppressor(req)).pipe(res); 51 | }); 52 | server.listen(8000); 53 | ``` 54 | 通过上面的代码,我们成功的将发送到浏览器端的数据进行了gzip压缩。我们只是使用了一个oppressor模块来处理这件事情。 55 | 56 | 一旦你学会使用流api,你可以将这些流模块像搭乐高积木或者像连接水管一样拼凑起来,从此以后你可能再也不会去使用那些没有流API的模块获取和推送数据了。 57 | 58 | ### 流模块基础 59 | 60 | nodejs 底层一共提供了4个流, Readable 流、Writable 流、Duplex 流和 Transform 流。 61 | 62 | 63 | 使用情景 | 类 | 需要重写的方法 64 | -------| ------| ------------- 65 | 只读 | Readable| _read 66 | 只写 | Writable | _write 67 | 双工 | Duplex | _read, _write 68 | 操作被写入数据,然后读出结果| Transform| _transform, _flush 69 | 70 | 71 | #### pipe 72 | 73 | 无论哪一种流,都会使用`.pipe()`方法来实现输入和输出。 74 | 75 | `.pipe()`函数很简单,它仅仅是接受一个源头`src`并将数据输出到一个可写的流`dst`中: 76 | 77 | src.pipe(dst) 78 | 79 | `.pipe(dst)`将会返回`dst`因此你可以链式调用多个流: 80 | 81 | a.pipe(b).pipe(c).pipe(d) 82 | 83 | 上面的代码也可以等价为: 84 | 85 | a.pipe(b); 86 | b.pipe(c); 87 | c.pipe(d); 88 | 89 | 这和你在unix中编写流代码很类似: 90 | 91 | a | b | c | d 92 | 93 | 只不过此时你是在node中编写而不是在shell中! 94 | 95 | ### readable流 96 | 97 | Readable流可以产出数据,你可以将这些数据传送到一个writable,transform或者duplex流中,只需要调用`pipe()`方法: 98 | 99 | readableStream.pipe(dst) 100 | 101 | #### 创建一个readable流 102 | 103 | 现在我们就来创建一个readable流! 104 | 105 | var Readable = require('stream').Readable; 106 | 107 | var rs = new Readable; 108 | rs.push('beep '); 109 | rs.push('boop\n'); 110 | rs.push(null); 111 | 112 | rs.pipe(process.stdout); 113 | 114 | 下面运行代码: 115 | 116 | $ node read0.js 117 | beep boop 118 | 119 | 在上面的代码中`rs.push(null)`的作用是告诉`rs`输出数据应该结束了。 120 | 121 | 需要注意的一点是我们在将数据输出到`process.stdout`之前已经将内容推送进readable流`rs`中,但是所有的数据依然是可写的。 122 | 123 | 这是因为在你使用`.push()`将数据推进一个readable流中时,一直要到另一个东西来消耗数据之前,数据都会存在一个缓存中。 124 | 125 | 然而,在更多的情况下,我们想要的是当需要数据时数据才会产生,以此来避免大量的缓存数据。 126 | 127 | 我们可以通过定义一个`._read`函数来实现按需推送数据: 128 | 129 | var Readable = require('stream').Readable; 130 | var rs = Readable(); 131 | 132 | var c = 97; 133 | rs._read = function () { 134 | rs.push(String.fromCharCode(c++)); 135 | if (c > 'z'.charCodeAt(0)) rs.push(null); 136 | }; 137 | 138 | rs.pipe(process.stdout); 139 | 140 | 代码的运行结果如下所示: 141 | 142 | $ node read1.js 143 | abcdefghijklmnopqrstuvwxyz 144 | 145 | 在这里我们将字母`a`到`z`推进了rs中,但是只有当数据消耗者出现时,数据才会真正实现推送。 146 | 147 | `_read`函数也可以获取一个`size`参数来指明消耗者想要读取多少比特的数据,但是这个参数是可选的。 148 | 149 | 需要注意到的是你可以使用`util.inherit()`来继承一个Readable流。 150 | 151 | 为了说明只有在数据消耗者出现时,`_read`函数才会被调用,我们可以将上面的代码简单的修改一下: 152 | 153 | var Readable = require('stream').Readable; 154 | var rs = Readable(); 155 | 156 | var c = 97 - 1; 157 | 158 | rs._read = function () { 159 | if (c >= 'z'.charCodeAt(0)) return rs.push(null); 160 | 161 | setTimeout(function () { 162 | rs.push(String.fromCharCode(++c)); 163 | }, 100); 164 | }; 165 | 166 | rs.pipe(process.stdout); 167 | 168 | process.on('exit', function () { 169 | console.error('\n_read() called ' + (c - 97) + ' times'); 170 | }); 171 | process.stdout.on('error', process.exit); 172 | 173 | 运行上面的代码我们可以发现如果我们只请求5比特的数据,那么`_read`只会运行5次: 174 | 175 | $ node read2.js | head -c5 176 | abcde 177 | _read() called 5 times 178 | 179 | 在上面的代码中,`setTimeout`很重要,因为操作系统需要花费一些时间来发送程序结束信号。 180 | 181 | 另外,`process.stdout.on('error',fn)`处理器也很重要,因为当`head`不再关心我们的程序输出时,操作系统将会向我们的进程发送一个`SIGPIPE`信号,此时`process.stdout`将会捕获到一个`EPIPE`错误。 182 | 183 | 上面这些复杂的部分在和操作系统相关的交互中是必要的,但是如果你直接和node中的流交互的话,则可有可无。 184 | 185 | 如果你创建了一个readable流,并且想要将任何的值推送到其中的话,确保你在创建流的时候指定了objectMode参数,`Readable({ objectMode: true })`。 186 | 187 | #### 消耗一个readable流 188 | 189 | 大部分时候,将一个readable流直接pipe到另一种类型的流或者使用through或者concat-stream创建的流中,是一件很容易的事情。但是有时我们也会需要直接来消耗一个readable流。 190 | 191 | process.stdin.on('readable', function () { 192 | var buf = process.stdin.read(); 193 | console.dir(buf); 194 | }); 195 | 196 | 代码运行结果如下所示: 197 | 198 | $ (echo abc; sleep 1; echo def; sleep 1; echo ghi) | node consume0.js 199 | 200 | 201 | 202 | null 203 | 204 | 当数据可用时,`readable`事件将会被触发,此时你可以调用`.read()`方法来从缓存中获取这些数据。 205 | 206 | 当流结束时,`.read()`将返回`null`,因为此时已经没有更多的字节可以供我们获取了。 207 | 208 | 你也可以告诉`.read()`方法来返回`n`个字节的数据。虽然所有核心对象中的流都支持这种方式,但是对于对象流来说这种方法并不可用。 209 | 210 | 下面是一个例子,在这里我们制定每次读取3个字节的数据: 211 | 212 | process.stdin.on('readable', function () { 213 | var buf = process.stdin.read(3); 214 | console.dir(buf); 215 | }); 216 | 217 | 运行上面的例子,我们将获取到不完整的数据: 218 | 219 | $ (echo abc; sleep 1; echo def; sleep 1; echo ghi) | node consume1.js 220 | 221 | 222 | 223 | 224 | 这是因为多余的数据都留在了内部的缓存中,因此这个时候我们需要告诉node我们还对剩下的数据感兴趣,我们可以使用`.read(0)`来完成这件事: 225 | 226 | process.stdin.on('readable', function () { 227 | var buf = process.stdin.read(3); 228 | console.dir(buf); 229 | process.stdin.read(0); 230 | }); 231 | 232 | 到现在为止我们的代码和我们所期望的一样了! 233 | 234 | $ (echo abc; sleep 1; echo def; sleep 1; echo ghi) | node consume2.js 235 | 236 | 237 | 238 | 239 | 240 | 我们也可以使用`.unshift()`方法来放置多余的数据。 241 | 242 | 使用`unshift()`方法能够放置我们进行不必要的缓存拷贝。在下面的代码中我们将创建一个分割新行的可读解析器: 243 | 244 | var offset = 0; 245 | 246 | process.stdin.on('readable', function () { 247 | var buf = process.stdin.read(); 248 | if (!buf) return; 249 | for (; offset < buf.length; offset++) { 250 | if (buf[offset] === 0x0a) { 251 | console.dir(buf.slice(0, offset).toString()); 252 | buf = buf.slice(offset + 1); 253 | offset = 0; 254 | process.stdin.unshift(buf); 255 | return; 256 | } 257 | } 258 | process.stdin.unshift(buf); 259 | }); 260 | 261 | 代码的运行结果如下所示: 262 | 263 | $ tail -n +50000 /usr/share/dict/american-english | head -n10 | node lines.js 264 | 'hearties' 265 | 'heartiest' 266 | 'heartily' 267 | 'heartiness' 268 | 'heartiness\'s' 269 | 'heartland' 270 | 'heartland\'s' 271 | 'heartlands' 272 | 'heartless' 273 | 'heartlessly' 274 | 275 | 当然,已经有很多这样的模块比如split来帮助你完成这件事情,你完全不需要自己写一个。 276 | 277 | ### writable流 278 | 279 | 一个writable流指的是只能流进不能流出的流: 280 | 281 | src.pipe(writableStream) 282 | 283 | #### 创建一个writable流 284 | 285 | 只需要定义一个`._write(chunk,enc,next)`函数,你就可以将一个readable流的数据释放到其中: 286 | 287 | var Writable = require('stream').Writable; 288 | var ws = Writable(); 289 | ws._write = function (chunk, enc, next) { 290 | console.dir(chunk); 291 | next(); 292 | }; 293 | 294 | process.stdin.pipe(ws); 295 | 296 | 代码运行结果如下所示: 297 | 298 | $ (echo beep; sleep 1; echo boop) | node write0.js 299 | 300 | 301 | 302 | 303 | 第一个参数,`chunk`代表写进来的数据。 304 | 305 | 第二个参数`enc`代表编码的字符串,但是只有在`opts.decodeString`为`false`的时候你才可以写一个字符串。 306 | 307 | 第三个参数,`next(err)`是一个回调函数,使用这个回调函数你可以告诉数据消耗者可以写更多的数据。你可以有选择性的传递一个错误对象`error`,这时会在流实体上触发一个`emit`事件。 308 | 309 | 在从一个readable流向一个writable流传数据的过程中,数据会自动被转换为`Buffer`对象,除非你在创建writable流的时候制定了`decodeStrings`参数为`false`,`Writable({decodeStrings: false})`。 310 | 311 | 如果你需要传递对象,需要指定`objectMode`参数为`true`,`Writable({ objectMode: true })`。 312 | 313 | #### 向一个writable流中写东西 314 | 315 | 如果你需要向一个writable流中写东西,只需要调用`.write(data)`即可。 316 | 317 | process.stdout.write('beep boop\n'); 318 | 319 | 为了告诉一个writable流你已经写完毕了,只需要调用`.end()`方法。你也可以使用`.end(data)`在结束前再写一些数据。 320 | 321 | var fs = require('fs'); 322 | var ws = fs.createWriteStream('message.txt'); 323 | 324 | ws.write('beep '); 325 | 326 | setTimeout(function () { 327 | ws.end('boop\n'); 328 | }, 1000); 329 | 330 | 运行结果如下所示: 331 | 332 | $ node writing1.js 333 | $ cat message.txt 334 | beep boop 335 | 336 | 如果你在创建writable流时指定了`highWaterMark`参数,那么当没有更多数据写入时,调用`.write()`方法将会返回false。 337 | 338 | 如果你想要等待缓存情况,可以监听`drain`事件。 339 | 340 | 341 | 342 | ### duplex流 343 | 344 | 345 | Duplex流是一个可读也可写的流,全双工。 如图: 346 | ![](http://3.bp.blogspot.com/-hWPHqV9RJlM/VnrEyChmtnI/AAAAAAAABpQ/uTnbBCU87ek/s1600/duplex.PNG) 347 | 348 | 代码实现上: 349 | ```js 350 | const Readable = require('_stream_readable'); 351 | const Writable = require('_stream_writable'); 352 | 353 | util.inherits(Duplex, Readable); 354 | 355 | var keys = Object.keys(Writable.prototype); 356 | for (var v = 0; v < keys.length; v++) { 357 | var method = keys[v]; 358 | if (!Duplex.prototype[method]) 359 | Duplex.prototype[method] = Writable.prototype[method]; 360 | } 361 | ``` 362 | `Duplex` 首先继承了 `Readable`, 因为 javascript 没有 C++的多重继承的特性,所以 363 | 遍历 `Writable`的原型方法然后赋值到 `Duplex`的原型上。 364 | 365 | ### transform流 366 | 转换流(Transform streams)是一种输出由输入计算所得的双工流。它同时实现了 Readable 和 Writable 接口。 367 | 368 | Node中的转换流有: 369 | * zlib streams 370 | * crypto streams 371 | 372 | 你可以将transform流想象成一个流的中间部分,它可以读也可写,但是并不保存数据,它只负责处理流经它的数据。 373 | 374 | ### 总结 375 | 流式处理的优势: 将功能切分,并通过管道组合。 376 | 377 | 378 | ### 参考 379 | https://github.com/substack/stream-handbook 380 | -------------------------------------------------------------------------------- /chapter3/chapter3-2.md: -------------------------------------------------------------------------------- 1 | ## Yield 魔法 2 | ES6中的Generator的引入,极大程度上改变了JavaScript程序员对迭代器的看法,并为解决`callback hell`提供了新方法。 3 | 4 | ### Generators 5 | 迭代器模式是很常用的设计模式,但是实现起来,很多东西是程序化的;当迭代规则比较复杂时,维护迭代器内的状态,是比较麻烦的。 于是有了generator,何为generator? 6 | > Generators: a better way to build Iterators. 7 | 8 | 借助 yield 关键字,可以更优雅的实现fibonacci数列。 9 | 10 | ```js 11 | function* fibonacci() { 12 | let a = 0, b = 1; 13 | 14 | while(true) { 15 | yield a; 16 | [a, b] = [b, a + b]; 17 | } 18 | } 19 | ``` 20 | 21 | 22 | 23 | ### yield与异步 24 | yield可以暂停运行流程,那么便为改变执行流程提供了可能。这和Python的coroutine类似。 25 | 26 | Generator之所以可用来控制代码流程,就是通过yield来将两个或者多个Generator的执行路径互相切换。这种切换是语句级别的,而不是函数调用级别的。其本质是CPS变换。 27 | 28 | yield之后,实际上本次调用就结束了,控制权实际上已经转到了外部调用了generator的next方法的函数,调用的过程中伴随着状态的改变。那么如果外部函数不继续调用next方法,那么yield所在函数就相当于停在yield那里了。所以把异步的东西做完,要函数继续执行,只要在合适的地方再次调用generator 的next就行,就好像函数在暂停后,继续执行。 29 | 30 | 31 | 32 | ### V8 实现 33 | 34 | #### parse phase 35 | Generator function 和 `yield` 关键字处理是在 `parser.cc`, 我们看到 AST 解析函数: `Parser::ParseEagerFunctionBody()` 36 | 37 | ```c++ 38 | 3928 ZoneList* Parser::ParseEagerFunctionBody( 39 | 3929 const AstRawString* function_name, int pos, Variable* fvar, 40 | 3930 Token::Value fvar_init_op, FunctionKind kind, bool* ok) { 41 | 3931 ..... 42 | 3954 // For generators, allocate and yield an iterator on function entry. 43 | 3955 if (IsGeneratorFunction(kind)) { 44 | 3956 ZoneList* arguments = 45 | 3957 new(zone()) ZoneList(0, zone()); 46 | 3958 CallRuntime* allocation = factory()->NewCallRuntime( 47 | 3959 ast_value_factory()->empty_string(), 48 | 3960 Runtime::FunctionForId(Runtime::kCreateJSGeneratorObject), arguments, 49 | 3961 pos); 50 | 3962 VariableProxy* init_proxy = factory()->NewVariableProxy( 51 | 3963 function_state_->generator_object_variable()); 52 | 3964 Assignment* assignment = factory()->NewAssignment( 53 | 3965 Token::INIT_VAR, init_proxy, allocation, RelocInfo::kNoPosition); 54 | 3966 VariableProxy* get_proxy = factory()->NewVariableProxy( 55 | 3967 function_state_->generator_object_variable()); 56 | 3968 Yield* yield = factory()->NewYield( 57 | 3969 get_proxy, assignment, Yield::kInitial, RelocInfo::kNoPosition); 58 | 3970 body->Add(factory()->NewExpressionStatement( 59 | 3971 yield, RelocInfo::kNoPosition), zone()); 60 | 3972 } 61 | 3973 62 | 3974 ParseStatementList(body, Token::RBRACE, false, NULL, CHECK_OK); 63 | 3975 64 | 3976 if (IsGeneratorFunction(kind)) { 65 | 3977 VariableProxy* get_proxy = factory()->NewVariableProxy( 66 | 3978 function_state_->generator_object_variable()); 67 | 3979 Expression* undefined = 68 | 3980 factory()->NewUndefinedLiteral(RelocInfo::kNoPosition); 69 | 3981 Yield* yield = factory()->NewYield(get_proxy, undefined, Yield::kFinal, 70 | 3982 RelocInfo::kNoPosition); 71 | 3983 body->Add(factory()->NewExpressionStatement( 72 | 3984 yield, RelocInfo::kNoPosition), zone()); 73 | 3985 } 74 | 3986 ... 75 | 76 | ``` 77 | L3955 判断是否是Generator function。 `ParseStatementList` 解析 function 函数体。 78 | 注意,Generator function 也是一种 function, 在 V8中,同样用 `JSFunction` 表示。 79 | 80 | 在两个 if 函数体中,创建了 `Yield::kInitial`和 `Yield::kFinal` 两个Yield AST 节点。 81 | 82 | Yield 状态分为: 83 | 84 | ```c++ 85 | enum Kind { 86 | kInitial, // The initial yield that returns the unboxed generator object. 87 | kSuspend, // A normal yield: { value: EXPRESSION, done: false } 88 | kDelegating, // A yield*. 89 | kFinal // A return: { value: EXPRESSION, done: true } 90 | }; 91 | ``` 92 | 93 | ### codegen phase 94 | 机器码生成(x64平台)主要集中在 `runtime-generator.cc`, `full-codegen-x64.cc`。 95 | 96 | `runtime-generator.cc` 提供了 `Create`, `Suspend`, `Resume`, `Close`等 stub 代码段, 97 | 98 | 给 full-codegen 内联使用,生成汇编代码。 99 | 100 | 我们先来看到 `RUNTIME_FUNCTION(Runtime_CreateJSGeneratorObject)`, 101 | 102 | ```c++ 103 | 14 RUNTIME_FUNCTION(Runtime_CreateJSGeneratorObject) { 104 | 15 HandleScope scope(isolate); 105 | 16 DCHECK(args.length() == 0); 106 | 17 107 | 18 JavaScriptFrameIterator it(isolate); 108 | 19 JavaScriptFrame* frame = it.frame(); 109 | 20 Handle function(frame->function()); 110 | 21 RUNTIME_ASSERT(function->shared()->is_generator()); 111 | 22 112 | 23 Handle generator; 113 | 24 if (frame->IsConstructor()) { 114 | 25 generator = handle(JSGeneratorObject::cast(frame->receiver())); 115 | 26 } else { 116 | 27 generator = isolate->factory()->NewJSGeneratorObject(function); 117 | 28 } 118 | 29 generator->set_function(*function); 119 | 30 generator->set_context(Context::cast(frame->context())); 120 | 31 generator->set_receiver(frame->receiver()); 121 | 32 generator->set_continuation(0); 122 | 33 generator->set_operand_stack(isolate->heap()->empty_fixed_array()); 123 | 34 generator->set_stack_handler_index(-1); 124 | 35 125 | 36 return *generator; 126 | 37 } 127 | ``` 128 | 129 | 函数根据当前的 Frame, 创建一个 `JSGeneratorObject`对象来储存 `JSFunction`, `Context` ,pc 指针, 130 | 设置操作数栈为空。 131 | 132 | 133 | yield 后,实际上就是保存当前的执行环境,L74保存当前的操作数栈,并保存到JSGeneratorObject对象中。 134 | ```c++ 135 | 40 RUNTIME_FUNCTION(Runtime_SuspendJSGeneratorObject) { 136 | 41 HandleScope handle_scope(isolate); 137 | 42 DCHECK(args.length() == 1); 138 | 43 CONVERT_ARG_HANDLE_CHECKED(JSGeneratorObject, generator_object, 0); 139 | 44 140 | 45 JavaScriptFrameIterator stack_iterator(isolate); 141 | 46 JavaScriptFrame* frame = stack_iterator.frame(); 142 | 47 RUNTIME_ASSERT(frame->function()->shared()->is_generator()); 143 | 48 DCHECK_EQ(frame->function(), generator_object->function()); 144 | 49 145 | 50 // The caller should have saved the context and continuation already. 146 | 51 DCHECK_EQ(generator_object->context(), Context::cast(frame->context())); 147 | 52 DCHECK_LT(0, generator_object->continuation()); 148 | 53 149 | 54 // We expect there to be at least two values on the operand stack: the return 150 | 55 // value of the yield expression, and the argument to this runtime call. 151 | 56 // Neither of those should be saved. 152 | 57 int operands_count = frame->ComputeOperandsCount(); 153 | 58 DCHECK_GE(operands_count, 2); 154 | 59 operands_count -= 2; 155 | 60 156 | 61 if (operands_count == 0) { 157 | 62 // Although it's semantically harmless to call this function with an 158 | 63 // operands_count of zero, it is also unnecessary. 159 | 64 DCHECK_EQ(generator_object->operand_stack(), 160 | 65 isolate->heap()->empty_fixed_array()); 161 | 66 DCHECK_EQ(generator_object->stack_handler_index(), -1); 162 | 67 // If there are no operands on the stack, there shouldn't be a handler 163 | 68 // active either. 164 | 69 DCHECK(!frame->HasHandler()); 165 | 70 } else { 166 | 71 int stack_handler_index = -1; 167 | 72 Handle operand_stack = 168 | 73 isolate->factory()->NewFixedArray(operands_count); 169 | 74 frame->SaveOperandStack(*operand_stack, &stack_handler_index); 170 | 75 generator_object->set_operand_stack(*operand_stack); 171 | 76 generator_object->set_stack_handler_index(stack_handler_index); 172 | 77 } 173 | 78 174 | 79 return isolate->heap()->undefined_value(); 175 | 80 } 176 | 177 | ``` 178 | 179 | Resume 对应于外部的 `next`,要恢复执行,首先我们得知道需要执行的 pc 指针偏移,机器代码存储在 180 | `JSFunction` 的 `Code` 对象中, L105 拿到 pc 首地址, L106从 `JSGeneratorObject`对象 181 | 取出偏移 offset 。 182 | 183 | L108 设置当前 Frame 的 pc 偏移。L118 恢复操作数栈, L126-L130根据恢复的 mode, 返回 value。 184 | 185 | ```c++ 186 | 90 RUNTIME_FUNCTION(Runtime_ResumeJSGeneratorObject) { 187 | 91 SealHandleScope shs(isolate); 188 | 92 DCHECK(args.length() == 3); 189 | 93 CONVERT_ARG_CHECKED(JSGeneratorObject, generator_object, 0); 190 | 94 CONVERT_ARG_CHECKED(Object, value, 1); 191 | 95 CONVERT_SMI_ARG_CHECKED(resume_mode_int, 2); 192 | 96 JavaScriptFrameIterator stack_iterator(isolate); 193 | 97 JavaScriptFrame* frame = stack_iterator.frame(); 194 | 98 195 | 99 DCHECK_EQ(frame->function(), generator_object->function()); 196 | 100 DCHECK(frame->function()->is_compiled()); 197 | 101 198 | 102 STATIC_ASSERT(JSGeneratorObject::kGeneratorExecuting < 0); 199 | 103 STATIC_ASSERT(JSGeneratorObject::kGeneratorClosed == 0); 200 | 104 201 | 105 Address pc = generator_object->function()->code()->instruction_start(); 202 | 106 int offset = generator_object->continuation(); 203 | 107 DCHECK(offset > 0); 204 | 108 frame->set_pc(pc + offset); 205 | 109 ... 206 | 113 generator_object->set_continuation(JSGeneratorObject::kGeneratorExecuting); 207 | 114 208 | 115 FixedArray* operand_stack = generator_object->operand_stack(); 209 | 116 int operands_count = operand_stack->length(); 210 | 117 if (operands_count != 0) { 211 | 118 frame->RestoreOperandStack(operand_stack, 212 | 119 generator_object->stack_handler_index()); 213 | 120 generator_object->set_operand_stack(isolate->heap()->empty_fixed_array()); 214 | 121 generator_object->set_stack_handler_index(-1); 215 | 122 } 216 | 123 217 | 124 JSGeneratorObject::ResumeMode resume_mode = 218 | 125 static_cast(resume_mode_int); 219 | 126 switch (resume_mode) { 220 | 127 case JSGeneratorObject::NEXT: 221 | 128 return value; 222 | 129 case JSGeneratorObject::THROW: 223 | 130 return isolate->Throw(value); 224 | 131 } 225 | 132 ... 226 | 133 } 227 | ``` 228 | 这边我们关注下 args 参数, args[0]是`JSGeneratorObject` 对象`generator_object`, args[1]是Object 对象 229 | `value`, 也就是 `next` 的返回值,args[2]是表示 resume 模式的值。 230 | 231 | 对应的我们看到 `FullCodeGenerator::EmitGeneratorResume()` 中的这几行代码: 232 | ```c++ 233 | 2296 __ Push(rbx); 234 | 2297 __ Push(result_register()); 235 | 2298 __ Push(Smi::FromInt(resume_mode)); 236 | 2299 __ CallRuntime(Runtime::kResumeJSGeneratorObject, 3); 237 | ``` 238 | 239 | L2297从 result 寄存器中取出 value, L2299调用 `RUNTIME_FUNCTION(Runtime_ResumeJSGeneratorObject)`。 240 | 241 | 这样,从 yield value 到 g.next() 取出 value, 相信大家有了一个大概的认知了。 242 | 243 | ### 延伸 244 | 我们看到node.js依托 v8层面实现了协程,有兴趣的同学可以关心下 fibjs, 它是用 C库实现了协程,遇到异步调用就 "yield" 放弃 CPU, 245 | 交由协程调度,也解决了 callback hell 的问题。 246 | 本质思想上两种方案没本质区别: 247 | * Generator是利用yield特殊关键字来暂停执行,而fibers是利用Fiber.yield()暂停 248 | * Generator是利用函数返回的Generator句柄来控制函数的继续执行,而fibers是在异步回调中利用Fiber.current.run()继续执行。 249 | 250 | ### 参考 251 | * http://en.wikipedia.org/wiki/Continuation-passing_style 252 | * https://zh.wikipedia.org/zh-cn/协程 253 | * fibjs https://github.com/xicilion/fibjs 254 | -------------------------------------------------------------------------------- /chapter7/chapter7-1.md: -------------------------------------------------------------------------------- 1 | 2 | ## Event 3 | > Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient. 4 | 5 | 这是Node.Js官网对自身的介绍,明确强调了Node.Js使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。 6 | 7 | 而且在Node中大量核心模块都使用了Event的机制,因此可以说是整个Node里最重要的模块之一. 8 | 9 | 10 | ### 涉及源码 11 | - [lib/events.js](https://github.com/nodejs/node/blob/v6.0.0/lib/events.js) 12 | 13 | ### 观察者模式 14 | 15 | 16 | ![](https://upload.wikimedia.org/wikipedia/commons/thumb/8/8d/Observer.svg/854px-Observer.svg.png) 17 | 18 | 上图是 UML 的类图, 19 | 20 | 观察者模式是这样一种设计模式。一个被称作被观察者的对象,维护一组被称为观察者的对象,这些对象依赖于被观察者,被观察者自动将自身的状态的任何变化通知给它们。 21 | 22 | 当一个被观察者需要将一些变化通知给观察者的时候,它将采用广播的方式,这条广播可能包含特定于这条通知的一些数据。 23 | 24 | 使用观察者模式更深层次的动机是,当我们需要维护相关对象的一致性的时候,我们可以避免对象之间的紧密耦合。例如,一个对象可以通知另外一个对象,而不需要知道这个对象的信息。 25 | 26 | ### Event.js 实现 27 | EventEmitter 允许我们注册一个或多个函数作为 listeners。 在特定的事件触发时被调用。如下图: 28 | ![](https://github.com/yjhjstz/deep-into-node/blob/master/chapter7/2016-05-09%2014.13.19.png) 29 | #### listeners 存储 30 | 一般观察者的设计模式的实现逻辑是类似的,都是有一个类似map的结构,存储监听事件和回调函数的对应关系。 31 | ```js 32 | // This constructor is used to store event handlers. Instantiating this is 33 | // faster than explicitly calling `Object.create(null)` to get a "clean" empty 34 | // object (tested with v8 v4.9). 35 | function EventHandlers() {} 36 | EventHandlers.prototype = Object.create(null); 37 | EventEmitter.init = function() { 38 | ... 39 | if (!this._events || this._events === Object.getPrototypeOf(this)._events) { 40 | this._events = new EventHandlers(); 41 | this._eventsCount = 0; 42 | } 43 | 44 | this._maxListeners = this._maxListeners || undefined; 45 | }; 46 | ``` 47 | 在 EventEmitter 类中,以 键 / 值 对的方式来存储事件名和对应的监听器。 48 | 你可以会好奇,为什么创建一个最简单的 键 / 值 对搞的这么复杂,简单的一个 49 | `this._events = {};` 不就好咯。 50 | 51 | 是的,社区的最初实现是这样的,但随着 V8的升级,对 ES6支持的越来越完备,它的实现办法是使用一个空的构造函数,并且把这个构造的原型事先置空。 52 | 53 | 通过jsperf 比较两者的性能, 我们发现这种实现竟是简单实现性能的2倍! 54 | 55 | #### 增加事件监听 56 | addListener: 增加事件监听, on: addListener的别名,实际上是一样的。 57 | ```js 58 | 210 function _addListener(target, type, listener, prepend) { 59 | 211 var m; 60 | 212 var events; 61 | 213 var existing; 62 | 214 63 | 215 if (typeof listener !== 'function') 64 | 216 throw new TypeError('"listener" argument must be a function'); 65 | 217 66 | 218 events = target._events; 67 | 219 if (!events) { 68 | 220 events = target._events = new EventHandlers(); 69 | 221 target._eventsCount = 0; 70 | 222 } else { 71 | 223 ... 72 | 234 } 73 | 235 74 | 236 if (!existing) { 75 | 237 // Optimize the case of one listener. Don't need the extra array object. 76 | 238 existing = events[type] = listener; 77 | 239 ++target._eventsCount; 78 | 240 } else { 79 | 241 if (typeof existing === 'function') { 80 | 242 // Adding the second element, need to change to array. 81 | 243 existing = events[type] = prepend ? [listener, existing] : 82 | 244 [existing, listener]; 83 | 245 } else { 84 | 246 // If we've already got an array, just append. 85 | 247 if (prepend) { 86 | 248 existing.unshift(listener); 87 | 249 } else { 88 | 250 existing.push(listener); 89 | 251 } 90 | 252 } 91 | 253 92 | 254 // Check for listener leak 93 | 255 ... 94 | 264 } 95 | 265 96 | 266 return target; 97 | 267 } 98 | 99 | ``` 100 | 实际使用复杂场景时,会出现对回调顺序的需求。L250,默认添加监听是在事件监听数组的末尾。L247-L248,`prepend`标记是否在事件数组的前部添加。 101 | 102 | > 深入了解 https://github.com/nodejs/node/pull/6032 103 | 104 | #### 删除事件监听 105 | 在 EventEmitter#removeListener 这个 API 的实现里,需要从存储的监听器数组中除去一个元素,我们首先想到的就是使用 Array#splice 这个 API ,即 arr.splice(i, 1) 。不过这个 API 所提供的功能过于多了,它支持去除自定义数量的元素,还支持向数组中添加自定义的元素。所以,源码中选择自己实现一个最小可用的: 106 | ```js 107 | function spliceOne(list, index) { 108 | for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) 109 | list[i] = list[k]; 110 | list.pop(); 111 | } 112 | ``` 113 | 性能是原生调用的1.5倍。 114 | 115 | #### 事件触发 116 | 在事件触发时,监听器拥有的参数数量是任意的。 117 | ```js 118 | 136 EventEmitter.prototype.emit = function emit(type) { 119 | 137 var er, handler, len, args, i, events, domain; 120 | 138 var needDomainExit = false; 121 | 139 var doError = (type === 'error'); 122 | 140 123 | 141 events = this._events; 124 | 142 ... 125 | 169 126 | 170 handler = events[type]; 127 | 171 128 | 172 if (!handler) 129 | 173 return false; 130 | 174 ... 131 | 180 var isFn = typeof handler === 'function'; 132 | 181 len = arguments.length; 133 | 182 switch (len) { 134 | 183 // fast cases 135 | 184 case 1: 136 | 185 emitNone(handler, isFn, this); 137 | 186 break; 138 | 187 case 2: 139 | 188 emitOne(handler, isFn, this, arguments[1]); 140 | 189 break; 141 | 190 case 3: 142 | 191 emitTwo(handler, isFn, this, arguments[1], arguments[2]); 143 | 192 break; 144 | 193 case 4: 145 | 194 emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]); 146 | 195 break; 147 | 196 // slower 148 | 197 default: 149 | 198 args = new Array(len - 1); 150 | 199 for (i = 1; i < len; i++) 151 | 200 args[i - 1] = arguments[i]; 152 | 201 emitMany(handler, isFn, this, args); 153 | 202 } 154 | 206 ... 155 | 207 return true; 156 | ``` 157 | 把不定参数的函数调用转变成固定参数的函数调用,且最多支持到三个参数。超过3个参数则调用`emitMany`. 158 | 结果不言而喻,我们还是比较下会差多少,以三个参数为例: 159 | jsperf 显示的性能差距在1倍左右。 160 | 161 | > 深入了解 https://github.com/iojs/io.js/pull/601 162 | 163 | 164 | 165 | ### event在node中的应用 166 | #### 监控文件变化,通知感兴趣的观察者。 167 | 168 | ```js 169 | 1389 function FSWatcher() { 170 | 1390 EventEmitter.call(this); 171 | 1391 172 | 1392 var self = this; 173 | 1393 this._handle = new FSEvent(); 174 | 1394 this._handle.owner = this; 175 | 1395 176 | 1396 this._handle.onchange = function(status, event, filename) { 177 | 1397 if (status < 0) { 178 | 1398 self._handle.close(); 179 | 1399 const error = !filename ? 180 | 1400 errnoException(status, 'Error watching file for changes:') : 181 | 1401 errnoException(status, 182 | 1402 `Error watching file ${filename} for changes:`); 183 | 1403 error.filename = filename; 184 | 1404 self.emit('error', error); 185 | 1405 } else { 186 | 1406 self.emit('change', event, filename); 187 | 1407 } 188 | 1408 }; 189 | 1409 } 190 | 1410 util.inherits(FSWatcher, EventEmitter); 191 | ``` 192 | 193 | L1410, FSWatcher 对象继承 EventEmitter,使自身有了EventEmitter的方法。 194 | L1404, 当底层发生错误时,会发出通知事件 `error`。 195 | L1406, 文件发生变化时,FSWatcher 对象发射 `change`事件,具体的变化由 *event*标识,*filename*标识文件名。 196 | 197 | L1396, 挂在`FSEvent`对象上的方法 `onchange`作为 C++调用 Javascript 的回调,在不同的平台实现方式也不一样, 198 | 我们在文件系统章节将详细讲述。 199 | 200 | 上述是 fs 模块监听文件变化的实现,并导出API: `fs.watch()` 给外部使用,另外还有一个 `fs.watchFile()`。 201 | 我们查看官方文档: 202 | 203 | > fs.watchFile(filename, [options], listener) 204 | 205 | > Stability: 2 - Unstable. Use fs.watch instead, if available. 206 | 207 | > Watch for changes on filename. 208 | 209 | > fs.watch(filename, [options], [listener]) 210 | 211 | > Stability: 2 - Unstable. Not available on all platforms. 212 | 213 | - fs.watch() 官方建议使用。 214 | - fs.watch() 并不是全平台支持,只有 OSX 和 Windows 支持recursive选项。 215 | - fs.watch() 监听文件或目录, fs.watchFile() 监听文件。 216 | 217 | 218 | fs.watch() 如果传入 listener, 如下: 219 | ```js 220 | fs.watch('somedir', function (event, filename) { 221 | console.log('event is: ' + event); 222 | if (filename) { 223 | console.log('filename provided: ' + filename); 224 | } 225 | }); 226 | ``` 227 | 则默认添加函数 callback 到 `change`事件的观察者中。当然也可以换个姿势,如: 228 | 229 | ```js 230 | var watcher = fs.watch('somedir'); 231 | watcher.on('change', function (event, filename) { 232 | console.log('event is: ' + event); 233 | if (filename) { 234 | console.log('filename provided: ' + filename); 235 | } 236 | }).on('error', function(error) { 237 | 238 | }) 239 | ``` 240 | 可以实现链式调用, 比如符合目前很火的Reactive Programming。 241 | RP编程范式提高了编码的抽象程度,你可以更好地关注在商业逻辑中各种事件的联系避免大量细节而琐碎的实现,使得编码更加简洁。 242 | 243 | #### 逐行读取 (Readline) 244 | 245 | 我们来看看逐行读取对键盘输入的处理, 这涉及到比较复杂的状态机和事件发送,是学习事件模块非常好的一个例子。 246 | 247 | ```js 248 | 212 Interface.prototype._onLine = function(line) { 249 | 213 if (this._questionCallback) { 250 | 214 var cb = this._questionCallback; 251 | 215 this._questionCallback = null; 252 | 216 this.setPrompt(this._oldPrompt); 253 | 217 cb(line); 254 | 218 } else { 255 | 219 this.emit('line', line); 256 | 220 } 257 | 221 }; 258 | ``` 259 | 如果没有预先设定指定的query,然后用户应答后触发指定的callback,那么 `Interface`对象会触发 `line`事件。 260 | 在 input 流接受了一个 `\n` 时触发,通常在用户敲击回车或者返回时接收。 这是一个监听用户输入的利器。 261 | 监听 line 事件的示例: 262 | 263 | ```js 264 | var readline = require('readline'); 265 | var rl = readline.createInterface({ 266 | input: process.stdin, 267 | output: process.stdout 268 | }); 269 | rl.on('line', function (cmd) { 270 | console.log('You just typed: '+ cmd); 271 | }); 272 | ``` 273 | 274 | 该模块对复合功能按键,比如 Ctrl + c, Ctrl + z也做了相应的处理, 我们拿对 Ctrl + c 的代码进行分析: 275 | ```js 276 | 678 Interface.prototype._ttyWrite = function(s, key) { 277 | 679 key = key || {}; 278 | 680 279 | 681 // Ignore escape key - Fixes #2876 280 | 682 if (key.name == 'escape') return; 281 | 683 282 | 684 if (key.ctrl && key.shift) { 283 | 685 /* Control and shift pressed */ 284 | 686 switch (key.name) { 285 | 687 case 'backspace': 286 | 688 this._deleteLineLeft(); 287 | 689 break; 288 | 690 289 | 691 case 'delete': 290 | 692 this._deleteLineRight(); 291 | 693 break; 292 | 694 } 293 | 695 294 | 696 } else if (key.ctrl) { 295 | 697 /* Control key pressed */ 296 | 698 297 | 699 switch (key.name) { 298 | 700 case 'c': 299 | 701 if (this.listenerCount('SIGINT') > 0) { 300 | 702 this.emit('SIGINT'); 301 | 703 } else { 302 | 704 // This readline instance is finished 303 | 705 this.close(); 304 | 706 } 305 | 707 break; 306 | 708 省略... 307 | 709 } 308 | ``` 309 | - L681-L682, 忽略 `ESC` 键。 310 | - L684, 首先判断是否是 Ctrl 和 Shift复合键同时按下,如果是则L685-L694优先处理。 311 | - L696, 如果是按下 Ctrl 键,L699 继续判断,如果另一个是 `c` , 默认是关闭对象。 312 | - L701, 如果外部有观察者, 则发送 `SIGINT`事件,交由观察者处理。 313 | 314 | 315 | #### REPL 316 | 一个 Read-Eval-Print-Loop(REPL,读取-执行-输出循环)既可用于独立程序也可很容易地被集成到其它程序中。REPL 提供了一种交互地执行 JavaScript 并查看输出的方式。它可以被用作调试、测试或仅仅尝试某些东西。 317 | 318 | 在命令行中不带任何参数执行 node 您便会进入 REPL。它提供了一个简单的 Emacs 行编辑。 319 | 320 | REPLServer 继承 Interface,如代码所示: `inherits(REPLServer, rl.Interface);` 321 | 322 | 并监听 line 事件, 自定义关键字,以支持交互式的命令。 323 | ```shell 324 | $ NODE_DEBUG=REPL node 325 | REPL 37391: line ".help" 326 | break Sometimes you get stuck, this gets you out 327 | clear Alias for .break 328 | exit Exit the repl 329 | help Show repl options 330 | load Load JS from a file into the REPL session 331 | save Save all evaluated commands in this REPL session to a file 332 | ``` 333 | 334 | 我们看下代码实现: 335 | ```js 336 | 399 self.on('line', function(cmd) { 337 | 400 debug('line %j', cmd); 338 | 401 sawSIGINT = false; 339 | 402 340 | 403 // leading whitespaces in template literals should not be trimmed. 341 | 404 if (self._inTemplateLiteral) { 342 | 405 self._inTemplateLiteral = false; 343 | 406 } else { 344 | 407 cmd = self.lineParser.parseLine(cmd); 345 | 408 } 346 | 409 347 | 410 // Check to see if a REPL keyword was used. If it returns true, 348 | 411 // display next prompt and return. 349 | 412 if (cmd && cmd.charAt(0) === '.' && isNaN(parseFloat(cmd))) { 350 | 413 var matches = cmd.match(/^\.([^\s]+)\s*(.*)$/); 351 | 414 var keyword = matches && matches[1]; 352 | 415 var rest = matches && matches[2]; 353 | 416 if (self.parseREPLKeyword(keyword, rest) === true) { 354 | 417 return; 355 | 418 } else if (!self.bufferedCommand) { 356 | 419 self.outputStream.write('Invalid REPL keyword\n'); 357 | 420 finish(null); 358 | 421 return; 359 | 422 } 360 | 423 } 361 | 424 ... 362 | 425 } 363 | ``` 364 | - L400, 通过设置环境变量NODE_DEBUG=REPL打开调试功能。 365 | - L407, 解析 cmd 输入, 处理正则的情况。 366 | - L412, 查看是否以 `.`开头,并且不是浮点数,则利用正则匹配字符串, 367 | - 以 .help 为例,得到的 `matches` 为 `[ '.help', 'help', '', index: 0, input: '.help' ]`, 368 | keyword 为 help, rest 为 ''. 369 | - L416, 通过 keyword 从 commands 对象找到对应的方法执行。 370 | 371 | 372 | 373 | #### REPL实例 374 | 一个在curl(1)上运行的REPL实例的例子可以查看这里: https://gist.github.com/2053342 375 | 376 | 377 | 378 | ### EventEmitter vs Callbacks 379 | - EventEmitter 380 | - 可以通知多个listeners 381 | - 一般被调用多次。 382 | - Callback 383 | - 最多通知一个listener 384 | - 通常被调用一次,无论操作是成功还是失败。 385 | 386 | ### 总结 387 | Event 模块是观察者设计模式的典型应用。同时也是Reactive Programming的精髓所在。 388 | 389 | 390 | ### 参考 391 | [1].https://segmentfault.com/a/1190000005051034 392 | 393 | -------------------------------------------------------------------------------- /chapter2/chapter2-2.md: -------------------------------------------------------------------------------- 1 | 2 | ### 模块 3 | > If V8 is the engine of Node.js, npm is its soul! 4 | 5 | npm 世界最大的模块仓库,我们看几个数据: 6 | * ~21 万模块数量 7 | * 每天亿级模块下载量 8 | * 每周 10 亿级的模块下载量 9 | 10 | 由此诞生了一家做 npm 包管理的公司 `npmjs.com`. 11 | 12 | 13 | ### 模块加载准备操作 14 | 严格来讲,Node 里面分以下几种模块: 15 | 16 | * builtin module: Node 中以 c++ 形式提供的模块,如 tcp_wrap、contextify 等 17 | * constants module: Node 中定义常量的模块,用来导出如 signal, openssl 库、文件访问权限等常量的定义。如文件访问权限中的 O_RDONLY,O_CREAT、signal 中的 SIGHUP,SIGINT 等。 18 | * native module: Node 中以 JavaScript 形式提供的模块,如 http,https,fs 等。有些 native module 需要借助于 builtin module 实现背后的功能。如对于 native 模块 buffer , 还是需要借助 builtin node_buffer.cc 中提供的功能来实现大容量内存申请和管理,目的是能够脱离 V8 内存大小使用限制。 19 | * 3rd-party module: 以上模块可以统称 Node 内建模块,除此之外为第三方模块,典型的如 express 模块。 20 | 21 | ### builtin module 和 native module 生成过程 22 | ![](FgrfI3a1NyQLu0FoX76R5DbjdoL0.png) 23 | native JS module 的生成过程相对复杂一点,把 node 的源代码下载下来,自己编译后,会在 out/Release/obj/ 24 | gen 目录下生成一个文件 `node_natives.h`。 25 | 26 | 该文件由 js2c.py 生成。 js2c.py 会将 node 源代码中的 lib 目录下所有 js 文件以及 src 目录下的 node.js 文件中每一个字符转换成对应的 ASCII 码,并存放在相应的数组里面。 27 | 28 | ```c++ 29 | namespace node { 30 | const char node_native[] = {47, 47, 32, 67, 112 …} 31 | 32 | const char console_native[] = {47, 47, 32, 67, 112 …} 33 | 34 | const char buffer_native[] = {47, 47, 32, 67, 112 …} 35 | 36 | … 37 | 38 | } 39 | 40 | struct _native {const char name; const char* source; size_t source_len;}; 41 | 42 | static const struct _native natives[] = {{ “node”, node_native, sizeof(node_native)-1 }, 43 | 44 | {“dgram”, dgram_native, sizeof(dgram_native)-1 }, 45 | 46 | {“console”, console_native, sizeof(console_native)-1 }, 47 | 48 | {“buffer”, buffer_native, sizeof(buffer_native)-1 }, 49 | 50 | … 51 | 52 | } 53 | ``` 54 | * builtin C++ module 生成过程较为简单。每个 builtin C++ 模块的入口,都会通过宏 NODE_MODULE_CONTEXT_AWARE_BUILTIN 扩展为一个函数。例如对于 tcp_wrap 模块而言,会被扩展为函数 static void _register_tcp_wrap (void) attribute((constructor))。熟悉 GCC 的同学会知道通过 attribute((constructor)) 修饰的函数会在 node 的 main() 函数之前被执行,也就是说,我们的 builtin C++ 模块会被 main() 函数之前被加载进 modlist_builtin 链表。modlist_builtin 是一个 struct node_module 类型的指针,以它为头,get_builtin_module() 会遍历查找我们需要的模块。 55 | 56 | * 对于 node 自身提供的模块,其实无论是 native JS 模块还是 builtin C++ 模块,最终都在编译生成可执行文件时,嵌入到了 ELF 格式的二进制文件 node 里面。 57 | * 而对这两者的提取方式却不一样。对于 JS 模块,使用 process.binding(“natives”),而对于 C++ 模块则直接用 get_builtin_module() 得到,这部分会在 1.2 节讲述。 58 | 59 | 60 | ### module binding 61 | 62 | 在 node.cc 里面提供了一个函数 Binding()。当我们的应用或者 node 内建的模块调用 require() 来引用另一个模块时,背后的支撑者即是这里提到的 Binding() 函数。后面会讲述这个函数如何支撑 require() 的。这里先主要剖析这个函数。 63 | 64 | ```c++ 65 | static void Binding(const FunctionCallbackInfo& args) { 66 | Environment* env = Environment::GetCurrent(args); 67 | 68 | Local module = args[0]->ToString(env->isolate()); 69 | node::Utf8Value module_v(env->isolate(), module); 70 | 71 | Local cache = env->binding_cache_object(); 72 | Local exports; 73 | 74 | if (cache->Has(module)) { 75 | exports = cache->Get(module)->ToObject(env->isolate()); 76 | args.GetReturnValue().Set(exports); 77 | return; 78 | } 79 | 80 | // Append a string to process.moduleLoadList 81 | char buf[1024]; 82 | snprintf(buf, sizeof(buf), "Binding %s", *module_v); 83 | 84 | Local modules = env->module_load_list_array(); 85 | uint32_t l = modules->Length(); 86 | modules->Set(l, OneByteString(env->isolate(), buf)); 87 | 88 | node_module* mod = get_builtin_module(*module_v); 89 | if (mod != nullptr) { 90 | exports = Object::New(env->isolate()); 91 | // Internal bindings don't have a"module" object, only exports. 92 | CHECK_EQ(mod->nm_register_func, nullptr); 93 | CHECK_NE(mod->nm_context_register_func, nullptr); 94 | Local unused = Undefined(env->isolate()); 95 | // **for builtin module** 96 | mod->nm_context_register_func(exports, unused, 97 | env->context(), mod->nm_priv); 98 | cache->Set(module, exports); 99 | } else if (!strcmp(*module_v,"constants")) { 100 | exports = Object::New(env->isolate()); 101 | // for constants 102 | DefineConstants(exports); 103 | cache->Set(module, exports); 104 | } else if (!strcmp(*module_v,"natives")) { 105 | exports = Object::New(env->isolate()); 106 | // for native module 107 | DefineJavaScript(env, exports); 108 | cache->Set(module, exports); 109 | } else { 110 | char errmsg[1024]; 111 | snprintf(errmsg, 112 | sizeof(errmsg), 113 | "No such module: %s", 114 | *module_v); 115 | return env->ThrowError(errmsg); 116 | } 117 | 118 | args.GetReturnValue().Set(exports); 119 | } 120 | ``` 121 | * builtin 优先级最高。对于任何一个需要绑定的模块,都会优先到 builtin 模块列表 modlist_builtin 中去查找。查找过程非常简单,直接遍历这个列表,找到模块名字相同的那个模块即可。找到这个模块后,模块的注册函数会先被执行,且将一个重要的数据 exports 返回。对于 builtin module 而言,exports object 包含了 builtin C++ 模块暴露出来的接口名以及对于的代码。例如对模块 tcp_wrap 而言,exports 包含的内容可以用如下格式表示: {“TCP”: “/function code of TCPWrap entrance/”, “TCPConnectWrap”: “/function code of TCPConnectWrap entrance/”}。 122 | 123 | * constants 模块优先级次之。node 中的常量定义通过 constants 导出。导出的 exports 格式如下: {“SIGHUP”:1, “SIGKILL”:9, “SSL_OP_ALL”: 0x80000BFFL} 124 | 125 | * 对于 native module 而言,图 3 中除了数组 node_native 之外,所有的其它模块都会导出到 exports。格式如下: {“_debugger”: _debugger_native , “module”: module_native ,“config”: config_native } 其中,_debugger_native,module_native 等为数组名,或者说就是内存地址。 126 | 127 | 对比上面三类模块导出的 exports 结构会发现对于每个属性,它们的值代表着完全不同的意义。对于 builtin 模块而言,exports 的 TCP 属性值代表着函数代码入口,对于 constants 模块,SIGHUP 的属性值则代表一个数字,而对于 native 模块,_debugger 的属性值则代表内存地址(准确说应该是 .rodata 段地址)。 128 | 129 | 130 | ### 模块加载 131 | 我们仍旧从 `var http = require('http');` 说起。 132 | 133 | require 是怎么来的,为什么平白无故就能用呢,实际上都干了些什么? 134 | 135 | * [lib/module.js](https://github.com/nodejs/node/blob/v4.4.0/lib/module.js) 的中有如下代码。 136 | ```js 137 | // Loads a module at the given file path. Returns that module's 138 | // `exports` property. 139 | Module.prototype.require = function(path) { 140 | assert(path,'missing path'); 141 | assert(typeof path ==='string','path must be a string'); 142 | return Module._load(path, this); 143 | }; 144 | ``` 145 | 146 | 首先 assert 模块进行简单的 path 变量的判断,需要传入的 `path` 是一个 string 类型。 147 | 148 | ```js 149 | // Check the cache for the requested file. 150 | // 1. If a module already exists in the cache: return its exports object. 151 | // 2. If the module is native: call `NativeModule.require()` with the 152 | // filename and return the result. 153 | // 3. Otherwise, create a new module for the file and save it to the cache. 154 | // Then have it load the file contents before returning its exports 155 | // object. 156 | Module._load = function(request, parent, isMain) { 157 | if (parent) { 158 | debug('Module._load REQUEST %s parent: %s', request, parent.id); 159 | } 160 | 161 | var filename = Module._resolveFilename(request, parent); 162 | 163 | var cachedModule = Module._cache[filename]; 164 | if (cachedModule) { 165 | return cachedModule.exports; 166 | } 167 | 168 | if (NativeModule.nonInternalExists(filename)) { 169 | debug('load native module %s', request); 170 | return NativeModule.require(filename); 171 | } 172 | 173 | var module = new Module(filename, parent); 174 | 175 | if (isMain) { 176 | process.mainModule = module; 177 | module.id = '.'; 178 | } 179 | 180 | Module._cache[filename] = module; 181 | 182 | var hadException = true; 183 | 184 | try { 185 | module.load(filename); 186 | hadException = false; 187 | } finally { 188 | if (hadException) { 189 | delete Module._cache[filename]; 190 | } 191 | } 192 | 193 | return module.exports; 194 | }; 195 | ``` 196 | * 如果模块在缓存中,返回它的 exports 对象。 197 | * 如果是原生的模块,通过调用 `NativeModule.require()` 返回结果。 198 | * 否则,创建一个新的模块,并保存到缓存中。 199 | 200 | 让我们再深度遍历的方式查看代码到 `NativeModule.require`. 201 | ```js 202 | NativeModule.require = function(id) { 203 | if (id =='native_module') { 204 | return NativeModule; 205 | } 206 | 207 | var cached = NativeModule.getCached(id); 208 | if (cached) { 209 | return cached.exports; 210 | } 211 | 212 | if (!NativeModule.exists(id)) { 213 | throw new Error('No such native module '+ id); 214 | } 215 | 216 | process.moduleLoadList.push('NativeModule' + id); 217 | 218 | var nativeModule = new NativeModule(id); 219 | 220 | nativeModule.cache(); 221 | nativeModule.compile(); 222 | 223 | return nativeModule.exports; 224 | }; 225 | ``` 226 | 227 | 我们看到,缓存的策略这个贯穿在 node 的实现中。 228 | 229 | * 同样的,如果在 cache 中存在,则直接返回 exports 对象。 230 | * 如果不在,则加入到 `moduleLoadList` 数组中,创建新的 NativeModule 对象。 231 | 232 | 下面是最关键的一句 233 | ```js 234 | nativeModule.compile(); 235 | ``` 236 | 237 | 具体实现在 `node.js` 中: 238 | ```js 239 | NativeModule.getSource = function(id) { 240 | return NativeModule._source[id]; 241 | }; 242 | 243 | NativeModule.wrap = function(script) { 244 | return NativeModule.wrapper[0] + script + NativeModule.wrapper[1]; 245 | }; 246 | 247 | NativeModule.wrapper = ['(function (exports, require, module, __filename, __dirname) {','\n});' ]; 248 | 249 | NativeModule.prototype.compile = function() { 250 | var source = NativeModule.getSource(this.id); 251 | source = NativeModule.wrap(source); 252 | 253 | var fn = runInThisContext(source, { 254 | filename: this.filename, 255 | lineOffset: 0 256 | }); 257 | fn(this.exports, NativeModule.require, this, this.filename); 258 | 259 | this.loaded = true; 260 | }; 261 | ``` 262 | 263 | `wrap` 函数将 http.js 包裹起来, 交由 `runInThisContext` 编译源码,返回 fn 函数, 依次将参数传入。 264 | 265 | 266 | 267 | ### process 268 | 先看看 node.js 的底层 C++ 传递给 javascript 的一个变量 process,在一开始运行 node.js 时,程序会先配置好 process 269 | `Handleprocess = SetupProcessObject(argc, argv);` 270 | * 然后把 process 作为参数去调用 js 主程序 src/node.js 返回的函数,这样 process 就传递到 javascript 里了。 271 | 272 | ```js 273 | //node.cc 274 | 275 | // 通过 MainSource() 获取已转化的 src/node.js 源码,并执行它 276 | 277 | Local f_value = ExecuteString(MainSource(), IMMUTABLE_STRING(“node.js”)); 278 | // 执行 src/node.js 后获得的是一个函数,从 node.js 源码可以看出: 279 | 280 | //node.js 281 | 282 | //(function(process) { 283 | 284 | // global = this; 285 | 286 | // … 287 | 288 | //}) 289 | 290 | Local f = Local::Cast(f_value); 291 | // 创建函数执行环境,调用函数,把 process 传入 292 | 293 | Localglobal = v8::Context::GetCurrent()->Global(); 294 | 295 | Local args[1] = { 296 | Local::New(process) 297 | }; 298 | 299 | f->Call(global, 1, args); 300 | ``` 301 | 302 | ### vm 303 | `runInThisContext` 又是怎么一回事呢? 304 | ```js 305 | var ContextifyScript = process.binding('contextify').ContextifyScript; 306 | function runInThisContext(code, options) { 307 | var script = new ContextifyScript(code, options); 308 | return script.runInThisContext(); 309 | } 310 | ``` 311 | 312 | * node.cc 的 Binding 中有如下调用,对模块进行注册, 313 | `mod->nm_context_register_func(exports, unused, env->context(), mod->nm_priv);` 314 | 315 | 我们看下 node.h 中 mod 数据结构的定义: 316 | 317 | ```c++ 318 | struct node_module { 319 | int nm_version; 320 | unsigned int nm_flags; 321 | void* nm_dso_handle; 322 | const char* nm_filename; 323 | node::addon_register_func nm_register_func; 324 | node::addon_context_register_func nm_context_register_func; 325 | const char* nm_modname; 326 | void* nm_priv; 327 | struct node_module* nm_link; 328 | }; 329 | ``` 330 | 331 | * node.h 中还有如下宏定义,接着往下看! 332 | 333 | ```c++ 334 | #define NODE_MODULE_CONTEXT_AWARE_X(modname, regfunc, priv, flags) \ 335 | extern "C" { \ 336 | static node::node_module _module = \ 337 | { \ 338 | NODE_MODULE_VERSION, \ 339 | flags, \ 340 | NULL, \ 341 | __FILE__, \ 342 | NULL, \ 343 | (node::addon_context_register_func) (regfunc), \ 344 | NODE_STRINGIFY(modname), \ 345 | priv, \ 346 | NULL \ 347 | }; \ 348 | NODE_C_CTOR(_register_ ## modname) { \ 349 | node_module_register(&_module); \ 350 | } \ 351 | } 352 | 353 | #define NODE_MODULE_CONTEXT_AWARE_BUILTIN(modname, regfunc) \ 354 | NODE_MODULE_CONTEXT_AWARE_X(modname, regfunc, NULL, NM_F_BUILTIN) \ 355 | ``` 356 | 357 | * node_contextify.cc 中有如下宏调用,终于看清楚了!结合前面几点,实际上就是把 node_module 的 nm_context_register_func 与 node::InitContextify 进行了绑定。 358 | 359 | ```js 360 | NODE_MODULE_CONTEXT_AWARE_BUILTIN(contextify, node::InitContextify); 361 | ``` 362 | 我们回溯而上,通过 `node_module_register(&_module);`,`process.binding('contextify')`--> `mod->nm_context_register_func(exports, unused, env->context(), mod->nm_priv);` --> `node::InitContextify()`. 363 | 364 | 这样通过 `env->SetProtoMethod(script_tmpl,"runInThisContext", RunInThisContext);`,绑定了『runInThisContext』 和 `RunInThisContext`. 365 | 366 | runInThisContext 是将被包装后的源字符串转成可执行函数,(runInThisContext 来自 contextify 模块),runInThisContext 的作用,类似 eval,再执行这个被 eval 后的函数。 367 | 368 | 这样就成功加载了 `native` 模块, 标记 this.loaded = true; 369 | 370 | #### 总结 371 | Node.js 通过 cache 解决无限循环引用的问题, 也是系统优化的重要手段,通过以空间换时间,使得每次加载模块变得非常高效。 372 | 373 | 在实际的业务开发中,我们从堆的角度观察 node 启动模块后,缓存了大量的模块,包括第三方的模块,有的可能只加载使用一次。笔者觉得有必要有一种模块的卸载机制 [1], 374 | 可以降低对 V8 堆内存的占用,从而提升后续垃圾回收的效率。 375 | 376 | #### 参考 377 | [1].https://github.com/nodejs/node/issues/5895 378 | 379 | 380 | -------------------------------------------------------------------------------- /chapter3/chapter3-1.md: -------------------------------------------------------------------------------- 1 | 2 | ## Timer源码解读 3 | 4 | ### 涉及源码 5 | * [lib/timers.js](https://github.com/nodejs/node/blob/master/lib/timers.js) 6 | * [src/timer_wrap.cc](https://github.com/nodejs/node/blob/master/src/timer_wrap.cc) 7 | * [deps/uv/src/unix/timer.c](https://github.com/nodejs/node/blob/master/deps/uv/src/unix/timer.c) 8 | * [deps/uv/src/heap-inl.h](https://github.com/nodejs/node/blob/master/deps/uv/src/heap-inl.h) 9 | 10 | 主要分为 javascript 层面的实现和 `libuv` 层面的实现, 而timer_wrap.cc 作为一个bridge,完成 javascript 和 C++的交互调用。 11 | 12 | ### 使用场景 13 | 定时器主要的使用场景或者说适用场景: 14 | * 定时任务,比如业务中定时检查状态等; 15 | * 超时控制,比如网络超时控制重传。 16 | 17 | 在 node.js 的实现中, 18 | ```js 19 | function responseOnEnd() { 20 | // 省略 21 | debug('AGENT socket keep-alive'); 22 | if (req.timeoutCb) { 23 | socket.setTimeout(0, req.timeoutCb); 24 | req.timeoutCb = null; 25 | } 26 | } 27 | ```` 28 | 你可能会有疑问:为啥在 HTTP 模块要用呢? 29 | 30 | 我们知道HTTP协议采用“请求-应答”模式,当使用普通模式,即非KeepAlive模式时,每个请求/应答客户和服务器都要新建一个连接,完成之后立即断开连接(HTTP协议为无连接的协议);当使用Keep-Alive模式(又称持久连接、连接重用)时,Keep-Alive功能使客户端到服务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive功能避免了建立或者重新建立连接。 31 | 32 | ```js 33 | if (req.httpVersionMajor < 1 || req.httpVersionMinor < 1) { 34 | this.useChunkedEncodingByDefault = chunkExpression.test(req.headers.te); 35 | this.shouldKeepAlive = false; 36 | } 37 | ``` 38 | HTTP/1.0中默认是关闭的,需要在http头加入"Connection: Keep-Alive",才能启用Keep-Alive;http/1.1中默认启用Keep-Alive,如果加入"Connection: close",才关闭。 39 | 40 | 目前大部分浏览器都是用HTTP/1.1协议,也就是说默认都会发起Keep-Alive的连接请求了,Node.js 针对2种协议按上述代码做了判断处理。 41 | 42 | 当然了,这个连接不能就这么一直保持着,所以一般都会有一个超时时间,超过这个时间客户端还没有发送新的http请求,那么服务器就需要自动断开从而继续为其他客户端提供服务。 43 | Node.js的HTTP 模块对于每一个新的连接创建一个 socket 对象,调用socket.setTimeout设置一个定时器用于超时后自动断开连接。 44 | 45 | ### 数据结构选择 46 | 一个Timer本质上是这样的一个数据结构:deadline越近的任务拥有越高优先级,提供以下3种基本操作: 47 | * schedule 新增任务 48 | * cancel 删除任务 49 | * expire 执行到期的任务 50 | 51 | 实现方式 | schedule | cancel | expire 52 | -------| ----------| -------- | ------ 53 | 基于链表 | O(1) | O(n) | O(n) 54 | 基于排序链表 | O(n)| O(1) | O(1) 55 | 基于最小堆| O(lgn)| O(1) | O(1) 56 | 基于时间轮| O(1) | O(1) | O(1) 57 | 58 | timer 的实现历经变迁,每次变迁都是思维碰撞的火花,让我们走进源码,细细品味。 59 | 60 | 61 | ### libuv 实现 62 | #### 数据结构-最小堆 63 | 最小堆首先是二叉堆,二叉堆是完全二元树或者是近似完全二元树,它分为两种:最大堆和最小堆。 64 | 最大堆:父结点的键值总是大于或等于任何一个子节点的键值;最小堆:父结点的键值总是小于或等于任何一个子节点的键值。示意图如下: 65 | ![binary-tree](http://images.cnitblog.com/i/497634/201403/182339209436216.jpg) 66 | 67 | 节点定义在deps/uv/src/heap-inl.h,如下: 68 | ```c 69 | struct heap_node { 70 | struct heap_node* left; 71 | struct heap_node* right; 72 | struct heap_node* parent; 73 | }; 74 | ``` 75 | 根节点定义: 76 | ```c 77 | /* A binary min heap. The usual properties hold: the root is the lowest 78 | * element in the set, the height of the tree is at most log2(nodes) and 79 | * it's always a complete binary tree. 80 | * 81 | * The heap function try hard to detect corrupted tree nodes at the cost 82 | * of a minor reduction in performance. Compile with -DNDEBUG to disable. 83 | */ 84 | struct heap { 85 | struct heap_node* min; 86 | unsigned int nelts; 87 | }; 88 | ``` 89 | 这边我们可以清楚的看到,最小堆采用指针组织数据,而不是数组。`min`始终指向最小的节点如果存在的话。作为一个排序的集合,它还需要一个用户指定的比较函数,决定哪个节点更小,或者说当过期时间一样时,决定他们的次序。毕竟没有规则不成方圆。 90 | ```c 91 | static int timer_less_than(const struct heap_node* ha, 92 | const struct heap_node* hb) { 93 | const uv_timer_t* a; 94 | const uv_timer_t* b; 95 | 96 | a = container_of(ha, const uv_timer_t, heap_node); 97 | b = container_of(hb, const uv_timer_t, heap_node); 98 | 99 | if (a->timeout < b->timeout) 100 | return 1; 101 | if (b->timeout < a->timeout) 102 | return 0; 103 | 104 | /* Compare start_id when both have the same timeout. start_id is 105 | * allocated with loop->timer_counter in uv_timer_start(). 106 | */ 107 | if (a->start_id < b->start_id) 108 | return 1; 109 | if (b->start_id < a->start_id) 110 | return 0; 111 | 112 | return 0; 113 | } 114 | ``` 115 | 116 | 这边我们可以看到,首先比较两者的 `timeout` ,如果二者一样,则比较二者被`schedule`的 id, 该 id 由 `loop->timer_counter` 递增生成,在调用 117 | `uv_timer_start`时赋值给`start_id`. 118 | 119 | 120 | 121 | #### 具体实现 122 | ```c 123 | 62 int uv_timer_start(uv_timer_t* handle, 124 | 63 uv_timer_cb cb, 125 | 64 uint64_t timeout, 126 | 65 uint64_t repeat) { 127 | 66 uint64_t clamped_timeout; 128 | 67 129 | 68 if (cb == NULL) 130 | 69 return -EINVAL; 131 | 70 132 | 71 if (uv__is_active(handle)) 133 | 72 uv_timer_stop(handle); 134 | 73 135 | 74 clamped_timeout = handle->loop->time + timeout; 136 | 75 if (clamped_timeout < timeout) 137 | 76 clamped_timeout = (uint64_t) -1; 138 | 77 139 | 78 handle->timer_cb = cb; 140 | 79 handle->timeout = clamped_timeout; 141 | 80 handle->repeat = repeat; 142 | 81 /* start_id is the second index to be compared in uv__timer_cmp() */ 143 | 82 handle->start_id = handle->loop->timer_counter++; 144 | 83 145 | 84 heap_insert((struct heap*) &handle->loop->timer_heap, 146 | 85 (struct heap_node*) &handle->heap_node, 147 | 86 timer_less_than); 148 | 87 uv__handle_start(handle); 149 | 88 150 | 89 return 0; 151 | 90 } 152 | ``` 153 | * L68-L69, 做参数的检查,错误则返回 -EINVAL。 154 | * L71-L72,如有是一个活跃的 timer, 则立即停止它。 155 | * L74-L82, 参数赋值,上面提到的`start_id` 就是由`timer_counter`自增得到。 156 | * L84-L86, 插入 timer 节点到最小堆,此处算法复杂度为 O(lgn)。 157 | * L87, 标记句柄非活跃,并加入统计。 158 | 159 | ```c 160 | 93 int uv_timer_stop(uv_timer_t* handle) { 161 | 94 if (!uv__is_active(handle)) 162 | 95 return 0; 163 | 96 164 | 97 heap_remove((struct heap*) &handle->loop->timer_heap, 165 | 98 (struct heap_node*) &handle->heap_node, 166 | 99 timer_less_than); 167 | 100 uv__handle_stop(handle); 168 | 101 169 | 102 return 0; 170 | 103 } 171 | ``` 172 | L94,检查 handle, 如果是非活跃的,则说明没有启动过,则返回成功。 173 | L97-L99, 从最小堆中删除 timer的节点。 174 | L100, 重置句柄,并减少计数。 175 | 176 | 了解了如何开启和关闭一个定时器,我们看如何调度定时器。 177 | 178 | ```c 179 | int uv_run(uv_loop_t* loop, uv_run_mode mode) { 180 | ... 181 | while (r != 0 && loop->stop_flag == 0) { 182 | uv__update_time(loop); 183 | uv__run_timers(loop); 184 | ran_pending = uv__run_pending(loop); 185 | ... 186 | } 187 | ``` 188 | 在 node.js 的 event loop 中,更新时间后则立即调用`uv__run_timers`,可见 timer 作为一个外部系统依赖的模块,优先级是最高的。 189 | 190 | ```c 191 | 150 void uv__run_timers(uv_loop_t* loop) { 192 | 151 struct heap_node* heap_node; 193 | 152 uv_timer_t* handle; 194 | 153 195 | 154 for (;;) { 196 | 155 heap_node = heap_min((struct heap*) &loop->timer_heap); 197 | 156 if (heap_node == NULL) 198 | 157 break; 199 | 158 200 | 159 handle = container_of(heap_node, uv_timer_t, heap_node); 201 | 160 if (handle->timeout > loop->time) 202 | 161 break; 203 | 162 204 | 163 uv_timer_stop(handle); 205 | 164 uv_timer_again(handle); 206 | 165 handle->timer_cb(handle); 207 | 166 } 208 | 167 } 209 | ``` 210 | 211 | L155-L157, 取出最小的timer节点,如果为空,则跳出循环。 212 | L159-L161, 通过 heap_node 的偏移拿到对象的首地址,如果最小的 timeout时间大于当前的时间,则说明过期时间还没到,则退出循环。 213 | L163-L165, 删除 timer, 如果是需要重复执行的定时器,则通过调用`uv_timer_again`再次加入, L165执行 timer的 callback 任务后循环。 214 | 215 | #### 改进的分级时间轮实现 216 | https://github.com/libuv/libuv/pull/823 217 | 218 | 219 | ### 桥接层 220 | 阅读此节需要node.js addon 的知识,这边默认你已经了解。 221 | ```c 222 | 43 env->SetProtoMethod(constructor, "start", Start); 223 | 44 env->SetProtoMethod(constructor, "stop", Stop); 224 | 45 225 | 46 target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "Timer"), 226 | 47 constructor->GetFunction()); 227 | ``` 228 | Timer 的 addon 导出`start`,`stop`的方法,供js 层调用。 229 | ```c++ 230 | 71 static void Start(const FunctionCallbackInfo& args) { 231 | 72 TimerWrap* wrap = Unwrap(args.Holder()); 232 | 73 233 | 74 CHECK(HandleWrap::IsAlive(wrap)); 234 | 75 235 | 76 int64_t timeout = args[0]->IntegerValue(); 236 | 77 int64_t repeat = args[1]->IntegerValue(); 237 | 78 int err = uv_timer_start(&wrap->handle_, OnTimeout, timeout, repeat); 238 | 79 args.GetReturnValue().Set(err); 239 | 80 } 240 | 81 241 | 82 static void Stop(const FunctionCallbackInfo& args) { 242 | 83 TimerWrap* wrap = Unwrap(args.Holder()); 243 | 84 244 | 85 CHECK(HandleWrap::IsAlive(wrap)); 245 | 86 246 | 87 int err = uv_timer_stop(&wrap->handle_); 247 | 88 args.GetReturnValue().Set(err); 248 | 89 } 249 | ``` 250 | 251 | `Start`需要提供两个参数,1.超时时间 timeout; 2. 重复执行的周期。 252 | L78 调用`uv_timer_start`,其中 `OnTimeout`是该定时器的回调函数。 253 | 我们看下该函数实现: 254 | ```c++ 255 | 91 static void OnTimeout(uv_timer_t* handle) { 256 | 92 TimerWrap* wrap = static_cast(handle->data); 257 | 93 Environment* env = wrap->env(); 258 | 94 HandleScope handle_scope(env->isolate()); 259 | 95 Context::Scope context_scope(env->context()); 260 | 96 wrap->MakeCallback(kOnTimeout, 0, nullptr); 261 | 97 } 262 | ``` 263 | 你可能好奇,怎么就由 handle->data 取到对象指针了呢? 264 | ```c++ 265 | HandleWrap::HandleWrap(Environment* env, 266 | Local object, 267 | uv_handle_t* handle, 268 | AsyncWrap::ProviderType provider, 269 | AsyncWrap* parent) 270 | : AsyncWrap(env, object, provider, parent), 271 | flags_(0), 272 | handle__(handle) { 273 | handle__->data = this; 274 | ... 275 | } 276 | ``` 277 | 由于`TimerWrap`继承自`HandleWrap`,对象构造时就把 `handle` 的私有变量 `data` 指向了 this 指针,也就是`HandleWrap`。回调函数通过强转获取了 `TimerWrap` 对象。 278 | 279 | 令人感兴趣的是 L96,这边是由 C++ 调用 jsland. 查看该处的修改历史,笔者发现: 280 | 281 | > 282 | > timers: dispatch ontimeout callback by array index 283 | > 284 | > Achieve a minor speed-up by looking up the timeout callback on the timer 285 | > object by using an array index rather than a named property. 286 | 287 | > Gives a performance boost of about 1% on the misc/timers benchmarks. 288 | 289 | 之前的实现是属性查找,而通过极致的优化,属性查找被替换成数组索引, 290 | benchmark性能提升了1%。 而整个系统性能的提升正是来源于这点滴的积累。 291 | 292 | ### timers.js 293 | 有了桥接层,js便有了开启、关闭一个定时器的能力。 294 | 295 | 为了不影响到nodejs中的event loop,timer模块专门提供了一些内部的api:`timers._unrefActive` 给像socket这样的对象使用。 296 | 297 | 在最初的设计中,每次执行_unrefActive添加任务时都会维持着unrefList的顺序,保证超时时间最小的处于前面。这样在定时器超时后便可以以最快的速度处理超时任务并设置下一个定时器,但是在添加任务时最坏的情况下需要遍历unrefList链表中的所有节点。 298 | 299 | ```js 300 | 517 exports._unrefActive = function(item) { 301 | 518 var msecs = item._idleTimeout; 302 | 519 if (!msecs || msecs < 0) return; 303 | 520 assert(msecs >= 0); 304 | 521 305 | 522 L.remove(item); 306 | 523 307 | 524 if (!unrefList) { 308 | 525 debug('unrefList initialized'); 309 | 526 unrefList = {}; 310 | 527 L.init(unrefList); 311 | 528 312 | 529 debug('unrefTimer initialized'); 313 | 530 unrefTimer = new Timer(); 314 | 531 unrefTimer.unref(); 315 | 532 unrefTimer.when = -1; 316 | 533 unrefTimer[kOnTimeout] = unrefTimeout; 317 | 534 } 318 | 535 319 | 536 var now = Timer.now(); 320 | 537 item._idleStart = now; 321 | 538 322 | 539 if (L.isEmpty(unrefList)) { 323 | 540 debug('unrefList empty'); 324 | 541 L.append(unrefList, item); 325 | 542 326 | 543 unrefTimer.start(msecs, 0); 327 | 544 unrefTimer.when = now + msecs; 328 | 545 debug('unrefTimer scheduled'); 329 | 546 return; 330 | 547 } 331 | 548 332 | 549 var when = now + msecs; 333 | 550 334 | 551 debug('unrefList find where we can insert'); 335 | 552 336 | 553 var cur, them; 337 | 554 338 | 555 for (cur = unrefList._idlePrev; cur != unrefList; cur = cur._idlePrev) { 339 | 556 them = cur._idleStart + cur._idleTimeout; 340 | 557 341 | 558 if (when < them) { 342 | 559 debug('unrefList inserting into middle of list'); 343 | 560 344 | 561 L.append(cur, item); 345 | 562 346 | 563 if (unrefTimer.when > when) { 347 | 564 debug('unrefTimer is scheduled to fire too late, reschedule'); 348 | 565 unrefTimer.start(msecs, 0); 349 | 566 unrefTimer.when = when; 350 | 567 } 351 | 568 352 | 569 return; 353 | 570 } 354 | 571 } 355 | 572 356 | 573 debug('unrefList append to end'); 357 | 574 L.append(unrefList, item); 358 | 575 }; 359 | 360 | ``` 361 | L524-L534, 是有且只创建一个`unrefTimer`,来处理超时的内部使用定时器,处理完一个则顺序处理下一个。 362 | 363 | L553-L571, 当需要插入一个定时器时,则需要保证`unrefList`有序,需要遍历链表找到插入的位置,最差的情况下是 O(N)。 364 | 365 | 很显然,在HTTP中建立连接是最频繁的操作,那么向`unrefList`链表中添加节点也就非常频繁了,而且最开始设置的定时器其实最后真正会超时的非常少,因为中间涉及到io的正常操作时便会取消定时器。所以问题就变成最耗性能的操作非常频繁,而几乎不花时间的操作却很少被执行到。 366 | 367 | 针对这种情况,如何解决呢? 368 | 369 | 显然这里也遵从80/20原则。思路上我们应该使80%的情况变得更高效。 370 | 371 | #### 使用不排序的链表 372 | 主要思路就是将对unrefList链表的遍历操作,移到unrefTimeout定时器超时处理中。这样每次查找出已经超时的任务就需要花比较多的时间了O(n),但是插入操作却变得非常简单O(1),而插入节点正是最频繁的操作。 373 | ```js 374 | 572 exports._unrefActive = function(item) { 375 | 573 ....省略 376 | 574 var now = Timer.now(); 377 | 575 item._idleStart = now; 378 | 576 379 | 577 var when = now + msecs; 380 | 578 381 | 579 // If the actual timer is set to fire too late, or not set to fire at all, 382 | 580 // we need to make it fire earlier 383 | 581 if (unrefTimer.when === -1 || unrefTimer.when > when) { 384 | 582 unrefTimer.start(msecs, 0); 385 | 583 unrefTimer.when = when; 386 | 584 debug('unrefTimer scheduled'); 387 | 585 } 388 | 586 389 | 587 debug('unrefList append to end'); 390 | 588 L.append(unrefList, item); 391 | 589 }; 392 | ``` 393 | 可以看到 L588,之前遍历查找在新的实现中 [e5bb668](https://github.com/misterdjules/node/commit/e900f0cf79ce38712ee7f95f3cb0bee8fc56ba89),简单的变成抽象List的`append`操作。 394 | 395 | > https://github.com/joyent/node/issues/8160 396 | 397 | #### 使用二叉堆 398 | 399 | 二叉堆达到了插入和查找的平衡,和目前 libuv 的实现一致。 400 | 有兴趣的可以查看: 401 | * https://github.com/misterdjules/node/commits/fix-issue-8160-with-heap, 基于 v0.12. 402 | 403 | #### 社区改进实现 404 | 405 | * 有序链表的实现的版本只采用了一个`unrefTimer`来执行任务,在内存上是节省了,但却很难达到性能的平衡。 406 | * 二叉堆实现在正常的连接场景下却输于不排序链表。 407 | 408 | 社区通过演变,实现采用的是哈希+链表的结合,以空间换时间。其实是一种时间轮算法的演化。 409 | ```js 410 | ╔════ > Object Map 411 | ║ 412 | ╠══ 413 | ║ refedLists: { '40': { }, '320': { etc } } (keys of millisecond duration) 414 | ╚══ ┌─────────┘ 415 | │ 416 | ╔══ │ 417 | ║ TimersList { _idleNext: { }, _idlePrev: (self), _timer: (TimerWrap) } 418 | ║ ┌────────────────┘ 419 | ║ ╔══ │ ^ 420 | ║ ║ { _idleNext: { }, _idlePrev: { }, _onTimeout: (callback) } 421 | ║ ║ ┌───────────┘ 422 | ║ ║ │ ^ 423 | ║ ║ { _idleNext: { etc }, _idlePrev: { }, _onTimeout: (callback) } 424 | ╠══ ╠══ 425 | ║ ║ 426 | ║ ╚════ > Actual JavaScript timeouts 427 | ║ 428 | ╚════ > Linked List 429 | ``` 430 | 我们先看下数据结构的组织: 431 | 432 | * `refedLists`的键是超时时间,值是一个具有相同超时时间的链表。 433 | * `unrefedLists`也是同理。 434 | 435 | ```js 436 | 107 // Internal APIs that need timeouts should use `_unrefActive()` instead of 437 | 108 // `active()` so that they do not unnecessarily keep the process open. 438 | 109 exports._unrefActive = function(item) { 439 | 110 insert(item, true); 440 | 111 }; 441 | 114 // The underlying logic for scheduling or re-scheduling a timer. 442 | 115 // 443 | 116 // Appends a timer onto the end of an existing timers list, or creates a new 444 | 117 // TimerWrap backed list if one does not already exist for the specified timeout 445 | 118 // duration. 446 | 119 function insert(item, unrefed) { 447 | 120 const msecs = item._idleTimeout; 448 | 121 if (msecs < 0 || msecs === undefined) return; 449 | 122 450 | 123 item._idleStart = TimerWrap.now(); 451 | 124 452 | 125 const lists = unrefed === true ? unrefedLists : refedLists; 453 | 126 454 | 127 // Use an existing list if there is one, otherwise we need to make a new one. 455 | 128 var list = lists[msecs]; 456 | 129 if (!list) { 457 | 130 debug('no %d list was found in insert, creating a new one', msecs); 458 | 131 // Make a new linked list of timers, and create a TimerWrap to schedule 459 | 132 // processing for the list. 460 | 133 list = new TimersList(msecs, unrefed); 461 | 134 L.init(list); 462 | 135 list._timer._list = list; 463 | 136 464 | 137 if (unrefed === true) list._timer.unref(); 465 | 138 list._timer.start(msecs, 0); 466 | 139 467 | 140 lists[msecs] = list; 468 | 141 list._timer[kOnTimeout] = listOnTimeout; 469 | 142 } 470 | 143 471 | 144 L.append(list, item); 472 | 145 assert(!L.isEmpty(list)); // list is not empty 473 | 146 } 474 | ``` 475 | 476 | 我们比较下上述实现: 477 | * L128,根据键值(超时时间)拿到 list ,如有不为undefined,则简单的`append`到最后面就好了,复杂度O(1)。 478 | * L130-L141, 如果为 undefined, 则创建一个`TimersList`,包含一个C的定时器,来处理链表中的任务。 479 | * `listOnTimeout`也变得很简单,取出链表的任务,复杂度取决于链表的长度O(m), m < N。 480 | 481 | 482 | 模块使用一个链表来保存所有超时时间相同的对象,每个对象中都会存储开始时间_idleStart以及超时时间_idleTimeout。链表中第一个加入的对象一定会比后面加入的对象先超时,当第一个对象超时完成处理后,重新计算下一个对象是否已经到时或者还有多久到时,之前创建的Timer对象便会再次启动并设置新的超时时间,直到当链表上所有的对象都已经完成超时处理,此时便会关闭这个Timer对象。 483 | 484 | 通过这种巧妙的设计,使得一个Timer对象得到了最大的复用,从而极大的提升了timer模块的性能。 485 | 486 | ### Timer在node中的应用 487 | * 动态更新 HTTP Date字段的缓存 488 | 489 | ```js 490 | 31 var dateCache; 491 | 32 function utcDate() { 492 | 33 if (!dateCache) { 493 | 34 var d = new Date(); 494 | 35 dateCache = d.toUTCString(); 495 | 36 timers.enroll(utcDate, 1000 - d.getMilliseconds()); 496 | 37 timers._unrefActive(utcDate); 497 | 38 } 498 | 39 return dateCache; 499 | 40 } 500 | 41 utcDate._onTimeout = function() { 501 | 42 dateCache = undefined; 502 | 43 }; 503 | 504 | 228 // Date header 505 | 229 if (this.sendDate === true && state.sentDateHeader === false) { 506 | 230 state.messageHeader += 'Date: ' + utcDate() + CRLF; 507 | 231 } 508 | ``` 509 | L230,每次构造 Date字段值都会去获取系统时间,但精度要求不高,只需要秒级就够了,所以在1S 的连接请求可以复用 dateCache 的值,超时后重置为`undefined`. 510 | 511 | L34-L35,下次获取会重启生成。 512 | 513 | L36-L37,重新设置超时时间以便更新。 514 | 515 | * HTTP 连接超时控制 516 | 517 | ```js 518 | 303 if (self.timeout) 519 | 304 socket.setTimeout(self.timeout); 520 | 305 socket.on('timeout', function() { 521 | 306 var req = socket.parser && socket.parser.incoming; 522 | 307 var reqTimeout = req && !req.complete && req.emit('timeout', socket); 523 | 308 var res = socket._httpMessage; 524 | 309 var resTimeout = res && res.emit('timeout', socket); 525 | 310 var serverTimeout = self.emit('timeout', socket); 526 | 311 527 | 312 if (!reqTimeout && !resTimeout && !serverTimeout) 528 | 313 socket.destroy(); 529 | 314 }); 530 | ``` 531 | 默认的 timeout 为`this.timeout = 2 * 60 * 1000;`也就是120s。 L313,超时则销毁 socket。 532 | 533 | ### 小结 534 | Node.js 的 timer 模块闪烁着很多程序设计的精髓。 535 | * 数据结构抽象 536 | - linkedlist.js 抽象出链表的基础操作。 537 | * 以空间换时间 538 | - 相同超时时间的定时器分组,而不是使用一个`unrefTimer`,复杂度降到 O(1)。 539 | * 对象复用 540 | - 相同超时时间的定时器共享一个底层的 C的 timer。 541 | * 80/20法则 542 | - 优化主要路径的性能。 543 | 544 | 545 | ### 参考文档 546 | [1].https://github.com/nodejs/node/wiki/Optimizing-_unrefActive 547 | 548 | [2].http://alinode.aliyun.com/blog/9 549 | --------------------------------------------------------------------------------