1. 程式人生 > >Retrofit基本使用和原始碼解析

Retrofit基本使用和原始碼解析

目錄介紹

  • 1.關於Retrofit基本介紹
  • 2.最簡單使用【配合Rx使用】
  • 3.註解的種類

    • 請求方法註解
    • 請求頭註解
    • 標記註解
    • 引數註解
    • 其它註解
  • 4.Retrofit相關請求引數

    • @Query()【備註:get請求/ 接上引數 】
    • @QueryMap()【備註:get請求/ 接上引數 】
    • @Path()【備註:get請求/ 替換url中某個欄位】
    • @Body()【備註:post請求/ 指定一個物件作為HTTP請求體】
    • @Field()【備註:post請求/ 用於傳送表單資料】
    • @FieldMap()【備註:post請求/ 用於傳送表單資料】
    • @Header/@Headers()【備註: 新增請求頭部 】
    • @Part()作用於方法的引數,用於定義Multipart請求的每和part
    • @PartMap()作用於方法的引數
    • 使用時注意事項
  • 5.Retrofit與RxJava結合

    • 使Rxjava與retrofit結合條件
    • 可以看到 Observable觀察者
    • 可以看到訂閱者
  • 6.OkHttpClient

    • 攔截器說明
    • 日誌攔截器
    • 請求頭攔截器
    • 統一請求攔截器
    • 快取攔截器
    • 自定義CookieJar
  • 7.踩坑經驗

    • url被轉義
  • 8.Form表單提交與multipart/form-data

    • 8.1 form表單常用屬性
    • 8.2 瀏覽器提交表單時,會執行如下步驟
    • 8.3 提交方式
    • 8.4 POST請求
    • 8.5 enctype指定的content-type
  • 9.content-type介紹

    • 9.1 application/x-www-form-urlencoded
    • 9.2 application/json
    • 9.3 text/xml
    • 9.4 multipart/form-data
  • 10.Retrofit原始碼深入分析

    • 10.1 設計模式分析[建造者模式]
    • 10.2 如何理解動態代理模式
    • 10.3 如何攔截方法,解析註解
    • 10.4 如何構建Retrofit的Call
    • 10.5 如何執行網路非同步請求enqueue方法
  • N.關於其他

    • 參考部落格
    • 版本更新說明
    • 部落格介紹

1.關於Retrofit基本介紹

  • Retrofit是Square 公司開發的一款正對Android 網路請求的框架。底層基於OkHttp 實現,OkHttp 已經得到了google 官方的認可。
  • Retrofit是由Square公司出品的針對於Android和Java的型別安全的Http客戶端,如果看原始碼會發現其實本質上是OkHttp的封裝,使用面向介面的方式進行網路請求,利用動態生成的代理類封裝了網路介面請求的底層,其將請求返回JavaBean,對網路認證REST API進行了很友好的支援。使用Retrofit將會極大的提高我們應用的網路體驗。
  • RxJava + Retrofit + okHttp組合,流行的網路請求框架

    • Retrofit 負責請求的資料和請求的結果,使用介面的方式呈現,OkHttp 負責請求的過程,RxJava 負責非同步,各種執行緒之間的切換。
    • RxJava 在 GitHub 主頁上的自我介紹是 "a library for composing asynchronous and event-based programs using observable sequences for the Java VM"(一個在 Java VM 上使用可觀測的序列來組成非同步的、基於事件的程式的庫)。這就是 RxJava ,概括得非常精準。總之就是讓非同步操作變得非常簡單。
  • 為什麼要使用Retrofit?

    • 優點

      • 請求的方法引數註解可以定製
      • 支援同步、非同步和RxJava
      • 超級解耦
      • 可以配置不同的反序列化工具來解析資料,如json、xml等
    • 其他說明

      • 在處理HTTP請求的時候,因為不同場景或者邊界情況等比較難處理。你需要考慮網路狀態,需要在請求失敗後重試,需要處理HTTPS等問題,二這些事情讓你很苦惱,而Retrofit可以將你從這些頭疼的事情中解放出來。

        • 效率高,其次Retrofit強大且配置靈活,第三和OkHttp無縫銜接,第四Jack Wharton主導的(你懂的)。

