Skip to content

Commit

Permalink
完成流教程
Browse files Browse the repository at this point in the history
  • Loading branch information
yunnysunny committed Nov 10, 2024
1 parent 348efdf commit 21caaa8
Showing 1 changed file with 29 additions and 96 deletions.
125 changes: 29 additions & 96 deletions text/03_node_basic.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,115 +330,48 @@ outputStream.on('finish', function () {
**代码 3.4.2.2.2 chapter3/stream/transform_simple.js**

上述代码中我们通过可读流 inputStream 来采集数据的数字, myTransfrom 转化的函数中将采集到的数字筛选出偶数来,最终可写流 outputStream 拿到最终转化的数字。
## 3.5 TCP
#### 3.4.2.3 身兼两职的流

Transform 流其实内部继承自 [`Duplex`](https://nodejs.org/api/stream.html#class-streamduplex) ,Duplex 内部同时拥有可读流和可写流,但是跟 Transform 不同的是,Duplex 其内部读写的数据是分别存储在两个缓冲区中,两者没有关联关系,相互独立。来自TCP 中的 [net.Socket](https://nodejs.org/dist/latest-v12.x/docs/api/net.html#net_class_net_socket) 类就是继承自 [stream.Duplex](https://nodejs.org/dist/latest-v12.x/docs/api/stream.html#stream_class_stream_duplex) 类。

TCP 属于传输层协议,大家都对 HTTP 的服务编写比较熟悉,我们可以通过 API 方便的发送请求、接收响应数据。但是你发送的 HTTP 请求时,在 API 底层要封装成符合 HTTP 协议的请求数据数据包,对端在接收响应后,也是 API 底层帮我们把 HTTP 数据包解析出来,抛到应用层。HTTP 1.x 时代,只能通过客户端发送请求来触发通信流动,服务器端不能主动给客户端发送数据,如果想实现全双工的通信,就直接的就是借助 TCP 层面的协议。

> HTTP 2.x 开始,服务器端和客户端可以互相发送数据,与此类似的功能,还包括 html5 标准中的 WebSocket 协议。
两端程序在使用 TCP 协议进行通信时,一个首要要解决的问题时,如何知道对方发送过来的是一个完整的数据包。这种要求初学者听上去可能有些过分,构建一个 http 服务的时候,也没有看到还得判断对方的包什么时候结束,这是由于语言底层代码已经帮你处理这些问题,不需要手动实现了。

> 对于 HTTP 1.1 来说,服务器端需要先读取 HTTP 的头信息部分,读取到 `\r\n` 代表头信息部分结束。如果请求数据包中有正文内容的话,需要在头信息中指定 [Content-Length](https://httpwg.org/specs/rfc7230.html#header.content-length) 或者 [Transfer-Encoding](https://httpwg.org/specs/rfc7230.html#header.transfer-encoding)。服务器端根据这两个字段来决定请求正文部分如何解析,需要读取多少字节算是正文结尾。
>
> HTTP 1.1 协议,支持客户端在一条连接上发送多个请求,但是多个请求直接的数据包内容“不混杂”,也就是说如果发送一次请求,必须把当前请求包的数据一次性全发完,才能发送第二个请求包(但是发完第一个请求包之后,不用等待相应包返回,就可以直接发第二个请求包)。所以服务器端也需要根据上述规则来区分不同请求数据包,否则会发生数据紊乱。
如果自己实现一个 TCP 协议的话,数据包之间的“分割”要自己实现。常见的设计思路是设定一个固定长度的头部信息,在其内部放置正文长度,服务器端读取完头部之后,就能拿到正文长度,(正常情况下)再读取一次后,就可以得到一个完整的数据包。

这种情况下,使用 no-flow 模式显得更适合,这样就可以精确控制每次读取一个完整包,使用 flow 模式就显得比较麻烦。为了演示 TCP 的使用,会给出一个小 demo,下面的代码是从 [pandora](https://github.com/midwayjs/pandora) 项目中的一个模块演变而来。

为了方便阅读代码,首先我们需要定义通信协议。

![](images/tcp_package.png)

**图 3.5.1**

数据包总体上分为头部和正文两部分,头部一共五个字节,第一个字节用来存储一些元数据,剩下的四个字节用来存储正文数据部分的长度。

> 正文长度如果用两个字节的话,就只能携带 64KB 的数据,如果使用三个字节的话可以携带 16MB 的数据,四个字节可以携带 4GB 的数据。很多 TCP 协议会使用四个字节来承载正文长度,其实真正在使用的过程中,很多应用采用 TCP 协议来做指令控制,其实正文数据不需要这么大,这里之所以采用四字节,其实是惯例使然。
头信息中的首字节,只采用了最高位的 3 bit,其他 5 bit 留作以后控制用,采用的 3 bit 用来标识正文数据的数据类型,这里仅仅预设了两种类型: `000` 表示正文是 JSON 数据, `001` 标识正文是二进制数据。

正文长度的四字节采用了大端(大小端的知识点,具体可以参见 [维基百科地址](https://zh.wikipedia.org/wiki/%E5%AD%97%E8%8A%82%E5%BA%8F))的模式。

这里只贴出 socket 数据读取部分的代码,因为这部分代码是讲解流 API 的关键代码。上面提到我们用了 noflow 模式,所以这里使用 readable 的监听事件:
Transform 流其实内部继承自 [`Duplex`](https://nodejs.org/api/stream.html#class-streamduplex) ,Duplex 内部同时拥有可读流和可写流,但是跟 Transform 不同的是,Duplex 其内部读写的数据是分别存储在两个缓冲区中,两者没有关联关系,相互独立。Node API 中的 [net.Socket](https://nodejs.org/dist/latest-v12.x/docs/api/net.html#net_class_net_socket) 类就是继承自 [stream.Duplex](https://nodejs.org/dist/latest-v12.x/docs/api/stream.html#stream_class_stream_duplex) 类,由于 TCP 双向通信的特性,既能收也能发,且在操作系统层面收发使用的是不同的缓冲区,所以使用 Duplex 类是特别贴合的。为了演示 Duplex 类的使用,我们还是举一个菜鸟驿站的例子,驿站既可以收快递,也可以往外寄快递,和 TCP 的例子类似,他们收上来的快递和要寄出的快递肯定不是同一个(这里不讨论拒收等特殊情况)。那我们可以使用下面的代码来演示:

```javascript
socket.on('readable', () => {
try {
// 在这里循环读,避免在 _readPacket 里嵌套调用,导致调用栈过长
let remaining = false;
do {
console.time('readPacket');
remaining = this._readPacket();
console.timeEnd('readPacket');
}
while (remaining);
} catch (err) {
slogger.error('', err);
err.name = 'PacketParsedError';
this._close(err);
const { Duplex } = require('stream');
class PostHouse extends Duplex {
constructor (options) {
super(options);
this.postingLetters = [];
}
});
```

**代码 3.5.1**

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

```javascript
/**
* 读取服务器端数据,反序列化成对象
*
* @fires Client~EVENT_NEW_MESSAGE
*/
_readPacket() {
if (!(this._bodyLength)) {
this._header = this.read(HEAD_LEN);
if (!this._header) {
console.log('头部数据尚不完整')
return false;
}
// 通过头部信息获得body的长度
this._bodyLength = this.getBodyLength(this._header);
_read () {
//
}
console.log('正文长度' + this._bodyLength);
let body;
if (this._bodyLength > 0) {
body = this.read(this._bodyLength);
if (!body) {
slogger.info('正文数据尚不完整');
return false;
}

receiveLetter (letter) {
this.push(letter);
}
this._bodyLength = null;
let entity = this.decode(body, this._header);
// console.log(entity)
setImmediate(() => {
this.emit(EVENT_NEW_MESSAGE, entity.data, (res) => {
this.send(res);
}, this);
});
return true;
}
```

**代码 3.5.2**

stream 触发 readable 事件的时候,代表有数据可读了,但是我们应用层想要的数据未必完整,比如说 **图3.5.1** 中,我们要求一次性读取 5 个字节长度的头部数据来,好确认下面正文数据长度。stream 可能在收到一个字节的时候,就出发 readable 时间,这时候我们使用 read 函数的时候,无法读取出来 5 个字节,这时候 read 函数返回 null,_readPacket 函数返回 false,代码 3.5.1 中的 while 循环就会退出,这样我们就需要等待下一次 readable 事件触发的时候,再尝试读取 5 个字节看看能否读取出来。

> 官方文档中,对于 net 包中 socket 对象的 read 函数是没有直接给出的,大家可以直接看 stream 包中 readable 的 [read](https://nodejs.org/api/stream.html#stream_readable_read_size) 函数文档。
const postHouse = new PostHouse({
objectMode: true, // 以对象模式工作, 方便传递字符串
});
postHouse.on('data', function (data) {
console.log('收到信', data);
});
postHouse.receiveLetter('信件1');// 使用字符串格式插入可读流数据
postHouse.write('要寄出的信件x');
console.log(postHouse.postingLetters);
```

代码这样设计,还有一个好处,就是 TCP 协议天生会出现极小概率的 “断包” 问题,即发送端传输过程中,部分数据丢失,只能被迫重传,这样接收端一次读取的数据并不完整,需要再次尝试读取,而我们上述的代码天生具有这个特性。
**代码 3.4.2.2.2 chapter3/stream/duplex_post_house.js**

## 3.6 总结
通过上述代码可以看到我们构建出来的驿站类 PostHouse,既可以接收快递,也可以寄出快递,但是两者的数据是不共享的,没有相互干扰。
## 3.5 总结

我们用两个小节讲述了 Node 中如何处理静态资源和动态请求,看完这些之后,如果你是一个初学者,可能会因此打退堂鼓,这也太麻烦了,如果通过这种方式来处理数据,跟 php java 之类的比起来毫无优势可言嘛。大家不要着急,Node 社区已经给大家准备了各种优秀的 Web 开发框架,比如说 [Express](https://expressjs.com/)[Koa](https://koajs.com/),绝对让你爱不释手。你可以从本书的第 6 章中学习到 express 基本知识。

本章示例代码可以从这里找到:https://github.com/yunnysunny/nodebook-sample/tree/master/chapter3

## 3.7 参考文档

1. Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing https://httpwg.org/specs/rfc7230.html

0 comments on commit 21caaa8

Please sign in to comment.