• CombineStream 与 Multipart Form


    最近开始研究http 特别是multipart 表单,想弄明白他是怎么work 的。在nodejs 里,可以使用form-data 来组合一个multipart 表单,然后使用http.request 发送出去

    var formData = require('form-data');
    var http = require('http');
    var urlParse = require('url').parse;
    var fs = require('fs');
    
    f = new formData();
    f.append('image', fs.createReadStream('./test.jpg'));
    
    var options = urlParse(url);
    options.method = 'post';
    options.headers = f.getHeaders();
    
    var req = http.request(options);
    f.pipe(req);

    在服务端就可以接受到post 过去的表单内容(node 可以使用multiparty 中间件)

    form-data 之所以能pipe 到req,说明他至少是一个Readable stream, 查看代码后发现他继承于combine-stream。继续查看combine-stream 发现这是一个duplex stream,功能是接受多个readable stream 把他们串起来然后合并成一个readable stream。很有意思。

    了解了combine-stream 是干什么用的以后,再了解一下multipart 表单是怎么组成的 发现其于其它表单的区别在于

    1. header 里的Content-Type。
    2. body 里的内容由定义的boundary 分隔。

    最后发现,用combine-stream 来组装multipart 表单是一个很好的选择。因为stream 是资源的抽象,目的是为了在速度和系统资源占用之间做个取舍(想象一下如果没有stream,发送任何文件需要把整个文件放进内存;资源还没完全生产的情况,比如直播,没有流的抽象就没法实现)

    http 的req 就是一个writeable stream(res 也一样),所有请求内容都可以通过req.write(或者res.write) 发送过去。具体后面是怎么发送的,是用socket?然后再tcp?则不用关心,stream 这层抽象屏蔽了underlying system。我们只用关心从readable stream 读,往writeable stream 里面写就可以了。

    所以在发送multipart 表单的时候,我们只用:

    1. 往req 里写header,注意特殊的Content-Type。
    2. 往req 里pipe 文件流。
    3. 往req 里写boundray 分隔符。
    4. 重复2,3步。直到完成所有文件发送。
    5. 往req 里写尾信息。

    可以发现在2,3两步就涉及很多文件流,比如上传多张图片到服务器,那么我们就要往req 里pipe 第一个文件(然后加上boundray)等待其结束后再接着pipe 下一张图片的流。这样重复多次非常麻烦。不如把所有这些流串在一起,合并成一整个流,这样把流之间切换的重复逻辑包装起来,既不容易出错也更简洁易理解。


    CombineStream 要怎么实现呢。刚才了解到了,CombineStream 的逻辑不过是把一串readable stream 按顺序串起来,一个流结束了马上换另一个,直到所有添加的流都结束为止。那么首先CombineStream 必须是duplex 的。node 里的transform stream 就是最佳人选。

     1 var Transform = require('stream').Transform;
     2 var util = require('util');
     3 var assert = require('assert');
     4 var fs = require('fs');
     5 
     6 function CombineStream(options) {
     7     Transform.call(this, options);
     8 
     9     this._streams = [];
    10     this._currentStream = null;
    11 
    12     this._prepare = function(){
    13         var stream = this._currentStream = this._streams.shift();
    14         if (stream) {
    15             if (typeof stream === 'string') {
    16                 this.push(stream);
    17                 this._prepare();
    18                 return;
    19             }
    20             stream.pipe(this, {end: false});
    21             stream.on('end', function(){
    22                 this._prepare();
    23             }.bind(this));
    24             stream.on('error', function(err){
    25                 console.error(err);
    26             });
    27         } else {
    28             this.end();
    29         }
    30     };
    31 }
    32 util.inherits(CombineStream, Transform);
    33 
    34 CombineStream.prototype.append = function(stream) {
    35     this._streams.push(stream);
    36 }
    37 
    38 CombineStream.prototype._transform = function(chunk, encoding, callback) {
    39     callback(null ,chunk);
    40 }
    41 
    42 CombineStream.prototype.pipe = function(dest, options) {
    43     this._prepare();
    44     Transform.prototype.pipe.call(this, dest, options);
    45 }
    46 
    47 module.exports = CombineStream;

    这样就实现了一个简单的CombineStream。只加了很少代码:append 方法添加流,这里对string 做了适配;_prepare 这个私有方法用来实现流切换的逻辑。

    然后就可以试试用CombineStream来构造一个multipart表单然后发送。

    var cs = new CombineStream();
    cs.append('-----------------------------287032381131322
    Content-Disposition: form-data; name="image"; filename="test.jpg"
    Content-Type: image/jpg
    
    ');
    cs.append(gmStream);
    cs.append('
    -----------------------------287032381131322--');
    
    var options = urlParse(url);
    options.method = 'post';
    options.headers = {
        'keep-alive': 300, 
        'content-type':'multipart/form-data; boundary=---------------------------287032381131322',
        'Transfer-Encoding': 'chunked'
    };
    
    var req = http.request(options);
    cs.pipe(req);
    
    req.on('error', function(err){
        console.error(err);
    });
    req.on('response', function(res) {
        //deal res here.
    });

    这里我hardcode 了分隔信息和尾信息,中间有很多 可以看出没有系统的处理方法的话很容易出错。这样如果在server 端使用multiparty 中间件来解析表单的话可以得到正确的文件上传内容。

    { image: 
       [ { fieldName: 'image',
           originalFilename: 'test.jpg',
           path: '/var/folders/02/pwvm1df51nsfvg373jf8ksg00000gn/T/hwM6h-88fqiemXyna1Hd09eK.jpg',
           headers: [Object],
           size: 10988 } ] }

    Conclusion:

    可见form-data 模块基本就是这么工作的。

  • 相关阅读:
    Jmeter_远程启动 I
    jmeter(九)逻辑控制器
    Mysql innodb 间隙锁 (转)
    MySQL- InnoDB锁机制
    Innodb间隙锁,细节讲解(转)
    性能测试:压测中TPS上不去的几种原因分析(就是思路要说清楚)
    Redis性能调优
    Redis基础
    VMThread占CPU高基本上是JVM在频繁GC导致,原因基本上是冰法下短时间内创建了大量对象堆积造成频繁GC。
    关于Hibernate二级缓存第三方插件EHCache缓存
  • 原文地址:https://www.cnblogs.com/agentgamer/p/5405409.html
Copyright © 2020-2023  润新知