2.最簡單使用

  • Api介面
public interface DouBookApi {
    /**
    * 根據tag獲取圖書
    * @param tag  搜尋關鍵字
    * @param count 一次請求的數目 最多100
    *              https://api.douban.com/v2/book/search?tag=文學&start=0&count=30
    */
    @GET("v2/book/search")
    Observable<DouBookBean> getBook(@Query("tag") String tag,
                                    @Query("start") int start,
                                    @Query("count") int count);
}
  • Model類
public class DouBookModel {

    private static DouBookModel bookModel;
    private DouBookApi mApiService;

    public DouBookModel(Context context) {
        mApiService = RetrofitWrapper
                .getInstance(ConstantALiYunApi.API_DOUBAN)   //baseUrl地址
                .create(DouBookApi.class);
    }

    public static DouBookModel getInstance(Context context){
        if(bookModel == null) {
            bookModel = new DouBookModel(context);
        }
        return bookModel;
    }

    public Observable<DouBookBean> getHotMovie(String tag, int start , int count) {
        Observable<DouBookBean> book = mApiService.getBook(tag, start, count);
        return book;
    }
}
  • 抽取類
public class RetrofitWrapper {

    private static RetrofitWrapper instance;
    private Retrofit mRetrofit;

    public RetrofitWrapper(String url) {
        OkHttpClient.Builder builder = new OkHttpClient.Builder();

        //列印日誌
        HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
        logging.setLevel(HttpLoggingInterceptor.Level.BODY);
        builder.addInterceptor(logging).build();
        OkHttpClient client = builder.addInterceptor(new LogInterceptor("HTTP")).build();

        //解析json
        Gson gson = new GsonBuilder()
                .setLenient()
                .create();
        
        mRetrofit = new Retrofit
                .Builder()
                .baseUrl(url)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .client(client)
                .build();
    }

    public  static RetrofitWrapper getInstance(String url){
        //synchronized 避免同時呼叫多個介面,導致執行緒併發
        synchronized (RetrofitWrapper.class){
            instance = new RetrofitWrapper(url);
        }
        return instance;
    }

    public <T> T create(final Class<T> service) {
        return mRetrofit.create(service);
    }
}
  • 使用
DouBookModel model = DouBookModel.getInstance(activity);
model.getHotMovie(mType,start,count)
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Subscriber<DouBookBean>() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onNext(DouBookBean bookBean) {

            }
        });

3.註解的種類

  • 請求方法註解
@GET        get請求
@POST       post請求
@PUT        put請求
@DELETE     delete請求
@PATCH      patch請求,該請求是對put請求的補充,用於更新區域性資源
@HEAD       head請求
@OPTIONS    option請求
@HTTP       通用註解,可以替換以上所有的註解,其擁有三個屬性:method,path,hasBody
  • 請求頭註解
@Headers    用於新增固定請求頭,可以同時新增多個。通過該註解新增的請求頭不會相互覆蓋,而是共同存在
@Header     作為方法的引數傳入,用於新增不固定值的Header,該註解會更新已有的請求頭
  • 標記註解
@FormUrlEncoded    
表示請求傳送編碼表單資料,每個鍵值對需要使用@Field註解
用於修飾Fiedl註解 和FileldMap註解
使用該註解,表示請求正文將使用表單網址編碼。欄位應該宣告為引數,並用@Field 註解和 @FieldMap 註解,使用@FormUrlEncoded 註解的請求將具有"application/x-www-form-urlencoded" MIME型別。欄位名稱和值將先進行UTF-8進行編碼,再根據RFC-3986進行URI編碼。

@Multipart    
作用於方法     
表示請求傳送multipart資料,使用該註解,表示請求體是多部分的,每個部分作為一個引數,且用Part註解宣告。

