起初的源码:
router.post("/getfile", (req, res) => {
let c = new Client();
let params = req.query;
let filePath = params.filepath;
c.on('ready', function () {
c.get(filePath, function (err, stream) {
if (err) {
res.end(JSON.stringify({
code: 1,
message: err,
data: ''
}));
return '';
}
stream.once('close', function () {
c.end();
});
stream.pipe(res);
});
});
c.connect(connectKey)
});
问题说明:
打log后发现进入路由的时间很快,并且执行速度也很快,毫秒级别的运行时间,但是接口的返回显得很慢,所以是stream.pipe(res);
的相关问题。
问题分析
我打算分析pipe相关的源码,但是网上没找到资源,只找到nodejs的api中有对pipe和流的其他用法,所以最后的解决方法用的是流的读取。我把它放在这。
ftp在ready后获取err和stream两个参数,其中stream是流的含义,所以在官方文档中找到流的文件读写方法,并通过eventEmitter事件触发各类操作,列出如下:
- close —— writeable
流及其任何底层资源(例如文件描述符)已关闭时,则会触发 ‘close’ 事件 - error —— writeable
如果在写入或管道数据时发生错误,则会触发 ‘error’ 事件 - finish —— writeable
在调用stream.end()
方法之后,并且所有数据都已刷新到底层系统,则触发 ‘finish’ 事件 - pipe —— writeable
当在可读流上调用 stream.pipe() 方法将此可写流添加到其目标集时,则触发 ‘pipe’ 事件
···可写流就先写这么多,暂时用不到
既然是管道的方法,我认为是服务器和本机没有缓存的原因,导致ftp的传输是字节流,所以响应速度很慢,但是在多次实践中发现读取时间一直是稳定在10s左右,所以排除字节流的读取方法。
另一种猜想也和合理的是管道运输需要一个目的缓存,但是直接使用pipe方法的res没有文件流的接收方,数据就会堵在发送方,直至管道结束。
解决方法(另一种读取方案)
我当时的解决方法是使用流的读取事件获取chunk,最后返回给res。源码如下
router.post("/getfile", (req, res) => {
let c = new Client();
let params = req.query;
let filePath = params.filepath;
c.on('ready', function () {
c.get(filePath, function (err, stream) {
if (err) {
res.end(JSON.stringify({
code: 1,
message: err,
data: ''
}));
return '';
}
let data = '',
chunk = '';
stream.on('close', function () {
c.end();
});
stream.on('readable', function () {
while(null != (chunk = stream.read())) {
data += chunk;
}
setTimeout(() => {
res.end(JSON.stringify({
code: 0,
message: '',
data: data
}));
});
});
});
});
c.connect(connectKey)
});
根据官方文档描述:
每次调用 readable.read() 都会返回一个数据块或 null。 块不是串联的。 需要 while 循环来消费当前缓冲区中的所有数据。 当读取大文件时,.read() 可能会返回 null,到目前为止已经消费了所有缓冲的内容,但是还有更多的数据尚未缓冲。 在这种情况下,当缓冲区中有更多数据时,将触发新的 ‘readable’ 事件。 最后,当没有更多数据时,则将触发 ‘end’ 事件。
所以在没有可传输文件时,流的readable会触发一个end信号;但在实际应用中,end并没有触发,目前也不清楚原理,而且close不是立即触发的,也是10s后才触发。不过这种写法可以很快返回res。