Skip to content

Commit

Permalink
流教程改写
Browse files Browse the repository at this point in the history
  • Loading branch information
yunnysunny committed Oct 30, 2024
1 parent b90660c commit b82dfa1
Show file tree
Hide file tree
Showing 2 changed files with 38 additions and 31 deletions.
65 changes: 35 additions & 30 deletions text/03_node_basic.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@ node 的 stream API 是 node 的核心,HTTP 和 TCP 的各种 API ,都是基

首先 stream 的设计初衷是为了“节流”,说的直白些就是内存中待处理的数据量过大,如果处理的速度过慢,就是导致内存中挤压的数据越来越多,最终导致进程不稳定或者内存溢出进而崩溃,而 stream 的存在,就是构建一个缓冲地带。stream 的类(从功能上分为两种 [`Writable`](https://nodejs.org/dist/latest-v12.x/docs/api/stream.html#stream_class_stream_writable)[`Readable`](https://nodejs.org/dist/latest-v12.x/docs/api/stream.html#stream_class_stream_readable) )在初始化的时候会指定一个 `highWaterMark` 参数,借助此来约定内部使用缓冲区的长度,超过这个参数,就不应该往缓冲区添加数据了。下面对于 `Readable``Writable` 中的 `highWaterMark` 的使用流程分别进行说明。

`Readable` 通过 [push](https://nodejs.org/dist/latest-v12.x/docs/api/stream.html#stream_readable_push_chunk_encoding) 函数添加数据,数据在其内部存储为一个双向链接的数据结构(具体参见 [BufferList](https://github.com/nodejs/node/blob/master/lib/internal/streams/buffer_list.js) 源码,令人遗憾的是这么方便的数据结构在 Node API 中却没有暴露),如果当前链表中的数据长度达到 `highWaterMark`,`push` 函数就会返回 `false`,不过你依然可以调用 `push` 写入数据。有就是说内部链表的数据长度会大于 `highWaterMark`,Node 内部对于可读流的内存控制完全交给了调用者本身,这个 `highWaterMark` 就是一个警示作用,告诉你现在缓冲区已经满了,你自己看着办吧,如果你不理会,继续往里面写,撑爆了内存是你自己的责任,于我无关。`Readable` 通过 [read](https://nodejs.org/dist/latest-v12.x/docs/api/stream.html#stream_readable_read_size) 函数读取数据,读取的时候可以指定长度,如果指定了长度就从内部链表的队尾移出指定长度的元素交给调用者;如果没有指定长度,就会把所有元素移出交给用户。
`Readable` 通过 [push](https://nodejs.org/dist/latest-v12.x/docs/api/stream.html#stream_readable_push_chunk_encoding) 函数添加数据,数据在其内部存储为一个双向链接的数据结构(具体参见 [BufferList](https://github.com/nodejs/node/blob/master/lib/internal/streams/buffer_list.js) 源码,令人遗憾的是这么方便的数据结构在 Node API 中却没有暴露),如果当前链表中的数据长度达到 `highWaterMark`,`push` 函数就会返回 `false`,不过你依然可以调用 `push` 写入数据。也就是说内部链表的数据长度会大于 `highWaterMark`,Node 内部对于可读流的内存控制完全交给了调用者本身,这个 `highWaterMark` 就是一个警示作用,告诉你现在缓冲区已经满了,你自己看着办吧,如果你不理会,继续往里面写,撑爆了内存是你自己的责任,与我无关。`Readable` 通过 [read](https://nodejs.org/dist/latest-v12.x/docs/api/stream.html#stream_readable_read_size) 函数读取数据,读取的时候可以指定长度,如果指定了长度就从内部链表的队尾移出指定长度的元素交给调用者;如果没有指定长度,就会把所有元素移出交给用户。

`Writable` 内部维持一个计数器,代表有多少条数据还未写入完成,通过 [write](https://nodejs.org/dist/latest-v12.x/docs/api/stream.html#stream_writable_write_chunk_encoding_callback) 函数添加数据,此时计数器加一(假设我们此时只写一条数据),其内部调用 [_write](https://nodejs.org/dist/latest-v12.x/docs/api/stream.html#stream_writable_write_chunk_encoding_callback_1) 来实现实际的写操作,在 `_write` 实际写完之后在其回调函数中计数器减一。每次调用 `write` 时,如果计数器的值小于 `highWaterMark`,就返回 `true`,这样你可以安心的写;如果为 `false` 就代表当前待写入的数据超标了,如果再写入就有可能会导致内存中的数据越积越多,最终雪崩。这种将主动权放给调用者的行为是和 `Readable` 是如出一辙的。
`Writable` 内部维持一个计数器,代表有多少条数据还未写入完成,通过 [write](https://nodejs.org/dist/latest-v12.x/docs/api/stream.html#stream_writable_write_chunk_encoding_callback) 函数添加数据,此时计数器加一(假设我们此时只写一条数据),其内部调用 [`_write`](https://nodejs.org/dist/latest-v12.x/docs/api/stream.html#stream_writable_write_chunk_encoding_callback_1) 来实现实际的写操作,在 `_write` 实际写完之后在其回调函数中计数器减一。每次调用 `write` 时,如果计数器的值小于 `highWaterMark`,就返回 `true`,这样你可以安心的写;如果为 `false` 就代表当前待写入的数据超标了,如果再写入就有可能会导致内存中的数据越积越多,最终雪崩。这种将主动权放给调用者的行为是和 `Readable` 是如出一辙的。

### 3.4.2 创建自定义读写流

Expand All @@ -145,38 +145,51 @@ node 的 stream API 是 node 的核心,HTTP 和 TCP 的各种 API ,都是基
const { Readable } = require('stream');

class MyReadable extends Readable {
constructor(options) {
super(options);

}
_read() {
console.log('_read has been called');
const index = Math.random() * 0xff;
this.push(Buffer.from([index & 0xff]));
}
_read () {

}
}

const reader = new MyReadable({
highWaterMark:4,
highWaterMark: 4,
});
const initSize =6;
for (let i=0;i<initSize;i++) {
const pushResult = reader.push(Buffer.from([i & 0xff]));
console.log('现有流模式', reader.readableFlowing);
console.log('在流有数据前读取', reader.read());
const data = ['a', 'b', 'c', 'd', 'e', 'f'];
const dataLen = data.length;
for (let i = 0; i < dataLen; i++) {
const pushResult = reader.push(Buffer.from(data[i]));
if (!pushResult) {
console.warn('reach highwatermark, you have better not to push',i);
console.warn('达到highWater值了,最好不要再 push', i);
}
}
console.log(reader.read());

for (let i = 0; i < 3; i++) {
console.log('read after push', i, reader.read());
}
// 给可读流推送 null,表示数据已经读取完毕
reader.push(null);
console.log('可读取结束后读取', reader.read());
// 因为在流结束后,调用 push 函数,下面这句会触发可读流的 error 事件
reader.push('a');
reader.on('error', (err) => {
console.error('可读流出错', err);
});
// nodejs 中如果对于对象的 error 事件没有监听器,会导致进程触发 uncaughtException 事件
process.on('uncaughtException', (err) => {
console.error('uncaughtException', err);
});
```

**代码 3.4.2.1.1**

可读流依靠 `push` 函数来将数据添加到内部缓冲区,同时在当前事件轮询 “阶段” 的末尾判断缓冲区长度是否低于 `highWaterMark` ,如果低于这个值,就会强制触发调用 `read(0)`,这个调用只会填充满当前的缓冲区,尝试让其的长度达到 `highWaterMark`
上述代码中创建了一个简单的可读流。可读流提供了两种读取模式,flow 模式和 no-flow 模式,可读流有一个 `readableFlowing` 属性,默认为 `null`。从上述代码的输出中也可以发现,在没有做任何函数调用的情况下,可读流的 `readableFlowing``null`

> Node 中使用 process.nextTick 函数来将代码置于当前事件轮询 “阶段” 的末尾执行。事件轮询处在 1.2 节中有介绍,常见的阶段有 定时器回调阶段、pending 回调阶段、IO 事件轮询回调阶段、check 回调阶段,在任意以上阶段的回调中使用 nextTick 函数的话,则 nextTick 函数回调中将此阶段回调队列执行完成后,跟随执行。不过需要注意,如果 nextTick 执行次数过多,将会延长当前阶段的执行时间,导致其他阶段 “饥饿”。
如果给可读流对象增加 `data` 事件监听、调用函数 `resume` / `pipe` ,将会使用可读流进入 flow 模式,此时 `readableFlowing` 会被置为 true。调用 `pause` / `unpipe` 函数会将可读流切换到 no-flow 模式,并且将 `readableFlowing` 置为 false,这个时候必须手动调用函数 `resume` / `pipe` 才能将其切换回 flow 模式,如果在这种情况下添加 `data` 事件是无法切换为 flow 模式的。

将流置为 no-flow 还有一种方式就是添加 `readable` 事件监听。注意,如果你同时给可读流添加了 `readable``data` 的事件,则 `readable` 的优先级高于 `data`,流将回进入 no-flow 模式。当你将 `readable` 事件移出,只保留 `data` 事件时,则回到 flow 模式。同时需要注意到,添加了 `readable` 事件后,调用 `pause` `resume` 这两个函数是没有意义的。

在可读流的使用过程中,你应该尽量选择一种读取模式,以此降低自己代码的复杂度。Node 中通过调用可读流不同函数来隐式的修改其工作模式的方式,确实是一种比较让人艰涩难懂的设计。

read 函数内部会级联调用 _read ,我们一般会将数据的 push 操作放到 _read 中,虽然你可以手动调用 push 来写入内部缓冲区,但是将数据写入放到 _read 中可以尽量让流本身在读写之间达到平衡。

#### 3.4.2.2 自定义可写流

Expand Down Expand Up @@ -213,14 +226,6 @@ writer.on('error',function(err) {

`_write` 函数是供给内部调用使用的,在自己来实现可写流的子类时,这个函数是必须要实现的。`_write` 内部通过 `callback` 函数来标记写入完成。这个回调函数调用之前,认为数据是没有写入成功的。

### 3.4.3 可读流的两种读取模式

可读流提供了两种读取模式,flow 模式和 no-flow 模式,可读流有一个 `readableFlowing` 属性,默认为 `null`。如果给可读流对象增加 `data` 事件监听、调用函数 `resume` / `pipe` ,将会使用可读流进入 flow 模式,此时 `readableFlowing` 会被置为 true。调用 `pause` / `unpipe` 函数会将可读流切换到 no-flow 模式,并且将 `readableFlowing` 置为 false,这个时候必须手动调用函数 `resume` / `pipe` 才能将其切换回 flow 模式,如果在这种情况下添加 `data` 事件是无法切换为 flow 模式的。

将流置为 no-flow 还有一种方式就是添加 `readable` 事件监听。注意,如果你同时给可读流添加了 `readable``data` 的事件,则 `readable` 的优先级高于 `data`,流将回进入 no-flow 模式。当你将 `readable` 事件移出,只保留 `data` 事件时,则回到 flow 模式。同时需要注意到,添加了 `readable` 事件后,调用 `pause` `resume` 这两个函数是没有意义的。

在可读流的使用过程中,你应该尽量选择一种读取模式,以此降低自己代码的复杂度。Node 中通过调用可读流不同函数来隐式的修改其工作模式的方式,确实是一种比较让人艰涩难懂的设计。

### 3.4.4 可写流的缓冲区

为了防止可写写流写入的速度过快,可写流提供了两个函数 `cork``uncork`,调用 `cork` 后会把要写入的数据缓存起来,直到调用函数 `uncork` 后才会一股脑将缓存的数据做真正的写入。
Expand Down Expand Up @@ -286,7 +291,7 @@ socket.on('readable', () => {

**代码 3.5.1**

可以看的出来上述代码最核心的一句应该是 `remaining = this._readPacket()` ,这个 _readPacket 函数是做 socket 数据读取的关键函数:
可以看的出来上述代码最核心的一句应该是 `remaining = this._readPacket()` ,这个 `_readPacket` 函数是做 socket 数据读取的关键函数:

```javascript
/**
Expand Down
4 changes: 3 additions & 1 deletion text/14_node_log_and_monitor.md
Original file line number Diff line number Diff line change
Expand Up @@ -932,7 +932,9 @@ req_duration{serverName="chapter14",namespace="default",path="/b"} 126
grafana 使用 promql 来绘制图表,我们在 **图 14.3.1.7** 中看到的各个图标,都是基于 promql 语句查询绘制的界面。最简单的 promql 就是直接写指标名字(例如 `req_count`) ,它对应的数据是这个指标最新的瞬时向量的内容。
需要注意,我们 grafana 只支持瞬时向量和标量,不支持范围向量,但是这并不代表范围向量是没有用的。
需要注意,我们 grafana 只支持瞬时向量和标量,不支持范围向量,但是这并不代表范围向量是没有用的。对于计数器类型的指标来说,grafana 也不支持,这时候你可以使用 Prometheus 内置函数将计数器先转化成范围向量,然后再对范围向量内的数值做运算转成一个瞬时向量,比如说 rate 函数就是将范围向量转成瞬时向量的常用操作。
### 示例代码
本章节示例代码可以从这里找到 https://github.com/yunnysunny/nodebook-sample/tree/master/chapter14

0 comments on commit b82dfa1

Please sign in to comment.