@Streaming         
作用於方法
未使用@Straming 註解,預設會把資料全部載入記憶體,之後通過流獲取資料也是讀取記憶體中資料,所以返回資料較大時,需要使用該註解。
處理返回Response的方法的響應體,用於下載大檔案
提醒:如果是下載大檔案必須加上@Streaming 否則會報OOM
@Streaming
@GET
Call<ResponseBody> downloadFileWithDynamicUrlAsync(@Url String fileUrl);
  • 引數註解
引數註解:@Query 、@QueryMap、@Body、@Field、@FieldMap、@Part、@PartMap
  • 其它註解
@Path、@Url

4.Retrofit相關請求引數

  • @Query()【備註:get請求/ 接上引數 】
@Query:作用於方法引數,用於新增查詢引數,即請求引數
用於在url後拼接上引數,例如:
@GET("book/search")
Call<Book> getSearchBook(@Query("q") String name);//name由呼叫者傳入

相當於
@GET("book/search?q=name")
Call<Book> getSearchBook();
用於Get中指定引數
  • @QueryMap()【備註:get請求/ 接上引數 】
@QueryMap:作用於方法的引數。以map的形式新增查詢引數,即請求引數,引數的鍵和值都通過String.valueOf()轉換為String格式。預設map的值進行URL編碼,map中的每一項發鍵和值都不能為空,否則跑出IllegalArgumentException異常。
當然如果入參比較多,就可以把它們都放在Map中,例如:
@GET("book/search")
Call<Book> getSearchBook(@QueryMap Map<String, String> options);
  • @Path()【備註:get請求/ 替換url中某個欄位】
/**
 * http://api.zhuishushenqi.com/ranking/582ed5fc93b7e855163e707d
 * @return
 */
@GET("/ranking/{rankingId}")
Observable<SubHomeTopBean> getRanking(@Path("rankingId") String rankingId);


@GET("group/{id}/users")
Call<Book> groupList(@Path("id") int groupId);
* 像這種請求介面,在group和user之間有個不確定的id值需要傳入,就可以這種方法。我們把待定的值欄位用{}括起來,當然 {}裡的名字不一定就是id,可以任取,但需和@Path後括號裡的名字一樣。如果在user後面還需要傳入引數的話,就可以用Query拼接上,比如:
@GET("group/{id}/users")
Call<Book> groupList(@Path("id") int groupId, @Query("sort") String sort);
* 當我們呼叫這個方法時,假設我們groupId傳入1,sort傳入“2”,那麼它拼接成的url就是group/1/users?sort=2,當然最後請求的話還會加上前面的baseUrl
  • @Body()【備註:post請求/ 指定一個物件作為HTTP請求體】
使用@Body 註解定義的引數不能為null 。當你傳送一個post或put請求,但是又不想作為請求引數或表單的方式傳送請求時,使用該註解定義的引數可以直接傳入一個實體類,retrofit會通過convert把該實體序列化並將序列化的結果直接作為請求體傳送出去。

可以指定一個物件作為HTTP請求體,比如:
@POST("users/new")
Call<User> createUser(@Body User user);
它會把我們傳入的User實體類轉換為用於傳輸的HTTP請求體,進行網路請求。
多用於post請求傳送非表單資料,比如想要以post方式傳遞json格式資料
  • @Field()【備註:post請求/ 用於傳送表單資料】
用於傳送表單資料:
@FormUrlEncoded
@POST("user/edit")
Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);
注意開頭必須多加上@FormUrlEncoded這句註釋,不然會報錯。表單自然是有多組鍵值對組成,這裡的first_name就是鍵,而具體傳入的first就是值啦
多用於post請求中表單欄位,Filed和FieldMap需要FormUrlEncoded結合使用
  • @FieldMap()【備註:post請求/ 用於傳送表單資料】
