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日
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章