OkHttp3源码详解(一Request类)


           上次在哪篇博客看到这句话,我们不重复造轮子不表示我们不需要知道轮子该怎么造及如何更好的造,我觉得很有道理

对于用到项目的某个开源项目我们不应该只停留在会用的阶段,废话不多说,直接开始okhttp源码的学习之路

        从上至下我们先从Request类开始阅读

       

       一.Request

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

         

       
      Request request = new Request
                        .Builder()
                        .url(url)
                        .post(body)
                        .addHeader("Accept","*/*")
                        .cacheContro()
                       .build();

 通常我们通过构建折模式来构建一个Request,对于构建者在OkHttp的源码中到处都可以看到,大家可以自己去翻一翻,我们来看一下Request里的代码,Request主要有如下属性

 

  private final HttpUrl url;//请求url封装
  private final String method;//请求方法
  private final Headers headers;//请求头
  private final RequestBody body;//请求体,也就是http协议的实体内容
  private final Object tag;//被请求的标签

  private volatile URL javaNetUrl; // Lazily initialized.
  private volatile URI javaNetUri; // Lazily initialized.
  private volatile CacheControl cacheControl; // 缓存控制的封装

  1.HttpUrl

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

我们来看一下url的构成

scheme://username:password@host:port/pathSegment/pathSegment?queryParameter#fragment;

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

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

通过构造者模式来常见:

HttpUrl url = new HttpUrl.Builder()
        .scheme("https")
        .host("www.google.com")
        .addPathSegment("search")
        .addQueryParameter("q", "maplejaw")
        .build();

   

2.Headers

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

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

Headers.of("name1","value1","name2","value2",.....);
  还可以使用它的重载方法of(Map 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要求不唯一

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

/** Returns the field at {@code position} or null if that is out of range. */
  public String name(int index) {
    int nameIndex = index * 2;
    if (nameIndex < 0 || nameIndex >= namesAndValues.length) {
      return null;
    }
    return namesAndValues[nameIndex];
  }

  /** Returns the value at {@code index} or null if that is out of range. */
  public String value(int index) {
    int valueIndex = index * 2 + 1;
    if (valueIndex < 0 || valueIndex >= namesAndValues.length) {
      return null;
    }
    return namesAndValues[valueIndex];
  }

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

 @Override public String toString() {
    StringBuilder result = new StringBuilder();
    for (int i = 0, size = size(); i < size; i++) {
      result.append(name(i)).append(": ").append(value(i)).append("\n");
    }
    return result.toString();
  }

2.CacheControl

    Cache-Control对应请求头中Cache-Control中的值,我们先来看一下Http协议中Cache-Control

   ( 1)  Cache-Control:

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

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

     

     ②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();//只使用缓存

     ③请求时如何使用:

  

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());
        }
 
     以上如果Cache没有过去会直接返回cache而不会去发起网络请求,若过期自动发起网络请求,注意:如果您使用FORCE_CACHE和网络的响应需求,OkHttp则会返回一个504提示,告诉你不可满足请求响应,所以我们加一个判断在没有网络的情况下使用
      
     //判断网络是否连接
        boolean connected = NetworkUtil.isConnected(context);
         if (!connected) {
             request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build();
          }
     以上这些就是CacheControl类的学习,源码没必要看了,虽然很长但是比较简单,也就是通过方法来选择使用哪种缓存模式而已


3.RequestBody

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

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

  public static final MediaType TEXT = MediaType.parse("text/plain; charset=utf-8");
     public static final MediaType STREAM = MediaType.parse("application/octet-stream");
     public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");

     //构建字符串请求体
     RequestBody body1 = RequestBody.create(TEXT, string);
     //构建字节请求体
     RequestBody body2 = RequestBody.create(STREAM, byte);
     //构建文件请求体
     RequestBody body3 = RequestBody.create(STREAM, file);
     //post上传json
     RequestBody body4 = RequestBody.create(JSON, json);//json为String类型的

     //将请求体设置给请求方法内
     Request request = new Request.Builder()
       .url(url)
       .post(xx)// xx表示body1,body2,body3,body4中的某一个
       .build();


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

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

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

  

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



   知道了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;//如果contentType中没有指定charset,默认使用UTF-8
        contentType = MediaType.parse(contentType + "; charset=utf-8");
      }
    }
    byte[] bytes = content.getBytes(charset);
    return create(contentType, bytes);
  }
   最终会调用下面的方法

  

 /** Returns a new request body that transmits {@code content}. */
  public static RequestBody create(final MediaType contentType, final byte[] content,
      final int offset, final int byteCount) {
    if (content == null) throw new NullPointerException("content == null");
    Util.checkOffsetAndCount(content.length, offset, byteCount);
    return new RequestBody() {
      @Override public MediaType contentType() {
        return contentType;
      }

      @Override public long contentLength() {
        return byteCount;
      }

      @Override public void writeTo(BufferedSink sink) throws IOException {
        sink.write(content, offset, byteCount);
      }
    };
  }
   ②方式二:FormBody表单创建,我们来看一下

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

  

 RequestBody formBody=new FormBody.Builder()
                 .add("name","maplejaw")
                 .add("age","18")
                  ...     
                 .build();

  也就是简单的键值对,通过构建者模式得到FormBody对象,我们来看一下源码z

 

 private static final MediaType CONTENT_TYPE =
  MediaType.parse("application/x-www-form-urlencoded");//ContentType,请求头中

  private final List encodedNames;
  private final List encodedValues;

  private FormBody(List encodedNames, List encodedValues) {
    this.encodedNames = Util.immutableList(encodedNames);
    this.encodedValues = Util.immutableList(encodedValues);
  }

  /** The number of key-value pairs in this form-encoded body. */
  public int size() {
    return encodedNames.size();
  }

  public String encodedName(int index) {
    return encodedNames.get(index);
  }

  public String name(int index) {
    return percentDecode(encodedName(index), true);
  }

  public String encodedValue(int index) {
    return encodedValues.get(index);
  }

  public String value(int index) {
    return percentDecode(encodedValue(index), true);
  }

  @Override public MediaType contentType() {
    return CONTENT_TYPE;
  }

  @Override public long contentLength() {
    return writeOrCountBytes(null, true);
  }

  @Override public void writeTo(BufferedSink sink) throws IOException {
    writeOrCountBytes(sink, false);
  }
   
  private long writeOrCountBytes(BufferedSink sink, boolean countBytes) {
    long byteCount = 0L;

    Buffer buffer;
    if (countBytes) {
      buffer = new Buffer();
    } else {
      buffer = sink.buffer();
    }

    for (int i = 0, size = encodedNames.size(); i < size; i++) {
      if (i > 0) buffer.writeByte('&');
      buffer.writeUtf8(encodedNames.get(i));
      buffer.writeByte('=');
      buffer.writeUtf8(encodedValues.get(i));
    }

    if (countBytes) {
      byteCount = buffer.size();
      buffer.clear();
    }

    return byteCount;
  }
   我们主要来看一下方法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);
    }

  我们来看这个方法,我们是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;
  }

  好了OkHttp Request类的源码也差不读就这么多,比较简单,从简单的来嘛从浅到深,后面的博客将继续深入的学习OkHttp的源码,我这个人比较较真喜欢追究看源码弄懂到底是这么回事,这两天把大学里面的计算机网络的书又翻出来看了看,以前在学习的时候还是基础没打牢啊
 

 


你可能感兴趣的:(网络编程,android进阶)