上传头像的问题
8月份的时候曾经在项目中遇到要上传图片到服务器的问题,其实需求很典型:就是用户需要上传自己的头像。我们的项目使用的网络框架是很流行的Retrofit,而网络上常见的Retrofit的教程告诉我们正确的定义服务的姿势是像这样的:
public interface MusicListService {
@GET("music/listMusic.do")
Observable<List<Music>> getMusicList(@Query("start") int start, @Query("size") int size, @Query("categoryId") long parent_id);
}
这样形式的接口明显不能用来上传头像,所以我就需要琢磨怎么实现图片的上传。实际上关于用Retrofit上传图片的博客在网上实在太多,为什么我还要单独写一篇,主要是记录一下踩过的坑。而且,很多时候我们只知道怎么做,却不知道为什么要这样做,实在不应该。只说怎么做的博客太多,我想指出来为什么这么做。
上传实践
以下给出一个同时传递字符串参数和一张图片的服务接口的定义:
public interface UploadAvatarService {
@Multipart
@POST("user/updateAvatar.do")
Call<Response> updateAvatar (@Query("des") String description, @Part("uploadFile\"; filename=\"test.jpg\"") RequestBody imgs );
}
然后在实例化UploadAvatarService的地方,调用以下代码实现图片上传。以下函数被我删改过,可能无法一次完美运行,但大体是没错的。
private void uploadFile(final String filename) {
UploadAvatarService service = RetrofitUtil.createService(getContext(), UploadAvatarService.class);
final File file = new File(filename);
RequestBody requestBody = RequestBody.create(MediaType.parse("multipart/form-data"), file);
Call<Response> call = service.updateInfo(msg, requestBody );
call.enqueue(new Callback<Response>() {
@Override
public void onResponse(Call<Response> call, retrofit2.Response<Response> response) {
//。。。。。
}
@Override
public void onFailure(Call<Response> call, Throwable t) {
//。。。
}
});
}
POST实际提交的内容
历史上(1995年之前),POST上传数据的方式是很单一的,就像我们用GET方法传参一样,参数名=参数值,参数和参数之间用&隔开。就像这样:
param1=abc¶m2=def
只不过GET的参数列表放在URL里面,像这样:
http://url:port?param1=abc¶m2=def
而用POST传参时,参数列表放到HTTP报文的请求体里了,此时的请求头长这样。
POST http://www.test.org HTTP/1.1
Content-Type:application/x-www-form-urlencoded; charset=UTF-8
以前POST只支持纯文本的传输,上传文件就很恼火,奇技淫巧应该也可以实现上传文件,比如将二进制流当成文本传输。后来互联网工程任务组(IETF)在1995年11月推出了RFC1867。在RFC1867中提出了基于表单的文件上传标准。
This proposal makes two changes to HTML:
- Add a FILE option for the TYPE attribute of INPUT.
- Allow an ACCEPT attribute for INPUT tag, which is a list of
media types or type patterns allowed for the input.
In addition, it defines a new MIME media type, multipart/form-data,
and specifies the behavior of HTML user agents when interpreting a
form with
ENCTYPE="multipart/form-data" and/or <INPUT type="file">
tags.
简而言之,就是要增加文件上传的支持,另外还为<input>标签添加一个叫做accept的属性,同时定义了一个新的MIME类型,叫做multipart/form-data。而multipart,则是在我们传输非纯文本的数据时采用的数据格式,比如我们上传一个文件时,HTTP请求头像这样:
POST http://www.test.org HTTP/1.1
Content-Type:multipart/form-data; boundary=---------------------------7d52b13b519e2
如果要传输两张图片,请求体长这样:
-----------------------------7d52b13b519e2
Content-Disposition: form-data; name="upload1"; filename="test.jpg"
Content-Type: image/jpeg
/**此处应是test.jpg的二进制流**/
-----------------------------7d52b13b519e2
Content-Disposition: form-data; name="upload2"; filename="test2.jpg"
Content-Type: image/jpeg
/**此处应是test2.jpg的二进制流**/
-----------------------------7d52b13b519e2--
看到请求体里面,分隔两张图片的二进制流和描述信息的是一段长长的横线和一段十六进制表示的数字,这个东西称作boundary,在RFC1867中提到了:
3.3 use of multipart/form-data
The definition of multipart/form-data is included in section 7. A
boundary is selected that does not occur in any of the data. (This
selection is sometimes done probabilisticly.) Each field of the form
is sent, in the order in which it occurs in the form, as a part of
the multipart stream. Each part identifies the INPUT name within the
original HTML form. Each part should be labelled with an appropriate
content-type if the media type is known (e.g., inferred from the file
extension or operating system typing information) or as
application/octet-stream.
可以看到这串数字就是为了分隔不同的part的,这串数字可以是随机生成的,但是不能出现在提交的表单数据里(这个很好理解,如果跟表单数据中的某一部分冲突了,就起不到分隔的作用了)。
回归那段代码
在用Retrofit上传文件的那段代码中,使用@Multipart说明将使用Multipart格式提交数据,而@Part这个注解后面跟的参数则是以下请求头中加粗的部分:
-----------------------------7d52b13b519e2
Content-Disposition: form-data; name="upload1"; filename="test.jpg"
Content-Type: image/jpeg
这段加粗的部分放到Java代码中需要进行转义(对双引号和分号转义),就得到了如下的一个参数声明:
@Part("uploadFile\"; filename=\"test.jpg\"") RequestBody imgs
所以@Part后面跟的参数虽然看起来很奇怪,但如果知道了实际传输的数据格式,这一写法就很好理解了。