Android网络编程-深入理解OkHttp


OkHttp是当前Android开发中最常用的网络请求框架,也是每一个Android开发人员必须掌握的知识点。在刚学习Android的时候,我在郭老师的《第一行代码》中学过OkHttp的基本用法,但对其实现原理和设计思想没有做深入了解,而且也没有在实际项目中使用过。最近正好工作上要重构一个网络请求的apk,为了使重构的代码更加高效稳定,我决定从对OkHttp进行深入的学习,同时阅读其源代码,争取在开发时能够“胸有成竹,一气呵成”。好了,下面我们就从一些计算机网络的基本知识说起吧。

从网络编程基本概念说起

计算机网络模型

上网早已成为现代人生活中的平常事物,且网络传输速度也是越来越快。而网络请求过程中各个部件以什么样的规则通信就是网络模型所研究的内容,今天我们提到网络模型一般是指OSI七层参考模型和TCP/IP四层参考模型。

OSI(Open System Interconnection)七层参考模型,是国际标准化组织(ISO)制定的一个用于计算机或通讯系统间互联的标准体系。它是一个七层的、抽象的模型体,不仅包括一系列抽象的术语或概念,也包括具体的协议。
顾名思义,OSI模型共有七层,包括:1-物理层2-数据链路层3-网络层4-传输层5-会话层6-表示层 7-应用层,这里借用网络上一张经典图片,基本可以总结这七层模型的功能和关系:
OSI七层参考模型
下面例举了我们常用的设备处于哪一层:
物理层:网卡,网线,集线器,中继器,调制解调器
数据链路层:网桥,交换机
网络层:路由器

TCP/IP四层参考模型在分层上有些不同,但还是能大致和OSI各层对应起来,它的应用层包括OSI的应用层、表示层和会话层,传输层对应OSI的传输层,网络层对应网络层,物理层包括了数据链路层和物理层。至于二者的区别,借用网上某位大神的解释:

OSI是一个完整的、完善的宏观模型,他包括了硬件层(物理层),当然也包含了很多上面途中没有列出的协议(比如DNS解析协议等);而TCP/IP(参考)模型,更加侧重的是互联网通信核心(也是就是围绕TCP/IP协议展开的一系列通信协议)的分层,因此它不包括物理层,以及其他一些不想干的协议;其次,之所以说他是参考模型,是因为他本身也是OSI模型中的一部分,因此参考OSI模型对其分层。

TCP和UDP

TCP(传输控制协议,Transport Controll Protocol)和UDP(用户数据报协议,User Data Protocol)是传输层最重要的两种协议,为上层用户提供级别的通信可靠性。
传输控制协议(TCP):TCP协议全称是传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。客户端和服务器端建立TCP连接是通过三次握手来实现的,断开连接时通过四次挥手实现,这两个都是面试中经常被问到的知识点且十分重要,由于本篇重点还是写OkHttp的,这里就不再赘述了。
用户数据报协议(UDP):UDP协议全称是用户数据报协议,在网络中它与TCP协议一样用于处理数据包,是一种无连接的协议。
TCP/IP 和UDP最大的区别就是:TCP/IP是面向连接的,UDP是无连接的;同样,TCP/IP是可靠的,而UDP是不可靠的;但是UDP传输速度快(毕竟UDP不用各种握手挥手嘛),适合大量数据且不太关心可靠性的传输。

HTTP协议概述

HTTP协议,全名为Hyper Text Transfer Protocol(超文本传输协议),它是TCP/IP协议的一个应用层协议,用于定义WEB浏览器与WEB服务器之间交换数据 的过程及数据本身的格式。
典型的HTTP事务处理有如下的过程:
(1)客户与服务器建立连接;
(2)客户向服务器提出请求;
(3)服务器接受请求,并根据请求返回相应的文件作为应答;
(4)客户与服务器关闭连接。

请求格式
请求行 : 请求方式 请求路径 版本
请求头 : 以key-value形式组成,K:V。。。
空行
请求体 : 用于数据传递:get方式没有请求体(参数地址传递) post方式有请求体式:

响应格式:
响应行 :版本 响应码 响应信息
响应头 :以key-value形式组成,K:V。。。
空行
响应体 :响应正文

常用的请求方式:
Get:请求获取Request-URI所标识的资源
POST:在Request-URI所标识的资源后附加新的数据

Socket以及Http:
Socket:长连接,理论上客户端和服务端一旦建立连接,则不会主动断掉;但是由于各种环境因素可能会是连接断开,比如说:服务器端或客户端主机down了,网络故障,或者两者之间长时间没有数据传输,网络防火墙可能会断开该链接已释放网络资源。所以当一个socket连接中没有数据的传输,那么为了位置连续的连接需要发送心跳消息,具体心跳消息格式是开发者自己定义的。
Http:短连接,即客户端向服务器发送一次请求,服务器端响应后连接即会断掉。

