HTTP 是短连接请求,是需要在 TCP 三次握手成功,发送 GET/POST 请求,服务端返回 200 成功,在进行 TCP 四次分手完成。如下例子:
前三行,是客户端发起 TCP 三次握手。之后的三行,是客户端发送 GET 请求,获取 index.action 请求,服务端收到请求返回 200 OK,由服务端发送四次分手。
我们查看下 GET 请求 index.action 的 TCP 流:
我们从首页调转到栏目页,在进行抓包查看:
我们对表单提交的 POST 请求抓包:
我们对图片上的 POST 请求进行抓包:
POST请求,数据都是放在请求体内,而不是请求头内。
1
| 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 的是该部分数据的描述。
1
2
| Content-Disposition: form-data; name="uploadPhoto"; filename="TimLine......20160901133702.png"Content-Type: image/png |
浏览器采用默认的编码方式是 application/x-www-form-urlencoded , 可以通过指定 form 标签中的 enctype 属性使浏览器知道此表单是用 multipart/form-data 方式编码。
接下来才是数据。
1
2
3
4
| .PNG....IHDR.............$3.l....sRGB.........gAMA...... |
1. 提交的表单中的各个字段以及对应的值
2. 如果表单中有 file 控件,并且用户选择了上载文件, 则需要分析出字段的名称、文件在浏览器端的名字、文件的 Content-Type 和文件的内容。
字节数组的内容可以分解如下:
具体解码过程也可以分为两个步骤:
1. 将上载的数据分解成数据段,每个数据段对应着表单中的一个 Input 区。
2. 对每个数据段,再进行分解,提出上述要求得到的内容。
这两个步骤主要的操作有两个,一个是从一个数组中找出另一个数组的位置,类似于 String 类中的 indexOf 的功能,另一个是从一个数组中提取出另一个数组, 类似于 String 类中的 substring 的功能,为此我们可以专门写两个方法,实现这种功能。
1
2
| int byteIndexOf (byte[] source,byte[] search,int start)byte[] subBytes(byte[] source,int from,int end) |
为了便于使用,可以从这两个方法中衍生出下列方法
1
2
3
| int byteIndexOf (byte[] source,String search,int start) 以一个 String 作为搜索对象参数String subBytesString(byte[] source,int from,int end) 直接返回一个 Stringint bytesLen(String s) 返回字符串转化为字节数组后,字节数组的长度 |
这样,从一个字节数组中,根据标记提取出另一个字节数组可以表示如下:
假设我们已经将数据存入字节数组 buffer 中,分界符存入 String boundary 中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| 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 数组:
1
2
3
4
5
6
| String[] tokens={"name=\"","\"; filename=\"","\"\r\n","Content-Type: ","\r\n\r\n"}; |
对于一个不是文件的数据段,只可能有 tokens 中的第一个元素和最后一个元素,如果是一个文件数据段,则包含所有的元素。第一步先得到 tokens 中每个元素在这个数据段中的位置。
1
2
3
4
5
| 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. 得到字段名
1
| String name =subBytesString(buffer,position[0]+bytesLen(tokens[0]),position[1]); |
2. 得到文件名
1
| String file= subBytesString(buffer,position[1]+bytesLen(tokens[1]),position[2]); |
3. 得到 Content-Type
1
| String contentType=subBytesString(buffer,position[3]+bytesLen(tokens[3]),position[4]); |
1
| byte[] b=subBytes(buffer,position[4]+bytesLen(tokens[4]),buffer.length); |
且name 在 tokens[0] 和 tokens[2] 之间,value 在 tokens[4]之后
1. 得到 name
1
| String name =subBytesString(buffer,position[0]+bytesLen(tokens[0]),position[2]); |
1
| String value= subBytesString(buffer,position[4]+bytesLen(tokens[4]),buffer.length); |
转载请并标注: “本文转载自 linkedkeeper.com (文/张松然)”