Java Web 学习笔记之三: HttpURLConnection 模拟表单上传文件

Java Web 学习笔记之三: HttpURLConnection 模拟表单上传文件

描述

我们都知道,Java的HttpURLConnection 在使用post方式发送请求的时候,
会使用一个输出流,将请求的具体数据以网络字节流的形式发送给目标主机。

利用HttpURLConnection发送post请求的代码示意如下:

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
public String executePostByUsual(String actionURL, HashMap<String, String> parameters){
String response = "";
try{
URL url = new URL(actionURL);
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
//发送post请求需要下面两行
connection.setDoInput(true);
connection.setDoOutput(true);
connection.setUseCaches(false);
connection.setRequestMethod("POST");
connection.setRequestProperty("Connection", "Keep-Alive");
connection.setRequestProperty("Charset", "UTF-8");;
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
//设置请求数据内容
String requestContent = "";
Set<String> keys = parameters.keySet();
for(String key : keys){
requestContent = requestContent + key + "=" + parameters.get(key) + "&";
}
requestContent = requestContent.substring(0, requestContent.lastIndexOf("&"));
DataOutputStream ds = new DataOutputStream(connection.getOutputStream());
//使用write(requestContent.getBytes())是为了防止中文出现乱码
ds.write(requestContent.getBytes());
ds.flush();
try{
//获取URL的响应
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "utf-8"));
String s = "";
String temp = "";
while((temp = reader.readLine()) != null){
s += temp;
}
response = s;
reader.close();
}catch(IOException e){
e.printStackTrace();
System.out.println("No response get!!!");
}
ds.close();
}catch(IOException e){
e.printStackTrace();
System.out.println("Request failed!");
}
return response;
}

通过上面的方法我们可以通过HttpURLConnection发送Post请求。但是这时候问题来了,
如果我们想要通过自己编写代码,利用HttpURLConnection发送文件给服务器又该如何做呢?
现在网络上可以找到的开源的网络请求工具有很多,可以发送文件的也有很多,
然而开源在方便广大程序员的同时也会出现很多问题,比方说一些非常棘手的bug,以及不稳定的因素等等。

所以在这里,通过对HttpURLConnection这个Java标准里面的网络工具进行封装,实现文件上传的功能是一个非常不错的选择。

浏览器上传文件,一般是通过表单的形式上传的,在HTML页面中添加表单,
表单里面有文件元素,在用户点击选择文件后提交表单,文件就会跟普通数据一样,
一起发送到目标主机。

利用表单上传文件的另外一个优点是,通过模仿表单请求的格式,
HttpURLConnection发送的数据文件可以被不同环境的服务端识别并且正确接收,
而不用考虑数据流的格式问题造成的平台不兼容的问题。(如果直接通过流传输文件,
则相应的服务器端也要以相应的格式解析流,这样无疑加大了工作量)

说到这里,我们应该如何用HttpURLConnection模仿表单提交文件呢?

步骤

首先我们需要对浏览器的表单数据进行分析。

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
------WebKitFormBoundaryqqB2I9JGBYCWVzUH
Content-Disposition: form-data; name="Acc_name"

admin
------WebKitFormBoundaryqqB2I9JGBYCWVzUH
Content-Disposition: form-data; name="Acc_pwd"

admin
------WebKitFormBoundaryqqB2I9JGBYCWVzUH
Content-Disposition: form-data; name="file"; filename="newFile.txt"
Content-Type: text/plain

addauigd
afcaoiufa
afgda
fefre
afretg
vgdcxv
h
rg
rsdg
sg


segfs
segfse

segfs
------WebKitFormBoundaryqqB2I9JGBYCWVzUH--

这里以chrome浏览器的表单数据为例,我们可以看到,整个输出流的格式为:

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
--分隔符                                                                   -->浏览器随机生成
Content-Disposition: form-data; name="Acc_name" -->这个是文本参数数据
-->空一行
Admin -->数据
--分隔符 -->浏览器随机生成
Content-Disposition: form-data; name="file"; filename="newFile.txt" -->这个是文件参数
Content-Type: text/plain -->文件的类型
-->空一行
addauigd
afcaoiufa
afgda
fefre
afretg
vgdcxv
h
rg
rsdg
sg