同步和异步:
同步请求:发送方发出数据后,等接收方发回响应以后才发下一个数据包的通讯方式。
异步请求:发送方发出数据后,不等接收方发回响应,接着发送下个数据包的通讯方式

OkHttp的使用方法

好了,说了这么多,终于要开始本篇的主题了。OkHttp,在GitHub上它是这样介绍自己的:An HTTP client for Android, Kotlin, and Java. 它的优点有很多,例如:支持get请求和post请求,支持基于HHTTPtp的文件上传和下载,支持加载图片,支持下载文件透明的GZIP压缩,支持响应缓存避免重复的网络请求,支持使用连接池来降低响应延迟问题等。目前最新的版本为Kotlin版的4.2,不过本篇还是从常用的OkHttp3来进行学习。

GitHub地址:https://github.com/square/okhttp(目前主分支是Kotlin版,建议下载其它分支的java版本)

引入依赖:
由于OkHttp已经被google官方认定为推荐的android框架,因此在File-Project Structure 里选择添加,搜索okhttp即可直接添加依赖:
添加okhttp依赖
添加完后在build.gradle文件里会有如下一行:

implementation 'com.squareup.okhttp3:okhttp:3.14.4'

说明成功添加了依赖,当然也可以手动下载jar包后手动导入,这里不再细说。
OkHttp进行GET请求
使用OkHttp进行基本的Get请求需要以下四步:
1 . 新建OkHttpClient对象:

OkHttpClient client = new OkHttpClient();

2 . 构造Request对象:

    Request request = new Request.Builder()
                .get()
                .url("https:www.baidu.com")
                .build();

值得一提的是,OkHttp采用了Java中一种常见设计模式:builder (建造者)模式,这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式,在用户不知道对象的建造过程和细节的情况下就可以直接创建复杂的对象。这里采用了建造者模式和链式调用指明是进行Get请求,并传入Get请求的地址来构造
Request对象。

3 . 将Request封装为Call:

Call call = client.newCall(request);

4 . 根据情况调用同步或者异步请求方法(同步调用容易引起阻塞,因此我们一般都使用异步调用):

//同步调用

Response response = call.execute();

//异步调用

call.enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        //onFailure
    }

    @Override
    public void onResponse(Call call, final Response response) throws IOException {
        //onResponse
    }
});

OkHttp进行POST请求
那么进行POST请求与GET请求大体相似,只是在构建Request时需要传递一个RequestBody作为post的参数。RequestBody有两个子类:FormBody和MultipartBody,分别用于表单提交和文件上传。

1 . 构建RequestBody :

RequestBody requestBody  = new FormBody.Builder()
                .add("username", "admin")
                .add("password", "admin")
                .build();

2 . 构建Request,并传入requestBody:

Request request = new Request.Builder()
                .url("http://www.baidu.com/")
                .post(requestBody)
                .build();

这样就完成了表单的提交,其实和GET请求差不多。

response即为收到的响应,可以获取到字符串。同步调用呢比较简单,但是不推荐大家使用,异步调用会在请求成功后回调onFailure或onResponse函数,然后在回调函数里进行相关操作。

还有,应用要访问网络需要事先添加相应的权限:

要注意的是,Android3.0 以后已经不允许在主线程访问网络。

OkHttp源码解读

跟随请求过程漫步源码

下面我们顺着一个OkHttp请求,依次探索其源码,看下网络请求时其内部都做了什么。
1.首先从新建一个OkHttpClient对象说起,查看源码:

 public OkHttpClient() {
    this(new Builder());
  }
public Builder() {
  dispatcher = new Dispatcher();//调度器
  protocols = DEFAULT_PROTOCOLS;//协议
  connectionSpecs = DEFAULT_CONNECTION_SPECS;//传输层版本和连接协议
  eventListenerFactory = EventListener.factory(EventListener.NONE);
  proxySelector = ProxySelector.getDefault();//代理选择器
  cookieJar = CookieJar.NO_COOKIES;//cookie
  socketFactory = SocketFactory.getDefault();//socket 工厂
  hostnameVerifier = OkHostnameVerifier.INSTANCE;//主机名字确认
  certificatePinner = CertificatePinner.DEFAULT;//证书链
  proxyAuthenticator = Authenticator.NONE;//代理身份验证
  authenticator = Authenticator.NONE;//本地身份验证
  connectionPool = new ConnectionPool();//链接池 复用连接
  dns = Dns.SYSTEM;//域名
  followSslRedirects = true;//安全套接层重定向
  followRedirects = true;//本地重定向
  retryOnConnectionFailure = true;//重试连接失败
  connectTimeout = 10_000;//连接超时
  readTimeout = 10_000;//读取超时
  writeTimeout = 10_000;//写入超时
  pingInterval = 0;//ping间隔
}

创建OkHttpClient 时如果我们没有配置okhttp会帮我们实现上面这些默认配置。

2.新建Request时同样可以通过其内部类Build配置一些网络参数:

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

