浅谈 HTTP 表单提交图片
您目前处于:技术核心竞争力  2016-09-01

HTTP 是短连接请求,是需要在 TCP 三次握手成功,发送 GET/POST 请求,服务端返回 200 成功,在进行 TCP 四次分手完成。如下例子:

前三行,是客户端发起 TCP 三次握手。之后的三行,是客户端发送 GET 请求,获取 index.action 请求,服务端收到请求返回 200 OK,由服务端发送四次分手。

我们查看下 GET 请求 index.action 的 TCP 流:

我们从首页调转到栏目页,在进行抓包查看:

我们对表单提交的 POST 请求抓包:

我们对图片上的 POST 请求进行抓包:

POST请求,数据都是放在请求体内,而不是请求头内。

Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryeizpZwA55aMUXGAg

这行指出这个请求是 multipart/form-data 格式的,且 boundary 是 ----WebKitFormBoundaryeizpZwA55aMUXGAg 这个字符串。

boundary 是用来隔开表单中不同部分数据的。boundary 一般由系统随机产生,但也可以简单的用 ------------- 来代替。

关于分界符的规则可以概况为两条:

1. 除了最后一个分界符,每个分界符后面都加一个 CRLF 即 '\u000D' 和 '\u000A',最后一个分界符后面是两个分隔符"--"

2. 每个分界符的开头也要加一个 CRLF 和两个分隔符("-")。

需要注意的是,在 HTML 协议中,用 “/r/n” 换行,而不是 “/n”。

紧接着 boundary 的是该部分数据的描述。

Content-Disposition: form-data; name="uploadPhoto"; filename="TimLine......20160901133702.png"
Content-Type: image/png

浏览器采用默认的编码方式是 application/x-www-form-urlencoded , 可以通过指定 form 标签中的 enctype 属性使浏览器知道此表单是用 multipart/form-data 方式编码。

接下来才是数据。

.PNG
.
...
IHDR.............$3.l....sRGB.........gAMA......

图片上传到服务器,解码对是整个上载过程最繁琐的一个步骤,经过以上的流程, 我们可以得到一个包含有所有上载数据的一个字节数组和一个分界符, 还可以得到每个数据段中的分界符。 而我们要得到以下内容:

1. 提交的表单中的各个字段以及对应的值

2. 如果表单中有 file 控件,并且用户选择了上载文件, 则需要分析出字段的名称、文件在浏览器端的名字、文件的 Content-Type 和文件的内容。

字节数组的内容可以分解如下:

具体解码过程也可以分为两个步骤:

1. 将上载的数据分解成数据段,每个数据段对应着表单中的一个 Input 区。

2. 对每个数据段,再进行分解,提出上述要求得到的内容。

这两个步骤主要的操作有两个,一个是从一个数组中找出另一个数组的位置,类似于 String 类中的 indexOf 的功能,另一个是从一个数组中提取出另一个数组, 类似于 String 类中的 substring 的功能,为此我们可以专门写两个方法,实现这种功能。

int byteIndexOf (byte[] source,byte[] search,int start)
byte[] subBytes(byte[] source,int from,int end)

为了便于使用,可以从这两个方法中衍生出下列方法

int byteIndexOf (byte[] source,String search,int start)   以一个 String 作为搜索对象参数
String subBytesString(byte[] source,int from,int end)     直接返回一个 String
int bytesLen(String s)                  返回字符串转化为字节数组后,字节数组的长度

这样,从一个字节数组中,根据标记提取出另一个字节数组可以表示如下:

假设我们已经将数据存入字节数组 buffer 中,分界符存入 String boundary 中:

int pos1=0; //pos1 记录 在buffer 中下一个 boundary 的位置
//pos0,pos1 用于 subBytes 的两个参数
int pos0=byteIndexOf(buffer,boundary,0); //pos0 记录 boundary 的第一个字节在buffer 中的位置
do {
    pos0+=boundaryLen;
    //记录boundary后面第一个字节的下标
    pos1=byteIndexOf(buffer,boundary,pos0);
    if (pos1==-1)
        break;
    pos0+=2; //考虑到boundary后面的 \r\n
    PARSE[(subBytes(buffer,pos0,pos1-2));]
    //考虑到boundary后面的 \r\n
    pos0=pos1;
} while(true);

其中 PARSE 部分是对每一个数据段进行解码的方法,考虑到 Content-Disposition 等属性,首先定义一个 String 数组:

String[] tokens={"name=\"",
"\"; filename=\"",
"\"\r\n",
"Content-Type: ",
"\r\n\r\n"
};

对于一个不是文件的数据段,只可能有 tokens 中的第一个元素和最后一个元素,如果是一个文件数据段,则包含所有的元素。第一步先得到 tokens 中每个元素在这个数据段中的位置。

int[] position=new int[tokens.length];
for (int i=0;i < tokens.length ;i++ )
{
    position[i]=byteIndexOf(buffer,tokens[i],0);
}

第二步判断是否是一个文件数据段,如果是一个文件 数据段则 position[1] 应该大于0,并且 postion[1] 应该小于 postion[2] 即 position[1] > 0 && position[1] < position[2] 如果为真,则为一个文件数据段。

1. 得到字段名

String name =subBytesString(buffer,position[0]+bytesLen(tokens[0]),position[1]);

2. 得到文件名

String file= subBytesString(buffer,position[1]+bytesLen(tokens[1]),position[2]);

3. 得到 Content-Type

String contentType=subBytesString(buffer,position[3]+bytesLen(tokens[3]),position[4]);

4. 得到文件内容

byte[] b=subBytes(buffer,position[4]+bytesLen(tokens[4]),buffer.length);

否则,说明数据段是一个 name/value 型的数据段,

且name 在 tokens[0] 和 tokens[2] 之间,value 在 tokens[4]之后

1. 得到 name

String name =subBytesString(buffer,position[0]+bytesLen(tokens[0]),position[2]);

2. 得到 value

String value= subBytesString(buffer,position[4]+bytesLen(tokens[4]),buffer.length);



转载请并标注: “本文转载自 linkedkeeper.com ”  ©著作权归作者所有