@FormUrlEncoded
@POST("user/login")
Call<User> login(@FieldMap Map<String,String> map);
  • @Header/@Headers()【備註: 新增請求頭部 】
用於動態新增請求頭部:
@GET("user")
Call<User> getUser(@Header("Authorization") String authorization)

表示將頭部Authorization屬性設定為你傳入的authorization;當然你還可以用@Headers表示,作用是一樣的比如:
@Headers("Cache-Control: max-age=640000")
@GET("user")
Call<User> getUser()

當然你可以多個設定:
@Headers({
    "Accept: application/vnd.github.v3.full+json",
    "User-Agent: Retrofit-Sample-App"
})
@GET("user")
Call<User> getUser()
  • @Part()作用於方法的引數,用於定義Multipart請求的每和part
使用該註解定義的引數,引數值可以為空,為空時,則忽略。使用該註解定義的引數型別有如下3中方式可選:
1 okhttp2.MulitpartBody.Part,內容將被直接使用。省略part中的名稱,即@Part MultipartBody.Part part
2 如果型別是RequestBody,那麼該值直接與其內容型別一起使用。在註釋中提供part名稱(例如,@Part("foo") RequestBody foo)
3 其它物件型別將通過使用轉換器轉換為適當的格式。在註釋中提供part名稱(例如,@Part("foo") Image photo)。
@Multipart
@POST("/")
Call<ResponseBody> example(
       @Part("description") String description,
       @Part(value = "image", encoding = "8-bit") RequestBody image);
  • @PartMap()作用於方法的引數
以map的方式定義Multipart請求的每個part map中每一項的鍵和值都不能為空,否則丟擲IllegalArgumentException異常。
使用@PartMap 註解定義的引數型別有一下兩種:
1 如果型別是RequestBody,那麼該值將直接與其內容型別與其使用。
2 其它物件型別將通過使用轉換器轉換為適當的格式。

使用時注意事項

  • 1、Map用來組合複雜的引數,並且對於FieldMap,HeaderMap,PartMap,QueryMap這四種作用方法的註解,其引數型別必須為Map例項,且key的型別必須為String型別,否則丟擲異常。
  • 2、Query、QueryMap與Field、FieldMap功能一樣,生成的資料形式一樣;Query、QueryMap的資料體現在Url上;Field、FieldMap的資料是請求體
  • 3、{佔位符}和PATH儘量只用在URL的path部分,url的引數使用Query、QueryMap代替,保證介面的簡潔
  • 4、Query、Field、Part支援資料和實現了iterable介面的型別,如List、Set等,方便向後臺傳遞陣列,程式碼如下:
  • 5、以上部分註解真正的實現在ParameterHandler類中,每個註解的真正實現都是ParameterHandler類中的一個final型別的內部類,每個內部類都對各個註解的使用要求做了限制,比如引數是否可空、鍵和值是否可空等。
  • 6、@FormUrlEncoded 註解和@Multipart 註解不能同時使用,否則會丟擲methodError(“Only one encoding annotation is allowed.”),可在ServiceMethod類中parseMethodAnnotation()方法中找到不能同時使用的具體原因。
  • 7、@Path 與@Url 註解不能同時使用,否則會丟擲parameterError(p, "@Path parameters may not be used with @Url."),可在ServcieMethod類中parseParameterAnnotation()方法中找到不能同時使用的具體程式碼。其實原因也是很好理解:Path註解用於替換url中的引數,這就要求在使用path註解時,必須已經存在請求路徑。不然沒法替換路徑中指定的引數。而@Url 註解是在引數中指定了請求路徑的,這時候情定請求路徑已經晚,path註解找不到請求路徑,更別提更換請求路徑了中的引數了。
  • 8、使用@Body 註解的引數不能使用form 或multi-part編碼,即如果為方法使用了FormUrlEncoded或Multipart註解,則方法的引數中不能使用@Body 註解,否則會丟擲異常parameterError(p, “@Body parameters cannot be used with form or multi-part encoding.”)

