HttpURLConnection实现HTTP文件上传

实现功能前我们先想办法弄明白文件上传过程中请求的完整格式。
先创建一个Spring MVC的服务,实现一个简单的Form表单文件上传功能。偏服务端方向,在这里就不累述了,其实Spring MVC的官方sample里集成了文件上传的demo,代码直接拿来用就行,这不是本篇文章的重点。
打开浏览器,访问localhost:8080,选择一个图片上传

同时用Charles抓包,点击upload按钮后整个请求的request如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POST /upload HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (X11; Linux i586; rv:31.0) Gecko/20100101 Firefox/31.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://localhost:8080/upload
Cookie: CNZZDATA1258283573=524895976-1459330515-%7C1459476924; JSESSIONID=7863BDEF16DC5F5D91F72010F74D118F
Connection: keep-alive
Content-Type: multipart/form-data; boundary=---------------------------17158034114497953721118848252
Content-Length: 1026
-----------------------------17158034114497953721118848252
Content-Disposition: form-data; name="file"; filename="1394108530_download2.png"
Content-Type: image/png
�PNG××××××××××
-----------------------------17158034114497953721118848252--

阅读http协议的格式说明可知,header和body之间会有一个空行,所以1~11行为header的内容,13~18行为body的内容。在这里说明一下,第17行由于是上传文件的字节数据,存在乱码,所以粘贴到这里显示会有问题,删除了大部分的内容,完整格式可查看截图!

这是一个最标准的http POST Form 方式上传文件的请求原始协议内容,我们只要按照这个格式模拟请求,就可以实现文件上传功能了。

header部分比较简单,第10行Content-Type:multipart/form-data可以看出这是一个FORM表单请求,后面的boundary根据http协议可以知道其实是随机生成的一个字符串,标识唯一性,这个

“—————————17158034114497953721118848252”

是Firefox的算法生成的,如果在chrome下打开会不一样。提到随机生成一个唯一的字符串,在java里可以用UUID类实现。

主要看body部分,13行这里需要说明一点,这个字符串和上面随机生成的boundary并不相等,由于Firefox的生成算法默认是以27个”-“开头,细心的话可以发现这里的”-“有29个,阅读http协议知道这里应该是”–” + 上面生成的boundary组成。
第14行这里name的值要和服务端Form表单填写的值相对应,filename是要上传文件的名字。
15行根据上传文件的格式不同设置不同的Content-Type
紧接着第16行是一个空行
再下面就是要上传文件的字节数据
最后一行为”–” + boundary + “–”

分析完了,代码就比较简单了,直接贴出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
/**
* 模拟POST Form 方式上传文件到服务器
* @param file 需要上传的文件
* @param uploadUrl 文件上传地址
* @return 返回响应的内容
*/
private String uploadFile(File file, String uploadUrl) {
String BOUNDARY = UUID.randomUUID().toString(); // 边界标识 随机生成
String PREFIX = "--";
String LINE_END = "\r\n"; //换行符
String CONTENT_TYPE = "multipart/form-data"; // From表单
HttpURLConnection conn = null;
BufferedReader br = null;
try {
URL url = new URL(uploadUrl);
conn = (HttpURLConnection) url.openConnection();
conn.setReadTimeout(READ_TIME_OUT);
conn.setConnectTimeout(CONNECT_TIME_OUT);
conn.setDoInput(true);
conn.setDoOutput(true);
conn.setUseCaches(false);
conn.setRequestMethod(HttpPost.METHOD_NAME);
conn.setRequestProperty("Charset", CHARSET);
conn.setRequestProperty("connection", "keep-alive");
conn.setRequestProperty("Content-Type", CONTENT_TYPE + ";boundary=" + BOUNDARY);
conn.setRequestProperty("Content-Length", String.valueOf(file.length()));
if (file != null && file.exists()) {
// 当文件不为空,把文件包装并且上传
DataOutputStream dos = new DataOutputStream(conn.getOutputStream());
StringBuffer sb = new StringBuffer();
sb.append(PREFIX);
sb.append(BOUNDARY);
sb.append(LINE_END);
// name里面的值为服务器端需要的Form表单对应的key
// filename是文件的名字,包含后缀名的
sb.append("Content-Disposition: form-data; name=\"userfile\"; filename=\"" + file.getName() + "\""
+ LINE_END);
sb.append("Content-Type: application/octet-stream; charset=" + CHARSET + LINE_END); //此处待优化,应该根据不同文件类型生成不同的Content-Type
sb.append(LINE_END);
dos.write(sb.toString().getBytes());
InputStream is = new FileInputStream(file);
byte[] bytes = new byte[8 * 1024];
int len = 0;
while ((len = is.read(bytes)) != -1) {
dos.write(bytes, 0, len);
}
is.close();
dos.write(LINE_END.getBytes());
byte[] end_data = (PREFIX + BOUNDARY + PREFIX + LINE_END).getBytes();
dos.write(end_data);
dos.flush();
int responseCode = conn.getResponseCode();
if (responseCode == HttpStatus.SC_OK) {
LogUtil.d(TAG, "request success");
br = new BufferedReader(new InputStreamReader(
conn.getInputStream()));
StringBuilder sb = new StringBuilder();
String line = null;
while ((line = br.readLine()) != null) {
sb.append(line);
}
return sb.toString();
} else {
LogUtil.d(TAG, "response error");
}
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (br != null) {
br.close();
}
} catch (IOException e) {
e.printStackTrace();
}
if (conn != null) {
conn.disconnect();
}
}
return null;
}