1. 程式人生 > >採用Retrofit的PUT方式上傳檔案到apache

採用Retrofit的PUT方式上傳檔案到apache

概述

前段時間搭建了FastDFS用作檔案儲存,既然是檔案儲存,必然需要有檔案才能儲存。檔案可能是由客戶端傳遞上去,可以是視訊、也可以是圖片等。現在需要提供一個Android端傳遞視訊檔案的功能,一說到這,大家肯定想說,okhttp現實一個post表單就搞定了,但是post表單是需要服務端進行接收流,然後採用檔案IO方式輸出成視訊,但這次我打算使用PUT方式上傳一個檔案。

Apache搭建和配置

搭建

傳統的方式,網上應該有很多,客戶端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請求上傳視訊。

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);
}

分別說明一下三個方法的作用:

  1. postFormVideoFile(),這應該是經常使用的上傳檔案方式,Post提交表單,但需要有一個伺服器接收檔案。
  2. putFormVideoFile(),公司需要,直接採用PUT方式推送到apache上,上面已經搭建好環境了,然後我按照Post表單的形式改成Put表單形式,看上去很美好,其實這是個大坑。
  3. 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