5.Retrofit與RxJava結合

使Rxjava與retrofit結合條件

  • 在Retrofit物件建立的時候新增一句程式碼addCallAdapterFactory(RxJavaCallAdapterFactory.create())
完整程式碼
mRetrofit = new Retrofit
        .Builder()
        .baseUrl(url)
        .addConverterFactory(GsonConverterFactory.create(gson))
        .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
        .client(client)
        .build();
  • 可以看到 Observable觀察者
public Observable<DouBookBean> getHotMovie(String tag, int start , int count) {
    Observable<DouBookBean> book = mApiService.getBook(tag, start, count);
    return book;
}
  • 可以看到訂閱者

    • RxAndroid其實就是對RxJava的擴充套件。比如上面這個Android主執行緒在RxJava中就沒有,因此要使用的話就必須得引用RxAndroid
DouBookModel model = DouBookModel.getInstance(activity);
model.getHotMovie(mType,start,count)
        .subscribeOn(Schedulers.io())                    //請求資料的事件發生在io執行緒
        .observeOn(AndroidSchedulers.mainThread())        //請求完成後在主執行緒更顯UI
        .subscribe(new Observer<DouBookBean>() {        //訂閱
            @Override
            public void onCompleted() {
                //所有事件都完成,可以做些操作。。
            }

            @Override
            public void onError(Throwable e) {
                e.printStackTrace(); //請求過程中發生錯誤
            }

            @Override
            public void onNext(DouBookBean bookBean) {
                //這裡的book就是我們請求介面返回的實體類
            }
        });

6.OkHttpClient

  • 攔截器說明

    • addNetworkInterceptor新增的是網路攔截器Network,Interfacetor它會在request和response時分別被呼叫一次;
    • addInterceptor新增的是應用攔截器Application Interceptor他只會在response被呼叫一次。
  • 日誌攔截器

    • 一種是使用HttpLoggingInterceptor,需要使用到依賴
    compile 'com.squareup.okhttp3:logging-interceptor:3.5.0'
    
    /**
     * 建立日誌攔截器
     * @return
     */
    public static HttpLoggingInterceptor getHttpLoggingInterceptor() {
        HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
            @Override
            public void log(String message) {
                Log.e("OkHttp", "log = " + message);
            }
        });
        loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        return loggingInterceptor;
    }
  • 請求頭攔截器
/**
 * 請求頭攔截器
 * 使用addHeader()不會覆蓋之前設定的header,若使用header()則會覆蓋之前的header
 * @return
 */
public static Interceptor getRequestHeader() {
    Interceptor headerInterceptor = new Interceptor() {
        @Override
        public okhttp3.Response intercept(Chain chain) throws IOException {
            Request originalRequest = chain.request();
            Request.Builder builder = originalRequest.newBuilder();
            builder.addHeader("version", "1");
            builder.addHeader("time", System.currentTimeMillis() + "");
            Request.Builder requestBuilder = builder.method(originalRequest.method(), originalRequest.body());
            Request request = requestBuilder.build();
            return chain.proceed(request);
        }
    };
    return headerInterceptor;
}
使用addInterceptor()方法新增到OkHttpClient中
我的理解是,請求頭攔截器是為了讓服務端能更好的識別該請求,伺服器那邊通過請求頭判斷該請求是否為有效請求等...
  • 統一請求攔截器

    • 使用addInterceptor()方法新增到OkHttpClient中,統一請求攔截器的功能跟請求頭攔截器相類似
/**
 * 統一請求攔截器
 * 統一的請求引數
 */
public static Interceptor commonParamsInterceptor() {
    Interceptor commonParams = new Interceptor() {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request originRequest = chain.request();
            Request request;
            HttpUrl httpUrl = originRequest.url().newBuilder()
                    .addQueryParameter("paltform", "android")
                    .addQueryParameter("version", "1.0.0")
                    .build();
            request = originRequest.newBuilder()
                    .url(httpUrl)
                    .build();
            return chain.proceed(request);
        }
    };
    return commonParams;
}
  • 快取攔截器

    • 使用okhttp快取的話,先要建立Cache,然後在建立快取攔截器
