背景
在有心课堂《自己动手写HTTP框架》课程中有下列课程:
自拍要发朋友圈如何实现
通过自己写的HTTP框架实现将图片和文字等内容在一个接口中提交到后台。
如果你对http请求不是很熟悉,参考:
分析
课程中上传图片相关代码如下图所示:
从上面的代码中可以看出,把图片放在了列表中,图片描述放在了request.content中。
通过对该方法运行时的网络请求抓包分析如下:
--7d4a6d158c9Content-Disposition: form-data; name="data"Content-Type: text/plain; charset=UTF-8Content-Transfer-Encoding: 8bitstay4it--7d4a6d158c9Content-Disposition: form-data; name="file0"; filename="test.png"Content-Type: image/png
返回结果抓包分析如下:
从上图Contents项中可以看到有两个关键字段,分别是data和file0字段。
这两个字段是怎么产生的呢?
通过查看《自己动手写HTTP框架》相关代码,有如下方法:
public static void upload(OutputStream out, String filePath) throws AppException { String BOUNDARY = "7d4a6d158c9"; // 数据分隔线 DataOutputStream outStream = new DataOutputStream(out); try { outStream.writeBytes("--" + BOUNDARY + "\r\n"); outStream.writeBytes("Content-Disposition: form-data; name=\"file0\"; filename=\"" + filePath.substring(filePath.lastIndexOf("/") + 1) + "\"" + "\r\n"); outStream.writeBytes("\r\n"); byte[] buffer = new byte[1024]; FileInputStream fis = new FileInputStream(filePath); while (fis.read(buffer, 0, 1024) != -1) { outStream.write(buffer, 0, buffer.length); } fis.close(); outStream.write("\r\n".getBytes()); byte[] end_data = ("--" + BOUNDARY + "--\r\n").getBytes();// 数据结束标志 outStream.write(end_data); outStream.flush(); } catch (Exception e) { throw new AppException(AppException.ErrorType.UPLOAD, e.getMessage()); } }
这个是单张图片上传,紧接着看多张图片上传,代码如下:
/** * @param out * @param postContent * @param entities */ public static void upload(OutputStream out, String postContent, ArrayListentities) throws AppException { String BOUNDARY = "7d4a6d158c9"; // 数据分隔线 String PREFIX = "--", LINEND = "\r\n"; String CHARSET = "UTF-8"; DataOutputStream outStream = new DataOutputStream(out); try { StringBuilder sb = new StringBuilder(); sb.append(PREFIX); sb.append(BOUNDARY); sb.append(LINEND); sb.append("Content-Disposition: form-data; name=\"" + "data" + "\"" + LINEND); sb.append("Content-Type: text/plain; charset=" + CHARSET + LINEND); sb.append("Content-Transfer-Encoding: 8bit" + LINEND); sb.append(LINEND);// post content sb.append(postContent); sb.append(LINEND); outStream.write(sb.toString().getBytes()); int i = 0; for (FileEntity entity : entities) { StringBuilder sb1 = new StringBuilder(); sb1.append(PREFIX); sb1.append(BOUNDARY); sb1.append(LINEND); sb1.append("Content-Disposition: form-data; name=\"file" + (i++) + "\"; filename=\"" + entity.getFileName() + "\"" + LINEND); sb1.append("Content-Type: " + entity.getFileType() + LINEND); sb1.append(LINEND); outStream.write(sb1.toString().getBytes()); InputStream is = new FileInputStream(entity.getFilePath()); byte[] buffer = new byte[1024]; int len = 0; while ((len = is.read(buffer)) != -1) { outStream.write(buffer, 0, len); } is.close(); outStream.write(LINEND.getBytes()); } byte[] end_data = (PREFIX + BOUNDARY + PREFIX + LINEND).getBytes(); outStream.write(end_data); outStream.flush(); } catch (Exception e) { throw new AppException(AppException.ErrorType.UPLOAD, e.getMessage()); } }
通过对上面两段代码的比较,发现主区别在这个地方:
这个地方也是我们使用Retrofit上传的关键点所在,后面我们会再提到。
上面分析了这么多,我们来看看怎么使用retrofit来实现。
Retrofit实现文件和图片一起上传
如果对retrofit不是很了解,参考:
定义接口
在码小白的博客 中有下面内容:
图片和字符串同时上报
@Multipart@POST("upload/")Callregister( @Query("name") String cardName, @Query("phone") String callphone, @Query("address") String address, @Part("avatar\\"; filename=\\"avatar.jpg") RequestBody avatar, );
这种接口应该也是可以的,具体要怎么实现,都要与服务器接口保持一致,所以不能照搬照抄了。
根据对有心课堂提供的上传图片接口的大量抓包和测试总结,接口定义如下:
@Multipart@POST("v1/public/core/?service=user.updateAvatar")Observable
>> uploadMultipleTypeFile(@Part("data") String des, @PartMap Map params);
这里用到了@Partmap注解,将图片文件信息放入map中。
准备图片
在sdcard根目录存放两张图片,分别为test.png和test.jpg(不要是gif图片啊,服务器不支持)
代码实现
这里就不贴代码了,截图如下(如果看不清,鼠标右键在新窗口打开就可以看到原图了):
关键代码在于:
bodyMap.put("file"+i+"\"; filename=\""+file.getName(), RequestBody.create(MediaType.parse("image/png"),file));
看到这个是不是想起了上面我们提到的关键代码呢?下面再贴出来我们对比下。
sb1.append("Content-Disposition: form-data; name=\"file" + (i++) + "\"; filename=\"" + entity.getFileName() + "\"" + LINEND);
只要将对应的http请求头信息填写正确,就能上传成功。
那么问题又来了,怎么分析和正确拼写这个请求头呢?
在文章开头的时候有个抓包信息:
Content-Disposition: form-data; name="file0"; filename="test.png"
实质上上传文件Requestbody对应的请求头就是 name="file0"; filename="test.png",只要拼对了就没有问题了。
注意:
name="file0"; filename="test.png"这个请求头是根据有心课堂提供的上传接口写的,不适用其他上传接口,但原理是类似的;
单张图片上传通用的请求头是:name="file"; filename="test.png"
filename="test.png"这个一般是指(你希望)保存在服务器的文件名字。
举例说明
比如我们这样写请求头信息,如下代码所示:
bodyMap.put("name=\"file"+i+"\"; filename=\""+file.getName()+"\"", RequestBody.create(MediaType.parse("image/png"),file));
运行请求抓包请求头信息如下图所示:
出现了name="name="file1"这样的字段,拼接错误(不用加name字段),服务器也毫不留情的返回了错误:
{ "ret": 403, "data": [], "msg": "\u975e\u6cd5\u8bf7\u6c42\uff1a\u6587\u4ef6\u4e0a\u4f20\u51fa\u9519"}
这个问题我当初没有发现,后来还是请教了有心课堂的Stay才搞明白了。
好了,不知道我讲的大家明白了没有,最后来个成功运行的请求抓包截图吧:
关于文字类参数上传
写到最后忘了说文字参数了,文字参数相对文件来说容易些。
在接口中,我们有一个文字参数 @Part("data") String des
,如果你需要多个,增加就行了。需要注意的是这个参数的名字比如"data",不是前端自定义,而是后台接口定义的。
代码托管地址:
2016.8.19 凌晨