前段时间搭建了FastDFS用作文件存储,既然是文件存储,必然需要有文件才能存储。文件可能是由客户端传递上去,可以是视频、也可以是图片等。现在需要提供一个Android端传递视频文件的功能,一说到这,大家肯定想说,okhttp现实一个post表单就搞定了,但是post表单是需要服务端进行接收流,然后采用文件IO方式输出成视频,但这次我打算使用PUT方式上传一个文件。
传统的方式,网上应该有很多,客户端POST文件并用servlet作为服务器接收。这次介绍PUT方式,首先需要一个容器能作为文件存储的地址,选择使用Apache Http Server作为服务器,可以去官网下载http://httpd.apache.org/ ,最好安装在Linux。由于公司分配的虚拟机就包含了这个功能,所以就不给大家演示怎么安装Apache Http Server,应该跟Tomcat差不多。
Apache默认是不支持PUT方式的,现在要配置。
vim /etc/httpd/conf/httpd.conf
添加下面两行,如果有就不必添加,一般都是有的。
LoadModule dav_module modules/mod_dav.so
LoadModule dav_fs_module modules/mod_dav_fs.so
配置一个接收文件的路径,由于apache有一个默认路径/var/www/html,所以我直接在这个路径下加入以下配置。
别忘了设置监听端口,默认是80。
<Directory "/var/www/html/video">
Dav On
AllowOverride None
Options All
Order allow,deny
Allow from all
Directory>
到该/var/www/html目录下,创建一个video文件夹,并增加可写权限。
mkdir video
chmod 777 video -R
启动httpd服务,已经启动了可以重启,并查看运行状态。
service httpd start
service httpd restart
service httpd status
现在可以开始测试
curl --request PUT --data-binary @/root/install/1.mp4 --header "Content-Type: application/octet-stream" http://172.16.0.245:8200/video/mf.mp4
可以看到将1.mp4这个视频上传到apache服务器上,并且改名为mf.mp4。打开网页浏览http://172.16.0.245:8200/video/ 就能看到这个视频。
到这里,apache http就搭建好了,接下来要讲本文重点,android端使用retrofit2+rxjava+okhttp调用put请求上传视频。
在这里,记录一下遇到的坑,如果对retrofit+rxjava还不是很熟的可以先去了解一下。
首先看一下接口服务类。
public interface RestService {
/**
* 没有使用,只是拿出来作对比
*/
@Multipart
@POST()
Observable postFormVideoFile(@Url String url, @Part MultipartBody.Part file);
@Multipart
@PUT()
Observable putFormVideoFile(@Url String url, @Part MultipartBody.Part file);
@PUT()
Observable putBodyVideoFile(@Url String url, @Body RequestBody file);
}
分别说明一下三个方法的作用:
先介绍一下put表单的方式,为了找到原因,抓包分析。
Linux中执行
tcpdump -i any host 172.16.0.245 and port 8200 -w ./putForm.pcap
然后点击第二个按钮进行上传。
上传完成可以看到,的确生成了一个putForm.mp4文件,此时可以看到文件大小不对,android上显示的是10652806字节,上传文件的大小10653018字节,源视频大小就是10652806,可是android端显示的是没错,怎么传上去就有问题了。注意刚刚我们通过抓包生成了putForm.pcap,等会会分析抓包内容,和android端监听进度的方式来说明。
同样,先抓包。
tcpdump -i any host 172.16.0.245 and port 8200 -w ./putBody.pcap
然后点击第一个按钮进行上传。过程跟上面的git图一样,但多了两个文件。这时可以看到新多出两个文件。
其中putBody.mp4能正常播放,而putForm.mp4就不能播放,比较两者的抓包数据。
1.putBody.pcap中,只有一个消息头,里面直接给出了 Content-Length:10652806,这个就是源视频文件大小。再看一下这个方法,第二个参数以消息体的方式加入到http协议中,apache能直接将这个数据导出生成putBody.mp4。
@PUT()
Observable putBodyVideoFile(@Url String url, @Body RequestBody file);
2.putForm.pcap中,包含两个部分,一个是http消息头,一个是Part,这是很标准的表单提交消息体。首先生成了一个 boundary 用于分割不同的字段,用作每个Part的分割线,每一个Part可以表示为一个文件数据(从方法中也能看到第二个参数是一个Part)。Part中的Content-Length:10652806才是表示这个数据的正确大小,Http消息头中的Content-Length:10653018表示传输长度,将这个数据导出生成putForm.mp4是一个无效的mp4文件,也就不能播放。
@Multipart
@PUT()
Observable putFormVideoFile(@Url String url, @Part MultipartBody.Part file);
至于为什么两个方法调用的时候,demo上显示的上传大小都是10653018。CountingRequestBody是继承RequestBody,表示一个请求体,重写了contentLength(),表示这个文件的长度,而界面上显示的最大值,其实只是这个Body的大小,并不是http传输长度。
private static MediaType MEDIA_TYPE_PLAIN = MediaType.parse("multipart/form-data");
requestBody = RequestBody.create(MEDIA_TYPE_PLAIN, file);
public class CountingRequestBody extends RequestBody {
//实际的待包装请求体
protected RequestBody delegate;
//进度回调接口
protected Listener listener;
protected CountingSink countingSink;
public CountingRequestBody(RequestBody delegate, Listener listener) {
this.delegate = delegate;
this.listener = listener;
}
/**
* 重写调用实际的响应体的contentLength
* @return contentLength
* @throws IOException 异常
*/
@Override
public long contentLength() {
try {
return delegate.contentLength();
} catch (IOException e) {
e.printStackTrace();
}
return -1;
}
一开始,想着效仿post表单的形式改为put表单,结果一路是坑,后来在网上发现一个用okhttp实现put方式能达到我的目的,对照那个demo和抓包才找到原因。总结一下:
1. 采用表单的形式提交文件,最好是以post请求发送到服务器,由服务器进行接收处理。
2. 采用PUT方式,则直接用RequestBody的方式包装文件,并以二进制流的形式传输。
写次博客主要是为了做个记录,给自己一个做笔记的地方。文中对Http协议以及对上传文件的方式的理解感觉不是很到位,不足之处,希望能给出一些意见。
源码demo