OkHttpClient.Builder builder = new OkHttpClient.Builder();
//新增快取攔截器
//建立Cache
File httpCacheDirectory = new File("OkHttpCache");
Cache cache = new Cache(httpCacheDirectory, 10 * 1024 * 1024);
builder.cache(cache);
//設定快取
builder.addNetworkInterceptor(InterceptorUtils.getCacheInterceptor());
builder.addInterceptor(InterceptorUtils.getCacheInterceptor());
  • 快取攔截器, 快取時間自己根據情況設定
/**
 * 在無網路的情況下讀取快取,有網路的情況下根據快取的過期時間重新請求
 * @return
 */
public static Interceptor getCacheInterceptor() {
    Interceptor commonParams = new Interceptor() {
        @Override
        public okhttp3.Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            if (!NetworkUtils.isConnected()) {
                //無網路下強制使用快取,無論快取是否過期,此時該請求實際上不會被髮送出去。
                request = request.newBuilder()
                        .cacheControl(CacheControl.FORCE_CACHE)
                        .build();
            }
            Response response = chain.proceed(request);
            if (NetworkUtils.isConnected()) {
                //有網路情況下,根據請求介面的設定,配置快取。
                // 這樣在下次請求時,根據快取決定是否真正發出請求。
                String cacheControl = request.cacheControl().toString();
                //當然如果你想在有網路的情況下都直接走網路,那麼只需要
                //將其超時時間這是為0即可:String cacheControl="Cache-Control:public,max-age=0"
                int maxAge = 60 * 60;
                // read from cache for 1 minute
                return response.newBuilder()
                        .header("Cache-Control", cacheControl)
                        .header("Cache-Control", "public, max-age=" + maxAge)
                        .removeHeader("Pragma") .build();
            } else { //無網路
                int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
                return response.newBuilder()
                        .header("Cache-Control", "public,only-if-cached,max-stale=360000")
                        .header("Cache-Control", "public,only-if-cached,max-stale=" + maxStale)
                        .removeHeader("Pragma") .build();
            }
        }
    };
    return commonParams;
}
  • 自定義CookieJar
/**
 * 自定義CookieJar
 * @param builder
 */
public static void addCookie(OkHttpClient.Builder builder){
    builder.cookieJar(new CookieJar() {
        private final HashMap<HttpUrl, List<Cookie>> cookieStore = new HashMap<>();
        @Override
        public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
            cookieStore.put(url, cookies);
            //儲存cookie //也可以使用SP儲存
        }

        @Override
        public List<Cookie> loadForRequest(HttpUrl url) {
            List<Cookie> cookies = cookieStore.get(url);
            //取出cookie
            return cookies != null ? cookies : new ArrayList<Cookie>();
        }
    });
}

7.踩坑經驗

  • url被轉義
http://api.mydemo.com/api%2Fnews%2FnewsList?
罪魁禍首@Url與@Path註解,我們開發過程中,肯定會需要動態的修改請求地址
兩種動態修改方式如下:
@POST()
Call<HttpResult<News>> post(@Url String url, @QueryMap Map<String, String> map);
@POST("api/{url}/newsList")
Call<HttpResult<News>> login(@Path("url") String url, @Body News post);
第一種是直接使用@Url,它相當於直接替換了@POST()裡面的請求地址
第二種是使用@Path("url"),它只替換了@POST("api/{url}/newsList")中的{url}
如果你用下面這樣寫的話,就會出現url被轉義
@POST("{url}")
Call<HttpResult<News>> post(@Path("url") String url);
你如果執意要用@Path,也不是不可以,需要這樣寫
@POST("{url}")
Call<HttpResult<News>> post(@Path(value = "url", encoded = true) String url);

8.Form表單提交與multipart/form-data

