Retrofit基本使用和原始碼解析
阿新 • • 發佈:2018-12-09
目錄介紹
- 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日