• OkHttp3源码详解(一) Request类


    每一次网络请求都是一个Request,Request是对url,method,header,body的封装,也是对Http协议中请求行,请求头,实体内容的封装

    1 public final class Request {
    2   private final HttpUrl url;
    3   private final String method;
    4   private final Headers headers;
    5   private final RequestBody body;
    6   private final Object tag;
    7 
    8   private volatile CacheControl cacheControl; // Lazily initialized.

    1.HttpUrl

    HttpUrl主要用来规范普通的url连接,并且解析url的组成部分

    现通过下面的例子来示例httpUrl的使用
    https://www.google.com/search?q=maplejaw
    ①使用parse解析url字符串:

    HttpUrl url = HttpUrl.parse("https://www.google.com/search?q=maplejaw");

    ②通过构造者模式来常见:

    1 HttpUrl url = new HttpUrl.Builder()
    2         .scheme("https")
    3         .host("www.google.com")
    4         .addPathSegment("search")
    5         .addQueryParameter("q", "maplejaw")
    6         .build();

    2.Headers

    Headers用于配置请求头,对于请求头配置大家一定不陌生吧,比如Content-Type,User-Agent和Cache-Control等等。

    创建Headers也有两种方式。如下:
    (1)of()创建:传入的数组必须是偶数对,否则会抛出异常。

    Headers.of("name1","value1","name2","value2",.....);

    还可以使用它的重载方法of(Map<String,String> map)方法来创建

    (2)构建者模式创建:

    Headers mHeaders=new Headers.Builder()
                .set("name1","value1")//set表示name1是唯一的,会覆盖掉已经存在的
                .add("name2","value2")//add不会覆盖已经存在的头,可以存在多个
                .build();

    我们来看一下Header的内部,源码就不粘贴了很简单,Headers内部是通过一个数组来保存header private final String[] namesAndValues;大家可能会有这样的疑问,为什么不用Map而用数组呢?因为Map的Key是唯一的,而header要求不唯一

    另外,数组方便取数据吗?很方便,我们来看着三个方法

    最后通过toString方法转变成String,方便写入请求头,

     1 @Override 
     2 public String toString() {
     3     StringBuilder result = new StringBuilder();
     4     for (int i = 0, size = size(); i < size; i++) {
     5       result.append(name(i)).append(": ").append(value(i)).append("
    ");
     6     }
     7     return result.toString();
     8 }
     9 
    10 /** Returns the field at {@code position} or null if that is out of range. */
    11 public String name(int index) {
    12     int nameIndex = index * 2;
    13     if (nameIndex < 0 || nameIndex >= namesAndValues.length) {
    14       return null;
    15     }
    16     return namesAndValues[nameIndex];
    17 }
    18  
    19 /** Returns the value at {@code index} or null if that is out of range. */
    20 public String value(int index) {
    21     int valueIndex = index * 2 + 1;
    22     if (valueIndex < 0 || valueIndex >= namesAndValues.length) {
    23       return null;
    24     }
    25     return namesAndValues[valueIndex];
    26   }

    3.RequestBody

     requestBody也就是请求实体内容,我们先来看一下如何来构建一个RequestBody

    (1)Request.create()方法创建

     1 public static final MediaType TEXT = MediaType.parse("text/plain; charset=utf-8");
     2 public static final MediaType STREAM = MediaType.parse("application/octet-stream");
     3 public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
     4 
     5 //构建字符串请求体
     6 RequestBody body1 = RequestBody.create(TEXT, string);
     7 //构建字节请求体
     8 RequestBody body2 = RequestBody.create(STREAM, byte);
     9 //构建文件请求体
    10 RequestBody body3 = RequestBody.create(STREAM, file);
    11 //post上传json
    12 RequestBody body4 = RequestBody.create(JSON, json);//json为String类型的
    13 
    14 //将请求体设置给请求方法内
    15 Request request = new Request.Builder()
    16 .url(url)
    17 .post(xx)// xx表示body1,body2,body3,body4中的某一个
    18 .build();
    19  
    View Code

    (2)构建表单请求体,提交键值对(OkHttp3没有FormEncodingBuilder这个类,替代它的是功能更加强大的FormBody:)

     //构建表单RequestBody
    RequestBody formBody=new FormBody.Builder()
                     .add("name","maplejaw")
                     .add("age","18")
                      ...     
                     .build();

     (3)构建分块表单请求体:(OkHttp3取消了MultipartBuilder,取而代之的是MultipartBody.Builder()
                      既可以添加表单,又可以也可以添加文件等二进制数据)

     1 public static final MediaType STREAM = MediaType.parse("application/octet-stream");
     2  //构建表单RequestBody
     3 RequestBody multipartBody=new MultipartBody.Builder()
     4                 .setType(MultipartBody.FORM)//指明为 multipart/form-data 类型
     5                 .addFormDataPart("age","20") //添加表单数据
     6                 .addFormDataPart("avatar","111.jpg",RequestBody.create(STREAM,file)) //添加文件,其中avatar为表单名,111.jpg为文件名。
     7                 .addPart(..)//该方法用于添加RequestBody,Headers和添加自定义Part,一般来说以上已经够用
     8                 .build();
     9  
    10  

    知道了RequestBody的创建,我们来看一下源码
    RequestBody也就是请求实体内容,对于一个Get请求时没有实体内容的,Post提交才有,而且浏览器与服务器通信时基本上只有表单上传才会用到POST提交,所以RequestBody其实也就是封装了浏览器表单上传时对应的实体内容,对于实体内容是什么样还不清楚的可以去看一下我的一篇博客Android的Http协议的通信详解

    OkHttp3中RequestBody有三种创建方式

     ①方式一:

     public static RequestBody create(MediaType contentType, String content) {
        Charset charset = Util.UTF_8;
        if (contentType != null) {
          charset = contentType.charset();//MediaType的为请求头中的ContentType创建方式:public static final MediaType TEXT = 
                                          //MediaType.parse("text/plain; charset=utf-8")
          if (charset == null) {
            charset = Util.UTF_8;<span style="font-family:Microsoft YaHei;">//如果contentType中没有指定charset,默认使用UTF-8</span>
            contentType = MediaType.parse(contentType + "; charset=utf-8");
          }
        }
        byte[] bytes = content.getBytes(charset);
        return create(contentType, bytes);
      }
    View Code

    ②方式二:FormBody表单创建,我们来看一下

    FormBody用于普通post表单上传键值对,我们先来看一下创建的方法,再看源码

    1  RequestBody formBody=new FormBody.Builder()
    2                  .add("name","maplejaw")
    3                  .add("age","18")
    4                   ...     
    5                  .build();

    FormBody源码

     1 public final class FormBody extends RequestBody {
     2   private static final MediaType CONTENT_TYPE =
     3       MediaType.parse("application/x-www-form-urlencoded");
     4 
     5   private final List<String> encodedNames;
     6   private final List<String> encodedValues;
     7 
     8   private FormBody(List<String> encodedNames, List<String> encodedValues) {
     9     this.encodedNames = Util.immutableList(encodedNames);
    10     this.encodedValues = Util.immutableList(encodedValues);
    11   }
    12 
    13   /** The number of key-value pairs in this form-encoded body. */
    14   public int size() {
    15     return encodedNames.size();
    16   }
    17 
    18   public String encodedName(int index) {
    19     return encodedNames.get(index);
    20   }
    21 
    22   public String name(int index) {
    23     return percentDecode(encodedName(index), true);
    24   }
    25 
    26   public String encodedValue(int index) {
    27     return encodedValues.get(index);
    28   }
    29 
    30   public String value(int index) {
    31     return percentDecode(encodedValue(index), true);
    32   }
    33 
    34   @Override public MediaType contentType() {
    35     return CONTENT_TYPE;
    36   }
    37 
    38   @Override public long contentLength() {
    39     return writeOrCountBytes(null, true);
    40   }
    41 
    42   @Override public void writeTo(BufferedSink sink) throws IOException {
    43     writeOrCountBytes(sink, false);
    44   }
    45 
    46   /**
    47    * Either writes this request to {@code sink} or measures its content length. We have one method
    48    * do double-duty to make sure the counting and content are consistent, particularly when it comes
    49    * to awkward operations like measuring the encoded length of header strings, or the
    50    * length-in-digits of an encoded integer.
    51    */
    52   private long writeOrCountBytes(BufferedSink sink, boolean countBytes) {
    53     long byteCount = 0L;
    54 
    55     Buffer buffer;
    56     if (countBytes) {
    57       buffer = new Buffer();
    58     } else {
    59       buffer = sink.buffer();
    60     }
    61 
    62     for (int i = 0, size = encodedNames.size(); i < size; i++) {
    63       if (i > 0) buffer.writeByte('&');
    64       buffer.writeUtf8(encodedNames.get(i));
    65       buffer.writeByte('=');
    66       buffer.writeUtf8(encodedValues.get(i));
    67     }
    68 
    69     if (countBytes) {
    70       byteCount = buffer.size();
    71       buffer.clear();
    72     }
    73 
    74     return byteCount;
    75   }
    76 
    77   public static final class Builder {
    78     private final List<String> names = new ArrayList<>();
    79     private final List<String> values = new ArrayList<>();
    80 
    81     public Builder add(String name, String value) {
    82       names.add(HttpUrl.canonicalize(name, FORM_ENCODE_SET, false, false, true, true));
    83       values.add(HttpUrl.canonicalize(value, FORM_ENCODE_SET, false, false, true, true));
    84       return this;
    85     }
    86 
    87     public Builder addEncoded(String name, String value) {
    88       names.add(HttpUrl.canonicalize(name, FORM_ENCODE_SET, true, false, true, true));
    89       values.add(HttpUrl.canonicalize(value, FORM_ENCODE_SET, true, false, true, true));
    90       return this;
    91     }
    92 
    93     public FormBody build() {
    94       return new FormBody(names, values);
    95     }
    96   }
    97 }
    View Code

    我们主要来看一下方法writeOrCountBytes,通过writeOrCountBytes来计算请求体大小和将请求体写入BufferedSink。

    至于BufferSink和Buffer类,这两个类是Okio中的类,Buffer相当于一个缓存区,BufferedSink相当于OutputStream,它扩展了

    OutputStream的功能,Okio的完整源码我后续也会写博客

    ③方式三:MultipartBody分块表单创建

    MultipartBody, 既可以添加表单,又可以也可以添加文件等二进制数据,我们就看几个重要的方法

     public static Part createFormData(String name, String filename, RequestBody body) {
          if (name == null) {
            throw new NullPointerException("name == null");
          }
          StringBuilder disposition = new StringBuilder("form-data; name=");
          appendQuotedString(disposition, name);
     
          if (filename != null) {
            disposition.append("; filename=");
            appendQuotedString(disposition, filename);
          }
     
          return create(Headers.of("Content-Disposition", disposition.toString()), body);
        }
    View Code

    我们来看这个方法,我们是addPart还是addFormDataPart最终都走到了这个方法,封装成一个Part对象,也就是实体内容中

    的Content-Disposition跟文件二进制流或者键值对的值

    MultipartBody和FormBody大体上相同,主要区别在于writeOrCountBytes方法,分块表单主要是将每个块的大小进行累加来求出请求体大小,如果其中有一个块没有指定大小,就会返回-1。所以分块表单中如果包含文件,默认是无法计算出大小的,除非你自己给文件的RequestBody指定contentLength。

      private long writeOrCountBytes(BufferedSink sink, boolean countBytes) throws IOException {
        long byteCount = 0L;
     
        Buffer byteCountBuffer = null;
        if (countBytes) {
          //如果是计算大小的话,就new个
          sink = byteCountBuffer = new Buffer();
        }
        //循环块
        for (int p = 0, partCount = parts.size(); p < partCount; p++) {
          Part part = parts.get(p);
          //获取每个块的头
          Headers headers = part.headers;
          //获取每个块的请求体
          RequestBody body = part.body;
     
          //写 --xxxxxxxxxx 边界     
          sink.write(DASHDASH);
          sink.write(boundary);
          sink.write(CRLF);
     
          //写块的头
          if (headers != null) {
            for (int h = 0, headerCount = headers.size(); h < headerCount; h++) {
              sink.writeUtf8(headers.name(h))
                  .write(COLONSPACE)
                  .writeUtf8(headers.value(h))
                  .write(CRLF);
            }
          }
     
          //写块的Content_Type
          MediaType contentType = body.contentType();
          if (contentType != null) {
            sink.writeUtf8("Content-Type: ")
                .writeUtf8(contentType.toString())
                .write(CRLF);
          }
     
          //写块的大小
          long contentLength = body.contentLength();
          if (contentLength != -1) {
            sink.writeUtf8("Content-Length: ")
                .writeDecimalLong(contentLength)
                .write(CRLF);
          } else if (countBytes) {
            // We can't measure the body's size without the sizes of its components.
            //如果有个块没有这名大小,就返回-1.
            byteCountBuffer.clear();
            return -1L;
          }
     
          sink.write(CRLF);
     
          //如果是计算大小就累加,否则写入BufferedSink
          if (countBytes) {
            byteCount += contentLength;
          } else {
            body.writeTo(sink);
          }
     
          sink.write(CRLF);
        }
     
     //写 --xxxxxxxxxx-- 结束边界
        sink.write(DASHDASH);
        sink.write(boundary);
        sink.write(DASHDASH);
        sink.write(CRLF);
     
     
        if (countBytes) {
          byteCount += byteCountBuffer.size();
          byteCountBuffer.clear();
        }
     
        return byteCount;
      }
    View Code

    4.CacheControl

    ( 1)  Cache-Control:

    Cache-Control指定请求和响应遵循的缓存机制。在请求消息或响应消息中设置Cache-Control并不会修改另一个消息处理过程中的缓存处理过程。请求时的缓存指令有下几种:

    1. Public:所有内容都将被缓存(客户端和代理服务器都可缓存)。
    2. Private:内容只缓存到私有缓存中(仅客户端可以缓存,代理服务器不可缓存)
    3. no-cache:请求或者响应消息不能缓存
    4. no-store:不使用缓存,也不存储缓存
    5. max-age:缓存的内容将在指定时间(秒)后失效, 这个选项只在HTTP 1.1可用, 并如果和Last-Modified一起使用时, 优先级较高
    6. 在 xxx 秒后,浏览器重新发送请求到服务器,指定时间(秒)内,客户端会直接返回cache而不会发起网络请求,若过期会自动发起网络请求
    7. min-fresh:指示客户端可以接收响应时间小于当前时间加上指定时间的响应。
    8. max-stale:指示客户端可以接收超出超时期间的响应消息。如果指定max-stale消息的值,那么客户机可以接收超出超时期指定值之内的响应消息。

    (2)CacheControl类

    ①常用的函数

     final CacheControl.Builder builder = new CacheControl.Builder();
                builder.noCache();//不使用缓存,全部走网络
                builder.noStore();//不使用缓存,也不存储缓存
                builder.onlyIfCached();//只使用缓存
                builder.noTransform();//禁止转码
                builder.maxAge(10, TimeUnit.MILLISECONDS);//指示客户机可以接收生存期不大于指定时间的响应。
                builder.maxStale(10, TimeUnit.SECONDS);//指示客户机可以接收超出超时期间的响应消息
                builder.minFresh(10, TimeUnit.SECONDS);//指示客户机可以接收响应时间小于当前时间加上指定时间的响应。
                CacheControl cache = builder.build();//cacheControl
    View Code

    ②CacheControl的两个常量:

     public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();//不使用缓存
      public static final CacheControl FORCE_CACHE = new Builder()
          .onlyIfCached()
          .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
          .build();//只使用缓存
    View Code

    ③请求时如何使用:

    final CacheControl.Builder builder = new CacheControl.Builder();
                builder.maxAge(10, TimeUnit.MILLISECONDS);
                CacheControl cache = builder.build();
                final Request request = new Request.Builder().cacheControl(cache).url(requestUrl).build();
                final Call call = mOkHttpClient.newCall(request);//
                call.enqueue(new Callback() {
                    @Override
                    public void onFailure(Call call, IOException e) {
                        failedCallBack("访问失败", callBack);
                        Log.e(TAG, e.toString());
                    }
     
                    @Override
                    public void onResponse(Call call, Response response) throws IOException {
                        if (response.isSuccessful()) {
                            String string = response.body().string();
                            Log.e(TAG, "response ----->" + string);
                            successCallBack((T) string, callBack);
                        } else {
                            failedCallBack("服务器错误", callBack);
                        }
                    }
                });
                return call;
            } catch (Exception e) {
                Log.e(TAG, e.toString());
            }
    View Code

       以上如果Cache没有过期会直接返回cache而不会去发起网络请求,若过期自动发起网络请求,注意:如果您使用FORCE_CACHE和网络的响应需求,OkHttp则会返回一个504提示,告诉你不可满足请求响应,所以我们加一个判断在没有网络的情况下使用

         //判断网络是否连接
            boolean connected = NetworkUtil.isConnected(context);
             if (!connected) {
                 request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build();
              }
  • 相关阅读:
    RedHat5.8 编译内核驱动 合成initrd.img
    matrix-gui-2.0 将javascript文件夹改成js文件夹
    使用PHP配置文件
    Ubuntu 16.10 Apache PHP Server
    Ubuntu 16.10 中文环境 Shell输出英文提示
    制作SD卡img文件,并扩容
    Linux syslogd
    Windows cmd findstr
    jquery ztree异步搜索
    怎样在点击li时添加样式,移除兄弟样式
  • 原文地址:https://www.cnblogs.com/ganchuanpu/p/6014456.html
Copyright © 2020-2023  润新知