抽丝剥茧 okhttp3 (二)

上篇 抽丝剥茧 okhttp3 (一) https://www.jianshu.com/p/be8a204f76a3
本来这篇文打算解析okhttp中的关于http中Method 的封装的,但是看了一遍发现确实太简单值得一提。所以索性直接来剥他的Request吧。根据http协议,请求的规范如下图所示:

Request

Request盗的图.png

起始行开头的GET 或者POST表示请求访问服务器的类型,称为方法(method)。随后的字符串 /index.htm 指明了请求访问的资源对象,也叫做请求 URI(request-URI)。最后的 HTTP/1.1,即 HTTP 的版本号,用来提示客户端使用的 HTTP 协议功能。
综合来看,这段请求内容的意思是:请求访问某台 HTTP 服务器上的/index.htm 页面资源。
请求报文是由请求方法、请求 URI、协议版本、可选的请求首部字段和内容实体构成的。

request 各个部分的具体用途大家可以自行google,本文主要来看okhttp中对整个request的封装。
首先还是看这个类的注释:

/**
 * An HTTP request. Instances of this class are immutable if their {@link #body} is null or itself
 * immutable.
* 一个http请求,如果这个类的实例的body是空他就是不可变的,或者他自己就是不可变的 (什么玩意,好尴尬)
 */

大概意思是 正常情况下 他的对象实例是不可变的,想想每个http请求肯定也是一次性的,不存在被改变的情形,嗯,应该是这样的我猜的,有懂的请指教下 ,谢谢,,,Request没有父类也没有子类也没有包装类也没实现接口,所以全局的okhttp的request实现就此一家。

纵观这个类,也是采用建造者模式进行构建的,来看builder


  public static class Builder {
    HttpUrl url;
    String method;
    Headers.Builder headers;
    RequestBody body;
    Object tag;

    public Builder() {
      this.method = "GET";
      this.headers = new Headers.Builder();
    }

    Builder(Request request) {
      this.url = request.url;
      this.method = request.method;
      this.body = request.body;
      this.tag = request.tag;
      this.headers = request.headers.newBuilder();
    }
}

从成员变量和构造方法我们可以知道,builder 主要就是把http协议规定的所有元素通过传入进来。

  • HttpUrl url; 前文解析过 先进标准的现代url对象
  • String method; GET POST PUT DELETE 等等 而且默认是GET
  • Headers.Builder headers; 头部字段集合
  • RequestBody body; body 内容实体
  • Object tag; 不属于http协议,用于给每个请求打标记,可以取消获取请求实例
    其他方法中都是普通的类set get的方法,值得注意的是在设置url的时候
public Builder url(String url) {
      if (url == null) throw new NullPointerException("url == null");

      // Silently replace web socket URLs with HTTP URLs.
      if (url.regionMatches(true, 0, "ws:", 0, 3)) {
        url = "http:" + url.substring(3);
      } else if (url.regionMatches(true, 0, "wss:", 0, 4)) {
        url = "https:" + url.substring(4);
      }

      HttpUrl parsed = HttpUrl.parse(url);
      if (parsed == null) throw new IllegalArgumentException("unexpected url: " + url);
      return url(parsed);
    }

这里自动把ws 和wss 这两种websocket协议自动转成了http,大家可以讨论下这里的原因 ,和用意。

Header

除去HttpUrl和Method 我们简单看一下header部分。Headers类也很简单,是个final 类 ,说明也是既没父类也无子类,独一家。
此类中同样采用了构建者模式。
同样Headers类的内容也是没有提供修改方法,所以也是不可修改的,内部用一个数组存放key value 奇数位存key 偶数位存value。

RequestBody

RequestBody是这部分的重点角色,因为网络编程中很多的交互都通过body来进行,比如POST 中的表单,json数据上传,上传文件等等,都少不了body的身影。注意,get请求是没有body的

首先我们看RequestBody是个抽象类,并且有两个子类实现。从字面上可以看出子类分别代表post请求的两类body,form表单的body和multypart的多部分上传(多部分上传又分为几种,比如文件上传,json,文本等多种上传)


image.png

目前网上没有找到好的关于介绍http协议的文章,等遇到的时候贴出来吧。好的关于RequestBody的家族就这么大,下面来一一攻破。

首先来看他们的父类RequestBody。看他的结构:


RequestBody类结构图

看来很简单,封装了
contentType 对应http请求头中的contentType
contentLength 对应http请求头中的contentLength

writeTo(BufferedSink sink) 把数据写入okio提供的缓冲池中。

余下的是五个重载的创建RequestBody对象的create方法。
五个方法 最终都是 返回一个new 出来的RequestBody对象,并实现了相关方法:
比如:普通的请求


  /** Returns a new request body that transmits {@code content}. */
  public static RequestBody create(
      final @Nullable MediaType contentType, final ByteString content) {
    return new RequestBody() {
      @Override public @Nullable MediaType contentType() {
        return contentType;
      }

      @Override public long contentLength() throws IOException {
        return content.size();
      }

      @Override public void writeTo(BufferedSink sink) throws IOException {
        sink.write(content);
      }
    };
  }

上传文件的请求

