採用Retrofit的PUT方式上傳檔案到apache
概述
前段時間搭建了FastDFS用作檔案儲存,既然是檔案儲存,必然需要有檔案才能儲存。檔案可能是由客戶端傳遞上去,可以是視訊、也可以是圖片等。現在需要提供一個Android端傳遞視訊檔案的功能,一說到這,大家肯定想說,okhttp現實一個post表單就搞定了,但是post表單是需要服務端進行接收流,然後採用檔案IO方式輸出成視訊,但這次我打算使用PUT方式上傳一個檔案。
Apache搭建和配置
搭建
傳統的方式,網上應該有很多,客戶端POST檔案並用servlet作為伺服器接收。這次介紹PUT方式,首先需要一個容器能作為檔案儲存的地址,選擇使用Apache Http Server作為伺服器,可以去官網下載
配置
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請求上傳視訊。
Android上傳視訊
在這裡,記錄一下遇到的坑,如果對retrofit+rxjava還不是很熟的可以先去了解一下。
首先看一下介面服務類。
public interface RestService {
/**
* 沒有使用,只是拿出來作對比
*/
@Multipart
@POST()
Observable<ResponseBody> postFormVideoFile(@Url String url, @Part MultipartBody.Part file);
@Multipart
@PUT()
Observable<ResponseBody> putFormVideoFile(@Url String url, @Part MultipartBody.Part file);
@PUT()
Observable<ResponseBody> putBodyVideoFile(@Url String url, @Body RequestBody file);
}
分別說明一下三個方法的作用:
- postFormVideoFile(),這應該是經常使用的上傳檔案方式,Post提交表單,但需要有一個伺服器接收檔案。
- putFormVideoFile(),公司需要,直接採用PUT方式推送到apache上,上面已經搭建好環境了,然後我按照Post表單的形式改成Put表單形式,看上去很美好,其實這是個大坑。
- putBodyVideoFile(),為了解決上面那個方式的坑,直接採用PUT請求體的方式(一個最基本卻容易被遺忘的方式)。
putFormVideoFile
先介紹一下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端監聽進度的方式來說明。
putBodyVideoFile
同樣,先抓包。
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<ResponseBody> 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<ResponseBody> 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