segfs
segfse

Segfs -->文件内的具体内容
--分隔符-- -->浏览器随机生成
-->最后以回车结尾

根据浏览器表单的格式,我把请求方法写了出来,代码如下:

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
/**
* @author Johnson
* @method singleFileUploadWithParameters
* @description 集上传单个文件与传递参数于一体的方法
* @param actionURL 上传文件的URL地址包括URL
* @param fileType 文件类型(枚举类型)
* @param uploadFile 上传文件的路径字符串
* @param parameters 跟文件一起传输的参数(HashMap)
* @return String("" if no response get)
* @attention 上传文件name为file(服务器解析)
* */
public String singleFileUploadWithParameters(String actionURL, String uploadFile, MIME_FileType fileType, HashMap<String, String> parameters){
String end = "\r\n";
String twoHyphens = "--";
String boundary = "---------------------------7e0dd540448";
String response = "";
try{
URL url = new URL(actionURL);
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
//发送post请求需要下面两行
connection.setDoInput(true);
connection.setDoOutput(true);
//设置请求参数
connection.setUseCaches(false);
connection.setRequestMethod("POST");
connection.setRequestProperty("Connection", "Keep-Alive");
connection.setRequestProperty("Charset", "UTF-8");
connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
//获取请求内容输出流
DataOutputStream ds = new DataOutputStream(connection.getOutputStream());
String fileName = uploadFile.substring(uploadFile.lastIndexOf(this.PathSeparator) + 1);
//开始写表单格式内容
//写参数
Set<String> keys = parameters.keySet();
for(String key : keys){
ds.writeBytes(twoHyphens + boundary + end);
ds.writeBytes("Content-Disposition: form-data; name=\"");
ds.write(key.getBytes());
ds.writeBytes("\"" + end);
ds.writeBytes(end);
ds.write(parameters.get(key).getBytes());
ds.writeBytes(end);
}
//写文件
ds.writeBytes(twoHyphens + boundary + end);
ds.writeBytes("Content-Disposition: form-data; " + "name=\"file\"; " + "filename=\"");
//防止中文乱码
ds.write(fileName.getBytes());
ds.writeBytes("\"" + end);
ds.writeBytes("Content-Type: " + fileType.getValue() + end);
ds.writeBytes(end);
//根据路径读取文件
FileInputStream fis = new FileInputStream(uploadFile);
byte[] buffer = new byte[1024];
int length = -1;
while((length = fis.read(buffer)) != -1){
ds.write(buffer, 0, length);
}
ds.writeBytes(end);
fis.close();
ds.writeBytes(twoHyphens + boundary + twoHyphens + end);
ds.writeBytes(end);
ds.flush();
try{
//获取URL的响应
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "utf-8"));
String s = "";
String temp = "";
while((temp = reader.readLine()) != null){
s += temp;
}
response = s;
reader.close();
}catch(IOException e){
e.printStackTrace();
System.out.println("No response get!!!");
}
ds.close();
}catch(IOException e){
e.printStackTrace();
System.out.println("Request failed!");
}
return response;
}

利用这个方法,就可以在任意平台(只要支持java)发送带参数以及文件的请求了。
这个方法实现了参数post提交以及单个文件的上传。

小结

利用HttpURLConnection模拟浏览器表单上传文件的重点是表单的格式,
由于获取请求数据时候走了一些弯路,我在写格式的时候没有写对,
导致服务器没法正确解析请求流中的文件。最后经过不懈的努力终于让我找出了问题所在,
就是流的最后那个回车是必须要的,否则服务器就会报流非正常结束的错误。
解决了这个问题之后接下来的工作就轻松的多了。
希望通过我的这篇博客可以帮助到需要学习这一块的同志们少走一些弯路。