3.将request构建成Call然后调用Call.enqueue()执行异步请求:

  @Override public Call newCall(Request request) {
    return RealCall.newRealCall(this, request, false /* for web socket */);
  }

实际是调用了RealCall.newRealCall(),这里看一下RealCall的enqueue():

  @Override public void enqueue(Callback responseCallback) {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    transmitter.callStart();
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }

可以看到这里使用了synchronized关键字修饰确保同一时间只能调用一次,最后是利用dispatcher的enqueue(new AsyncCall(responseCallback))来实现的异步请求,下面就来一探OkHttp的重要类,调度器dispatcher。

探究调度器dispatcher

dispatcher,也有人叫分发器,它OkHttp里的一个重要概念,我们先看一下该类的主要成员变量:

public final class Dispatcher {
  private int maxRequests = 64;//支持的最大并发请求数量
  private int maxRequestsPerHost = 5;//每个主机的最大请求数量
  private @Nullable Runnable idleCallback;

  /** Executes calls. Created lazily. */
  private @Nullable ExecutorService executorService;//请求线程池

  /** Ready async calls in the order they'll be run. */
  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();将要运行的异步请求队列

  /** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();正在运行的异步请求队列

  /** Running synchronous calls. Includes canceled calls that haven't finished yet. */
  private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();///正在运行的同步请求队列

下面看下异步请求调用的方法:

  void enqueue(AsyncCall call) {
    synchronized (this) {
      readyAsyncCalls.add(call);
      
      // Mutate the AsyncCall so that it shares the AtomicInteger of an existing running call to
      // the same host.
      if (!call.get().forWebSocket) {
        AsyncCall existingCall = findExistingCallWithHost(call.host());
        if (existingCall != null) call.reuseCallsPerHostFrom(existingCall);
      }
    }
    promoteAndExecute();
  }

可以看到call被加入到了将要运行的异步请求队列中,然后如果构建newRealCall时传的forWebSocket为true则找到当前存在的call使其与同一主机共享现有的运行调用。(实话说这里没太理解,应该是后加的逻辑,网上也没找到)
之后运行promoteAndExecute(),看一下代码:

  private boolean promoteAndExecute() {
    assert (!Thread.holdsLock(this));

    List<AsyncCall> executableCalls = new ArrayList<>();
    boolean isRunning;
    synchronized (this) {
      for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
        AsyncCall asyncCall = i.next();

        if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
        if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // Host max capacity.

        i.remove();
        asyncCall.callsPerHost().incrementAndGet();
        executableCalls.add(asyncCall);
        runningAsyncCalls.add(asyncCall);
      }
      isRunning = runningCallsCount() > 0;
    }

    for (int i = 0, size = executableCalls.size(); i < size; i++) {
      AsyncCall asyncCall = executableCalls.get(i);
      asyncCall.executeOn(executorService());
    }

    return isRunning;
  }

其实就是把所有readAsyncCall中的Runnable在executorService里执行,executorService就是开启了一个线程池。

  public synchronized ExecutorService executorService() {
    if (executorService == null) {
      executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
          new SynchronousQueue<>(), Util.threadFactory("OkHttp Dispatcher", false));
    }
    return executorService;
  }

在线程池中会执行Runnable(也就是AsyncCall)的executeOn方法.

    void executeOn(ExecutorService executorService) {
      assert (!Thread.holdsLock(client.dispatcher()));
      boolean success = false;
      try {
        executorService.execute(this);
        success = true;
      } catch (RejectedExecutionException e) {
        InterruptedIOException ioException = new InterruptedIOException("executor rejected");
        ioException.initCause(e);
        transmitter.noMoreExchanges(ioException);
        responseCallback.onFailure(RealCall.this, ioException);
      } finally {
        if (!success) {
          client.dispatcher().finished(this); // This call is no longer running!
        }
      }
    }

executeOn会在线程中执行execute(this),这里主要调用的方法是getResponseWithInterceptorChain()(同步请求也会调用),这里真正开始网络请求并会拿到网络请求的响应response :

Response response = getResponseWithInterceptorChain();getResponseWithInterceptorChain()
  Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(new RetryAndFollowUpInterceptor(client));
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0,
        originalRequest, this, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    boolean calledNoMoreExchanges = false;
    try {
      Response response = chain.proceed(originalRequest);
      if (transmitter.isCanceled()) {
        closeQuietly(response);
        throw new IOException("Canceled");
      }
      return response;
    } catch (IOException e) {
      calledNoMoreExchanges = true;
      throw transmitter.noMoreExchanges(e);
    } finally {
      if (!calledNoMoreExchanges) {
        transmitter.noMoreExchanges(null);
      }
    }
  }

先是在List添加了一堆的interceptor,然后新建了一个Chain-RealInterceptorChain,并调用了chain的proceed方法。这里用到了责任链模式,留着以后再详细学习。

好了到这里网络请求流程基本已经走完了,感觉自己也随着OkHttp在网络世界里遨游了一圈,不过实话说有很多地方只是理解了皮毛,还需日后在实战中多多思考。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值