8.1 form表單常用屬性

  • action:url 地址,伺服器接收表單資料的地址
  • method:提交伺服器的http方法,一般為post和get
  • name:最好好吃name屬性的唯一性
  • enctype: 表單資料提交時使用的編碼型別,預設使用"pplication/x-www-form-urlencoded",如果是使用POST請求,則請求頭中的content-type指定值就是該值。如果表單中有上傳檔案,編碼型別需要使用"multipart/form-data",型別,才能完成傳遞檔案資料。

8.2 瀏覽器提交表單時,會執行如下步驟

  • 識別出表單中表單元素的有效項,作為提交項
  • 構建一個表單資料集
  • 根據form表單中的enctype屬性的值作為content-type對資料進行編碼
  • 根據form表單中的action屬性和method屬性向指定的地址傳送資料

8.3 提交方式

  • get:表單資料會被encodeURIComponent後以引數的形式:name1=value1&name2=value2 附帶在url?後面,再發送給伺服器,並在url中顯示出來。
  • post:content-type 預設"application/x-www-form-urlencoded"對錶單資料進行編碼,資料以鍵值對在http請求體重發送給伺服器;如果enctype 屬性為"multipart/form-data",則以訊息的形式傳送給伺服器。

8.4 POST請求

  • HTTP/1.1 協議規定的HTTP請求方法有OPTIONS、GET、HEAD、POST、PUT、DELETE、TRACE、CONNECT 這幾種。其中POST一般用於向伺服器提交資料。
  • 大家知道,HTTP協議是以ASCII 碼傳輸,建立在TCP/IP協議之上的應用層規範。規範把HTTP請求分為3大塊:狀態行、請求頭、訊息體。類似於如下:
<method> <request-URL> <version>
<headers>
<entity-body>
  • 協議規定POST提交的資料必須放在訊息主題(entity-body)中,但協議並沒有規定資料必須使用什麼編碼方式。實際上,開發者可以自己決定訊息體的格式,只要後面傳送的HTTP請求滿足上面的格式就可以了。
  • 但是,資料傳送出去後,還要伺服器解析成功才有意義。一般伺服器都內建了自動解析常見資料格式的功能。服務端通常是根據請求頭(headers)中的Content-Type欄位來獲知請求中的訊息主體是用何種方式編碼,再對主體進行解析。所以說到POST提交資料方法,包含了Content-Type和訊息主題編碼方式兩部分。

8.5 enctype指定的content-type

  • application/x-www-form-urlencoded
  • application/json
  • text/xml
  • multipart/form-data

9.content-type介紹

9.1 application/x-www-form-urlencoded

  • 這應該是最常見的POST提交資料的方式了。瀏覽器的原生<form>表單,如果不設定enctype屬性,那麼最終會以application/x-www-form-urlencoded方法提交資料。請求類似於如下內容(省略了部分無關的內容):

    • Content-Type 被指定為 application/x-www-form-urlencoded。
    • 提交的資料按照key-value的格式,也就是key1=value1,key2=value2這種方式進行編碼,key和val都進行URL轉碼。大部分伺服器都對這種方式支援。
    POST http://www.hao123.com/ HTTP/1.1
    Content-Type: application/x-www-form-urlencoded;charset=utf-8
    title=test&sub%5B%5D=1&sub%5B%5D=2&sub%5B%5D=3

9.2 application/json

  • application/json 這個Content-Type作為響應頭大家肯定不陌生。事實上現在已經基本都是都是這種方式了,來通知伺服器訊息體是序列化後的JSON字串。由於JSON規範的流行,除了低版本的IE之外的現在主流瀏覽器都原生支援JSON。當然伺服器也有處理JSON的函式。
  • JSON格式支援比鍵值對更復雜的結構化資料,這樣點也很有用,在需要提交資料層次非常深的資料時,用JSON序列化之後提交,非常方便。