/** Returns a new request body that transmits the content of {@code file}. */
  public static RequestBody create(final @Nullable MediaType contentType, final File file) {
    if (file == null) throw new NullPointerException("content == null");

    return new RequestBody() {
      @Override public @Nullable MediaType contentType() {
        return contentType;
      }

      @Override public long contentLength() {
        return file.length();
      }

      @Override public void writeTo(BufferedSink sink) throws IOException {
        Source source = null;
        try {
          source = Okio.source(file);
          sink.writeAll(source);
        } finally {
          Util.closeQuietly(source);
        }
      }
    };
  }

还是比较简单的。
接下来是FormBody 类:
首先我们看到他的contentType是固定的,即:

private static final MediaType CONTENT_TYPE =
      MediaType.parse("application/x-www-form-urlencoded");

这就是我们常见的post 提交上传表单的格式。所以这个类叫formbody;总体来看这特么又有个builder内部类。玩建造者玩的不亦乐乎,所以以后我们封装些什么或者做一类库的时候builder的使用可以多多借鉴这些开源项目。
来看这个类比父类多了什么。


FormBody结构图.png

除了builder类,主类中多了两组List 分别存放post的表单中的key 和value 并且一一对应的,(这样使用比Map的k-v形式性能更高)。并且在biulder中提供了add方法来添加参数,查看下面源码,值得我们注意的是当我们向formbody中添加参数时,如果add的时候key多次传入同一值时,后者并不会覆盖前者,而是继续追加,并传给服务端。

public Builder add(String name, String value) {
      if (name == null) throw new NullPointerException("name == null");
      if (value == null) throw new NullPointerException("value == null");

      names.add(HttpUrl.canonicalize(name, FORM_ENCODE_SET, false, false, true, true, charset));
      values.add(HttpUrl.canonicalize(value, FORM_ENCODE_SET, false, false, true, true, charset));
      return this;
    }

    public Builder addEncoded(String name, String value) {
      if (name == null) throw new NullPointerException("name == null");
      if (value == null) throw new NullPointerException("value == null");

      names.add(HttpUrl.canonicalize(name, FORM_ENCODE_SET, true, false, true, true, charset));
      values.add(HttpUrl.canonicalize(value, FORM_ENCODE_SET, true, false, true, true, charset));
      return this;
    }

拼接参数的逻辑在这里

 private long writeOrCountBytes(@Nullable 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;
  }

FormBody 到这里就结束了 ,实际上也很简单。MultipartBody 相对来说比较复杂,但不算难。对于multipart的请求体 我们用的最多的是multipart/form-data类型的,多用于上传文件。其他类型用的很少我懂的也是皮毛所以一展开。关于multipart/form-data 请求可以读下此文章:https://blog.csdn.net/five3/article/details/7181521

基于http的要求
MultipartBody封装了除了用烂了的builder外还有代表每个部分的Part类,builder用来组装这些分部分。

MultipartBody结构.png
Builder类结构

每个Part都有自己的一套header 和body

 final @Nullable Headers headers;
    final RequestBody body;

    private Part(@Nullable Headers headers, RequestBody body) {
      this.headers = headers;
      this.body = body;
    }

    public @Nullable Headers headers() {
      return headers;
    }

    public RequestBody body() {
      return body;
    }

MultipartBody中各个成员都来自于builder的构建。


  private final ByteString boundary;
  private final MediaType originalType;
  private final MediaType contentType;
  private final List<Part> parts;
  private long contentLength = -1L;

  MultipartBody(ByteString boundary, MediaType type, List<Part> parts) {
    this.boundary = boundary;
    this.originalType = type;
    this.contentType = MediaType.parse(type + "; boundary=" + boundary.utf8());
    this.parts = Util.immutableList(parts);
  }

同样在writeOrCountBytes方法中完成最终实体数据的拼接并写入缓冲池。

private long writeOrCountBytes(
      @Nullable BufferedSink sink, boolean countBytes) throws IOException {
    long byteCount = 0L;

    Buffer byteCountBuffer = null;
    if (countBytes) {
      sink = byteCountBuffer = new Buffer();
    }

    for (int p = 0, partCount = parts.size(); p < partCount; p++) {
//此层循环把每个part按照http的协议拼接好
      Part part = parts.get(p);
      Headers headers = part.headers;
      RequestBody body = part.body;

      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);
        }
      }

      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.
        byteCountBuffer.clear();
        return -1L;
      }

      sink.write(CRLF);

      if (countBytes) {
        byteCount += contentLength;
      } else {
        body.writeTo(sink);
      }

      sink.write(CRLF);
    }

    sink.write(DASHDASH);
    sink.write(boundary);
    sink.write(DASHDASH);
    sink.write(CRLF);

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

    return byteCount;
  }

至此,body完结,写的也不好,不足的东西以后慢慢补上,学习和分享的过程应该如此,一边分享一边学习共同进步和成长。

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,573评论 18 139
  • 最近难得有时间,可以看看平时经常用的牛逼的三方框架是怎么实现的,学习学习。比如okhttp ,眼下安卓开发 网络框...
    张哲1111阅读 1,228评论 0 1
  • 6.1 公钥密钥加密原理 6.1.1 基础知识 密钥:一般就是一个字符串或数字,在加密或者解密时传递给加密/解密算...
    AndroidMaster阅读 3,998评论 1 8
  • 一、简介 Retrofit是Square公司开发的一款针对Android网络请求的框架,Retrofit2底层基于...
    Devil不加V阅读 537评论 0 0
  • 最近我也睡懒觉 不是我爱 是你爱 我怕一觉醒来 陷入思念的自己
    林百错阅读 205评论 0 3