Android使用Retrofit上传图片到服务器

上传头像的问题

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&param2=def

只不过GET的参数列表放在URL里面,像这样:

http://url:port?param1=abc&param2=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:

  1. Add a FILE option for the TYPE attribute of INPUT.
  2. 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后面跟的参数虽然看起来很奇怪,但如果知道了实际传输的数据格式,这一写法就很好理解了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容