Node程序debug小记 (2)

当我们达成这个共识以后,就要开始进行问题的分析了。
首先你要了解你所使用的这几个包的作用是什么,如果能知道他们是怎么实现的那就更好了。

对于co,就是一个利用yield语法特性将Promise转换为更直观的写法罢了,没有什么额外的逻辑。
而urllib也会在每次调用request时创建一个新的client(刚开始有想过会不会是因为多次调用urllib导致的,不过用简单的Promise.resolve代替之后,这个念头也打消了)

那么矛头就指向了formstream,现在要进一步的了解它,不过通过官方文档进行查阅,并不能得到太多的有效信息。

源码阅读

源码地址

所以为了解决问题,我们需要去阅读它的源码,从你在代码中调用的那些 API 入手:

构造函数营养并不多,就是一些简单的属性定义,并且看到了它继承自Stream,这也是为什么能够在urllib的options中直接填写它的原因,因为是一个Stream的子类。

util.inherits(FormStream, Stream);

然后就要看field函数的实现了。

FormStream.prototype.field = function (name, value) { if (!Buffer.isBuffer(value)) { // field(String, Number) // https://github.com/qiniu/nodejs-sdk/issues/123 if (typeof value === 'number') { value = String(value); } value = new Buffer(value); } return this.buffer(name, value); };

从代码的实现看,field也只是一个Buffer的封装处理,最终还是调用了.buffer函数。
那么我们就顺藤摸瓜,继续查看函数的实现。

FormStream.prototype.buffer = function (name, buffer, filename, mimeType) { if (filename && !mimeType) { mimeType = mime.lookup(filename); } var disposition = { name: name }; if (filename) { disposition.filename = filename; } var leading = this._leading(disposition, mimeType); this._buffers.push([leading, buffer]); // plus buffer length to total content-length this._contentLength += leading.length; this._contentLength += buffer.length; this._contentLength += NEW_LINE_BUFFER.length; process.nextTick(this.resume.bind(this)); return this; };

代码不算少,不过大多都不是这次需要关心的,大致的逻辑就是将Buffer拼接到数组中去暂存,在最后结尾的地方,发现了这样的一句代码:process.nextTick(this.resume.bind(this))。
顿时眼前一亮,重点的是那个process.nextTick,大家应该都知道,这个是在Node中实现微任务的其中一个方式,而另一种实现微任务的方式,就是用Promise。

修改代码验证猜想

拿到这样的结果以后,我觉得仿佛找到了突破口,于是尝试性的将前边的代码改为这样:

const form = new Formstream() form.field('timestamp', moment().unix()) yield Promise.resolve(1) const options = { method: 'POST', headers: form.headers(), stream: form } process.nextTick(() => { urllib.request(url, options) })

发现,果然超时了。

从这里就能大致推断出问题的原因了。
因为看代码可以很清晰的看出,field函数在调用后,会注册一个微任务,而我们使用的yield或者process.nextTick也会注册一个微任务,但是field的先注册,所以它的一定会先执行。
那么很显而易见,问题就出现在这个resume函数中,因为resume的执行早于urllib.request,所以导致其超时。
这时候也可以同步的想一下造成request超时的情况会是什么。
只有一种可能性是比较高的,因为我们使用的是stream,而这个流的读取是需要事件来触发的,stream.on('data')、stream.on('end'),那么超时很有可能是因为程序没有正确接收到stream的事件导致的。

当然了,「程序员修炼之道」还讲过:

Don't Assume it - Prove It
不要假定,要证明

所以为了证实猜测,需要继续阅读formstream的源码,查看resume函数究竟做了什么。
resume函数是一个很简单的一次性函数,在第一次被触发时调用drain函数。

FormStream.prototype.resume = function () { this.paused = false; if (!this._draining) { this._draining = true; this.drain(); } return this; };

那么继续查看drain函数做的是什么事情。
因为上述使用的是field,而非stream,所以在获取item的时候,肯定为空,那么这就意味着会继续调用_emitEnd函数。
而_emitEnd函数只有简单的两行代码emit('data')和emit('end')。

FormStream.prototype.drain = function () { console.log('start drain') this._emitBuffers(); var item = this._streams.shift(); if (item) { this._emitStream(item); } else { this._emitEnd(); } return this; }; FormStream.prototype._emitEnd = function () { this.emit('data', this._endData); this.emit('end'); };

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zgwxsz.html