前言
在开发当中,我们常常需要实现文件上传,比较常见的就是图片上传,比如修改个头像什么的。但是这个功能在Android和iOS中都没有默认的实现类,对于Android我们可以使用Apache提供的HttpClient.jar来实现这个功能,其中依赖的类就是Apache的httpmime.jar中的MultipartEntity这个类。我就是要实现一个文件上传功能,但是我还得下载一个jar包,而这个jar包几十KB,这尼玛仿佛并非人间!今天我们就来自己实现文件上传功能,并且弄懂它们的原理。
在上一篇文章HTTP POST请求报文格式分析与Java实现文件上传中我们介绍了HTTP POST报文格式,如果有对POST报文格式不了解的同学可以先阅读这篇文章。
自定义实现MultipartEntity
我们知道,使用网络协议传输数据无非就是要遵循某个协议,我们在开发移动应用时基本上都是使用HTTP协议。HTTP协议说白了就是基于TCP的一套网络请求协议,你根据该协议规定的格式传输数据,然后服务器返回给你数据。你的协议参数要是传递错了,那么服务器只能给你返回错误。
这跟间谍之间对暗号有点相似,他们有一个规定的暗号,双方见面,A说: 天王盖地虎,B对: 宝塔镇河妖。对上了,说事;对不上,弄死这B。HTTP也是这样的,在HTTP请求时添加header和参数,服务器根据参数进行解析。形如 :
1
2
3
4
5
6
7
|
POST /api/feed/ HTTP/
1.1
这里是header数据
--分隔符
参数
1
--分隔符
参数
2
|
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
|
public
class
MultipartEntity
implements
HttpEntity {
private
final
static
char
[] MULTIPART_CHARS =
"-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
.toCharArray();
/**
* 换行符
*/
private
final
String NEW_LINE_STR =
"\r\n"
;
private
final
String CONTENT_TYPE =
"Content-Type: "
;
private
final
String CONTENT_DISPOSITION =
"Content-Disposition: "
;
/**
* 文本参数和字符集
*/
private
final
String TYPE_TEXT_CHARSET =
"text/plain; charset=UTF-8"
;
/**
* 字节流参数
*/
private
final
String TYPE_OCTET_STREAM =
"application/octet-stream"
;
/**
* 二进制参数
*/
private
final
byte
[] BINARY_ENCODING =
"Content-Transfer-Encoding: binary\r\n\r\n"
.getBytes();
/**
* 文本参数
*/
private
final
byte
[] BIT_ENCODING =
"Content-Transfer-Encoding: 8bit\r\n\r\n"
.getBytes();
/**
* 分隔符
*/
private
String mBoundary =
null
;
/**
* 输出流
*/
ByteArrayOutputStream mOutputStream =
new
ByteArrayOutputStream();
public
MultipartEntity() {
this
.mBoundary = generateBoundary();
}
/**
* 生成分隔符
*
* @return
*/
private
final
String generateBoundary() {
final
StringBuffer buf =
new
StringBuffer();
final
Random rand =
new
Random();
for
(
int
i =
0
; i <
30
; i++) {
buf.append(MULTIPART_CHARS[rand.nextInt(MULTIPART_CHARS.length)]);
}
return
buf.toString();
}
/**
* 参数开头的分隔符
*
* @throws IOException
*/
private
void
writeFirstBoundary()
throws
IOException {
mOutputStream.write((
"--"
+ mBoundary +
"\r\n"
).getBytes());
}
/**
* 添加文本参数
*
* @param key
* @param value
*/
public
void
addStringPart(
final
String paramName,
final
String value) {
writeToOutputStream(paramName, value.getBytes(), TYPE_TEXT_CHARSET, BIT_ENCODING,
""
);
}
/**
* 将数据写入到输出流中
*
* @param key
* @param rawData
* @param type
* @param encodingBytes
* @param fileName
*/
private
void
writeToOutputStream(String paramName,
byte
[] rawData, String type,
byte
[] encodingBytes,
String fileName) {
try
{
writeFirstBoundary();
mOutputStream.write((CONTENT_TYPE + type + NEW_LINE_STR).getBytes());
mOutputStream
.write(getContentDispositionBytes(paramName, fileName));
mOutputStream.write(encodingBytes);
mOutputStream.write(rawData);
mOutputStream.write(NEW_LINE_STR.getBytes());
}
catch
(
final
IOException e) {
e.printStackTrace();
}
}
/**
* 添加二进制参数, 例如Bitmap的字节流参数
*
* @param key
* @param rawData
*/
public
void
addBinaryPart(String paramName,
final
byte
[] rawData) {
writeToOutputStream(paramName, rawData, TYPE_OCTET_STREAM, BINARY_ENCODING,
"no-file"
);
}
/**
* 添加文件参数,可以实现文件上传功能
*
* @param key
* @param file
*/
public
void
addFilePart(
final
String key,
final
File file) {
InputStream fin =
null
;
try
{
fin =
new
FileInputStream(file);
writeFirstBoundary();
final
String type = CONTENT_TYPE + TYPE_OCTET_STREAM + NEW_LINE_STR;
mOutputStream.write(getContentDispositionBytes(key, file.getName()));
mOutputStream.write(type.getBytes());
mOutputStream.write(BINARY_ENCODING);
final
byte
[] tmp =
new
byte
[
4096
];
int
len =
0
;
while
((len = fin.read(tmp)) != -
1
) {
mOutputStream.write(tmp,
0
, len);
}
mOutputStream.flush();
}
catch
(
final
IOException e) {
e.printStackTrace();
}
finally
{
closeSilently(fin);
}
}
private
void
closeSilently(Closeable closeable) {
try
{
if
(closeable !=
null
) {
closeable.close();
}
}
catch
(
final
IOException e) {
e.printStackTrace();
}
}
private
byte
[] getContentDispositionBytes(String paramName, String fileName) {
StringBuilder stringBuilder =
new
StringBuilder();
stringBuilder.append(CONTENT_DISPOSITION +
"form-data; name=\""
+ paramName +
"\""
);
// 文本参数没有filename参数,设置为空即可
if
(!TextUtils.isEmpty(fileName)) {
stringBuilder.append(
"; filename=\""
+ fileName +
"\""
);
}
return
stringBuilder.append(NEW_LINE_STR).toString().getBytes();
}
@Override
public
long
getContentLength() {
return
mOutputStream.toByteArray().length;
}
@Override
public
Header getContentType() {
return
new
BasicHeader(
"Content-Type"
,
"multipart/form-data; boundary="
+ mBoundary);
}
@Override
public
boolean
isChunked() {
return
false
;
}
@Override
public
boolean
isRepeatable() {
return
false
;
}
@Override
public
boolean
isStreaming() {
return
false
;
}
@Override
public
void
writeTo(
final
OutputStream outstream)
throws
IOException {
// 参数最末尾的结束符
final
String endString =
"--"
+ mBoundary +
"--\r\n"
;
// 写入结束符
mOutputStream.write(endString.getBytes());
//
outstream.write(mOutputStream.toByteArray());
}
@Override
public
Header getContentEncoding() {
return
null
;
}
@Override
public
void
consumeContent()
throws
IOException,
UnsupportedOperationException {
if
(isStreaming()) {
throw
new
UnsupportedOperationException(
"Streaming entity does not implement #consumeContent()"
);
}
}
@Override
public
InputStream getContent() {
return
new
ByteArrayInputStream(mOutputStream.toByteArray());
}
}
|
用户可以通过addStringPart、addBinaryPart、addFilePart来添加参数,分别表示添加字符串参数、添加二进制参数、添加文件参数。在MultipartEntity中有一个ByteArrayOutputStream对象,先将这些参数写到这个输出流中,当执行网络请求时,会执行
1
|
writeTo(
final
OutputStream outstream)
|
例如我要向服务器发送一个文本、一张bitmap图片、一个文件,即这个请求有三个参数。代码如下 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
MultipartEntity multipartEntity =
new
MultipartEntity();
// 文本参数
multipartEntity.addStringPart(
"type"
,
"我的文本参数"
);
Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.thumb);
// 二进制参数
multipartEntity.addBinaryPart(
"images"
, bitmapToBytes(bmp));
// 文件参数
multipartEntity.addFilePart(
"images"
,
new
File(
"storage/emulated/0/test.jpg"
));
// POST请求
HttpPost post =
new
HttpPost(
"url"
) ;
// 将multipartEntity设置给post
post.setEntity(multipartEntity);
// 使用http client来执行请求
HttpClient httpClient =
new
DefaultHttpClient() ;
httpClient.execute(post) ;
|
MultipartEntity的输出格式会成为如下的格式 :
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
|
POST /api/feed/ HTTP/
1.1
Content-Type: multipart/form-data; boundary=o3Fhj53z-oKToduAElfBaNU4pZhp4-
User-Agent: Dalvik/
1.6
.
0
(Linux; U; Android
4.4
.
4
; M040 Build/KTU84P)
Host: www.myhost.com
Connection: Keep-Alive
Accept-Encoding: gzip
Content-Length:
168518
--o3Fhj53z-oKToduAElfBaNU4pZhp4-
Content-Type: text/plain; charset=UTF-
8
Content-Disposition: form-data; name=
"type"
Content-Transfer-Encoding: 8bit
This my type
--o3Fhj53z-oKToduAElfBaNU4pZhp4-
Content-Type: application/octet-stream
Content-Disposition: form-data; name=
"images"
; filename=
"no-file"
Content-Transfer-Encoding: binary
这里是bitmap的二进制数据
--o3Fhj53z-oKToduAElfBaNU4pZhp4-
Content-Type: application/octet-stream
Content-Disposition: form-data; name=
"file"
; filename=
"storage/emulated/0/test.jpg"
Content-Transfer-Encoding: binary
这里是图片文件的二进制数据
--o3Fhj53z-oKToduAElfBaNU4pZhp4---
|
看到很熟悉吧,这就是我们在文章开头时提到的POST报文格式。没错!HttpEntity就是负责将参数构造成HTTP的报文格式,文本参数该是什么格式、文件该是什么格式,什么类型,这些格式都是固定的。构造完之后,在执行请求时会将http请求的输出流通过writeTo(OutputStream) 函数传递进来,然后将这些参数数据全部输出到http输出流中即可。
明白了这些道理,看看代码也就应该明白了吧。
Volley中实现文件上传
Volley是Google官方推出的网络请求库,这个库很精简、优秀,但是他们也没有默认添加文件上传功能的支持。我们今天就来自定义一个Request实现文件上传功能,还是需要借助上面的MultipartEntity类,下面看代码:
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
|
/**
* MultipartRequest,返回的结果是String格式的
* @author mrsimple
*/
public
class
MultipartRequest
extends
Request<string> {
MultipartEntity mMultiPartEntity =
new
MultipartEntity();
public
MultipartRequest(HttpMethod method, String url,
Map<string, string=
""
> params, RequestListener<string> listener) {
super
(method, url, params, listener);
}
/**
* @return
*/
public
MultipartEntity getMultiPartEntity() {
return
mMultiPartEntity;
}
@Override
public
String getBodyContentType() {
return
mMultiPartEntity.getContentType().getValue();
}
@Override
public
byte
[] getBody() {
ByteArrayOutputStream bos =
new
ByteArrayOutputStream();
try
{
// 将mMultiPartEntity中的参数写入到bos中
mMultiPartEntity.writeTo(bos);
}
catch
(IOException e) {
Log.e(
""
,
"IOException writing to ByteArrayOutputStream"
);
}
return
bos.toByteArray();
}
@Override
protected
void
deliverResponse(String response) {
mListener.onResponse(response);
}
@Override
protected
Response<string> parseNetworkResponse(NetworkResponse response) {
String parsed;
try
{
parsed =
new
String(response.data, HttpHeaderParser.parseCharset(response.headers));
}
catch
(UnsupportedEncodingException e) {
parsed =
new
String(response.data);
}
return
Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
}
}
</string></string></string,></string>
|
使用示例代码:
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
|
MultipartRequest multipartRequest =
new
MultipartRequest(HttpMethod.POST,
null
,
new
RequestListener<string>() {
@Override
public
void
onStart() {
// TODO Auto-generated method stub
}
@Override
public
void
onComplete(
int
stCode, String response, String errMsg) {
}
});
// 获取MultipartEntity对象
MultipartEntity multipartEntity = multipartRequest.getMultiPartEntity();
multipartEntity.addStringPart(
"content"
,
"hello"
);
//
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.thumb);
// bitmap参数
multipartEntity.addBinaryPart(
"images"
, bitmapToBytes(bitmap));
// 文件参数
multipartEntity.addFilePart(
"images"
,
new
File(
"storage/emulated/0/test.jpg"
));
// 构建请求队列
RequestQueue queue = RequestQueue.newRequestQueue(Context);
// 将请求添加到队列中
queue.addRequest(multipartRequest);</string>
|