POST http://www.hao123.com/ HTTP/1.1
Content-Type: application/json;charset=utf-8
{"title":"test","sub":[1,2,3]}
  • 這種方案,可以很方便的提交複雜的結構化的資料,特別適合RESTful的介面。而且各大抓包工具如chrome自帶的開發者工具,Firebug、Fidder,都會以樹形結構展示JSON資料,非常友好。

9.3 text/xml

  • 它是一種使用HTTP作為傳輸協議,XML作為編碼方式的遠端呼叫規範。典型的XML-RPC是這樣的:

    • XML-RPC 協議很簡單、功能夠用,各種語言的實現都有。它的使用也很廣泛,但是我還是比較傾向於JSON,因為相比於JSON,XML太過於臃腫。
    POST http://www.example.com HTTP/1.1
    Content-Type: text/xml
    <?xml version="1.0"?>
    <methodCall>
        <methodName>examples.getStateName</methodName>
        <params>
            <param>
                <value><i4>41</i4></value>
            </param>
        </params>
    </methodCall>

9.4 multipart/form-data

  • 在最初的http協議中,沒有定義上傳檔案的Method, 為了實現這個功能,http協議組改造了post請求,新增一種post規範,設定這種規範的Content-Type為multipart/form-data;boundary=${bound},其中${bound}是定義分割符,用於分割各項內容(檔案,key-value對),不然伺服器無法正確識別各項內容。post body裡需要用到,儘量保證隨機唯一。
  • 這又是一個常見的POST資料提交的方式。我們使用表單上傳檔案時,必須讓form表單enctype等於multipart/form-data。
<form action="/upload" enctype="multipart/form-data" method="post">
    Username: <input type="text" name="username">
    Password: <input type="password" name="password">
    File: <input type="file" name="file">
    <input type="submit">
</form>
  • 案例如下所示

    • 這個例子稍微複雜點。首先生成了一個boundary用於分割不同的欄位,為了避免與正文內容重複,boundary很長很複雜。然後Content-Type裡指明瞭資料以multipart/form-data來編碼,本次請求的boundary是什麼內容。訊息主體裡按照欄位個數又分為多個結構型別的部分,每個部分都以---boundary開始,緊接著是內容描述資訊,然後是回車,然後是欄位的具體內容(文字和二進位制)。如果傳輸的是檔案,還要包含檔名和檔案型別資訊。訊息主體最後以----boundary----標誌結束。
    header
    Content-Type: multipart/form-data; boundary={boundary}\r\n
    
    body
    普通 input 資料
    --{boundary}\r\n
    Content-Disposition: form-data; name="username"\r\n
    \r\n
    Tom\r\n
    
    檔案上傳 input 資料
    --{boundary}\r\n
    Content-Disposition: form-data; name="file"; filename="myfile.txt"\r\n
    Content-Type: text/plain\r\n
    Content-Transfer-Encoding: binary\r\n
    \r\n
    hello word\r\n
    
    結束標誌
    --{boundary}--\r\n
    
    資料示例
    POST /upload HTTP/1.1
    Host: 172.16.100.128:5000
    Content-Length: 394
    Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryLumpDpF3AwbRwRBn
    Referer: http://172.16.100.128:5000/
    
    ------WebKitFormBoundaryUNZIuug9PIVmZWuw
    Content-Disposition: form-data; name="username"
    
    Tom
    ------WebKitFormBoundaryUNZIuug9PIVmZWuw
    Content-Disposition: form-data; name="password"
    
    passwd
    ------WebKitFormBoundaryUNZIuug9PIVmZWuw
    Content-Disposition: form-data; name="file"; filename="myfile.txt"
    Content-Type: text/plain
    
    hello world
    ------WebKitFormBoundaryUNZIuug9PIVmZWuw--

關於其他

參考部落格

版本更新說明

  • V1.0.1 更新2017年3月18日
  • V1.0.2 更新2017年5月21日
  • V1.0.3 更新2017年10月12日
  • V2.0.0 更新2018年2月23日