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.关于其他
    • 参考博客
    • 版本更新说明
    • 博客介绍

好消息

  • 博客笔记大汇总【16年3月到至今】,包括Java基础及深入知识点,Android技术博客,Python学习笔记等等,还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!同时也开源了生活博客,从12年起,积累共计47篇[近20万字],转载请注明出处,谢谢!
  • 链接地址:https://github.com/yangchong211/YCBlogs
  • 如果觉得好,可以star一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!

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日
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容