使用 Grinder 模拟 multipart/form-data 方式上传文件

猪小花1号2018-09-17 11:50

作者:牛小宝


最近需要用 Grinder 模拟 multipart/form-data 方式上传文件数据,所以对相关的内容做了针对性的了解,这里做下总结。
先来说说multipart/form-data,HTTP/1.1 协议中规定的HTTP的请求方法有OPTIONS、GET、HEAD、POST、PUT、DELETE、TRACE、CONNECT这几种。POST请求方法一般用来向服务端进行数据提交,multipart/form-data 就是POST提交数据的其中一种方式。
HTTP 协议是以 ASCII 码传输,建立在 TCP/IP 协议之上的应用层规范。规范把 HTTP 请求分为三个部分:状态行、请求头、消息主体,下面我们先来看看 multipart/form-data 的请求头:

POST http://pms.kaola.com/ajax/activity/moduleGoods/import/goods?moduleId=307191 HTTP/1.1
Host: pms.kaola.com
Proxy-Connection: keep-alive
Content-Length: 57037
Origin: http://pms.kaola.com
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryzEkKyQ8MgrNaiHQt
Accept: */*
Referer: http://pms.kaola.com/activity/conference/module/apply?moduleId=307191
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8

这里我们要关注的是 Content-Type 属性,Content-Type用于指定网页内容类型,默认是text/html,这里是将类型指定为 multipart/form-data ,后面有一个 boundary 字段属性,称之为“分割边界”,用于分割不同的字段,因此这个字段内容不能在其他地方出现,所以一般都是使用一段几乎不可能出现的数据。boundary 会作为分割边界放在请求体内,下面我们来看body的内容:

------WebKitFormBoundaryzEkKyQ8MgrNaiHQt
Content-Disposition: form-data; name="file"; filename="goods_import1.xls"
Content-Type: application/vnd.ms-excel


------WebKitFormBoundaryzEkKyQ8MgrNaiHQt--

body中的 "------WebKitFormBoundaryx0nWcWXAkX2l2qHm" 既是消息头中的boundary,如果上传有多个文件,都会以boundary分割开。Content-Disposition中包含name和filename属性,name是form表单提交内容里的name属性,通过查看HTML源码可以看到,如下:

<input type="file" accept="application/vnd.ms-excel" contenteditable="false" id="1474343655456-0" name="file">

filename是所上传文件的文件名,Content-Type是指上传的文件类型,"vnd.ms-excel"代表本次上传的是excel表格,最后还是以boundary结尾,末尾出多处"--"代表body的结束。

对multipart/form-data有一定了解之后我们开始写grinder脚本,脚本主要部分的代码如下:

... ...
from HTTPClient import Codecs, NVPair
from jarray import zeros
... ...
headers_post= \
    [ NVPair('User-Agent','Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36'),
      NVPair('Accept', 'application/json'),
      NVPair('Connection', 'keep-alive')]

url1 = 'http://pms.kaola.com/ajax/activity/conference/module/brand/import'

request100 = HTTPRequest(url = url1, headers = headers_post)

class TestRunner:

    def upload(self, fileName, moduleId):
        #文件参数数组,要上传多个文件,就添加多个NVpair
        #主要模拟下面的内容
        #name="File"; filename="username.csv"
        files = [NVPair("file", str(fileName))]
        #返回一个array(HTTPClient.NVPair, [None]),用于存放header里的Content-Type
        headers = zeros(1, NVPair)
        #请求参数
        parameter = [NVPair("moduleId", str(moduleId))]
        data = Codecs.mpFormDataEncode(parameter, files, headers)    
        result = request100.POST(url1, data, headers)
... ...

对于headers_post和request的定义与普通写脚本一样,我们主要看upload方法中的实现,方法体中的name,files,parameter都在代码中注释出了他们的作用,这些变量作为参数传递给了方法 Codecs.mpFormDataEncode(parameter, files, headers) ,这个方法的作用就是将我们传进去的键值对和文件编码为 multipart/form-data 方式传递的所用字节数组,方法原型如下:

public static final byte[] mpFormDataEncode(NVPair[] opts, NVPair[] files, NVPair[] ct_hdr)
    throws IOException

参数opts、files、ct_hdr分别对应 name、files和parameter,然后将返回值作为参数,以request100.POST(url1, data, headers)形式发送出去即可。

这时细心的同学可能会发现,在代码里我们并没有将 headers 中的 Content-Type 设置为 multipart/form-data,也没有定义boundary,请求是怎么发送成功的呢?我们先来看看 mpFormDataEncode 方法的部分源码:

public class Codecs
{
  ... ...
  public static final byte[] mpFormDataEncode(NVPair[] opts, NVPair[] files, NVPair[] ct_hdr)throws IOException
  {
    return mpFormDataEncode(opts, files, ct_hdr, null);
  }
  public static final byte[] mpFormDataEncode(NVPair[] opts, NVPair[] files, NVPair[] ct_hdr, FilenameMangler mangler)
    throws IOException
  {
    /* 首先对相关消息体进行填充*/
    byte[] boundary = "\r\n----------ieoau._._+2_8_GoodLuck8.3-dskdfJwSJKl234324jfLdsjfdAuaoei-----".getBytes("8859_1");
    byte[] cont_disp = "\r\nContent-Disposition: form-data; name=\"".getBytes("8859_1");
    byte[] cont_type = "\r\nContent-Type: ".getBytes("8859_1");
    byte[] filename = "\"; filename=\"".getBytes("8859_1");
    ... ...
    /* 此处对应传入的headers参数*/
    ct_hdr[0] = new NVPair("Content-Type", "multipart/form-data; boundary=" + new String(boundary, 4, boundary.length - 4, "8859_1"));
    ... ...
  }
}

从源码可以看到,代码中首先会对消息体进行填充相关信息,然后进行编码,最后会把 Content-Type 塞到我们传递的那个空headers参数里,其中参数 ct_hdr 对应传入的headers参数,这样就提供了 multipart/form-data 方式上传文件数据的必要信息。至此,Grinder脚本就可以发送 multipart/form-data 请求上传文件了。



网易云产品免费体验馆无套路试用,零成本体验云计算价值。  

本文来自网易实践者社区,经作者牛小宝授权发布