阅读 871

【开源解码】之OKHttp

整体流程

  • 创建一个OKhttpClient,请求客户端类,作为一个全局的实例进行保存,全局网络请求都使用这一个单例对象
  • 创建一个Request对象,封装了请求报文信息,包含url,path,请求方法get/post,请求参数,请求头,在Request内部是通过build链式生成了request对象
  • 通过调用request调用的newCall方法得到Call对象,在okhttp中call的实现类是RealCall,通过这个call能够进行后续的异步同步网络请求操作
  • 决定异步同步的是Dispatcher分发器,内部维护了一个线程池,这个线程池用来执行网络请求,realCall会把请求添加到Dispatcher中,Dispatcher中有三个队列来维护同步异步请求
  • 不管同步还是异步,在Okhttp内部都是通过拦截器链interceptors来进行真正的服务器数据获取,会构建一个拦截器链,依次执行每一个拦截器来将服务器数据返回,主要拦截器包括
    • RetryAndFollow: 负责失败重试及重定向之后直接请求
    • Bridge: 设置内容长度,内容编码,gzip压缩,cookie报头等,主要是请求前的操作
    • Cache: 负责缓存管理,当有符合要求的缓存时,直接返回缓存,而不需要经过网络端
    • Connect:为当前请求找到合适的连接,注意,这里可能会复用已有的连接
    • CallServer:向服务器发起真正的访问请求,接收到服务器返回的数据
  • 如图

同步与异步请求

同步请求

  1. 创建OkHttpClient和Request对象:通过build创建了OkHttpClient对象和Request对象
  2. 将Request封装成Call对象:通过build构建一个Request对象,通过OkHttpClient和Request对象,构建出Call对象
  3. 调用Call的execute()发送同步请求

注意:发送同步请求后,就会进入阻塞状态,直到收到响应。

异步请求

  1. 创建OkHttpClient和Request对象
  2. 将Request封装成Call对象
  3. 调用Call的enqueue发送同步请求,在onFailure和onResponse回调中做相应的数据处理

注意:onFailure和onResponse都是在工作线程(子线程)中执行的

enqueue方法总结

  • 判断当前call
  • 封装成了一个AsyncCall对象
  • client.dispatcher().enqueue()

与同步请求的区别

  • 发起请求的方法调用: 同步execute及异步enqueue
  • 阻塞线程与否:同步阻塞异步不阻塞

代码如下

    val url = "https://api.github.com/users/dsh/repos"
    val hostname = "api.github.com"

    val client = OkHttpClient.Builder()
      .build()
    val request: Request = Request.Builder()
      .url(url)
      .build()
    client.newCall(request)
      .enqueue(object : Callback {
        override fun onFailure(call: Call, e: IOException) {
          e.printStackTrace()
        }

        override fun onResponse(call: Call, response: Response) {
          println("Response status code: ${response.code()}")
        }
      })
复制代码

任务调度核心类Dispatcher

Q1.okhttp如何实现同步异步请求?
发送的同步/异步请求都会在dispatcher中管理其状态

Q2.到底什么是dispatcher
dispatcher的作用为维护请求的状态,并维护一个线程池,用于执行请求。

Q3.异步请求为什么需要两个队列

  • Dispatcher 生产者
  • ExecutorService 消费者池
  • Deque 缓存
  • Deque 正在运行的任务

Dispatcher的几个主要变量 Dispatcher的调度流程图解

OKHttp拦截器流

官网:拦截器是OkHttp中提供一种强大机制,它可以实现网络监听、请求以及响应重写、请求失败重试等功能

OkHttp主要有网络拦截器和应用程序拦截器,如图,我们主要关心应用程序拦截器 Okhttp主要的几个拦截器

核心逻辑:

  1. 在发起请求前对request进行处理
  2. 调用下一个拦截器,获取response
  3. 对response进行处理,返回给上一个拦截器

以同步请求方法execute为例,如图

  • ①创建一系列拦截器,并将其放入一个拦截器list中

  • ②创建一个拦截器链ReallnterceptorChain,并执行拦截器链的proceed方法

  • ③proceed方法创建并调用下一个拦截器,获取response

  • ④每一个拦截器对response进行处理,返回给上一个拦截器

RetryAndFollowUpInterceptor 重试重定向拦截器

主要负责失败重连,并不是所有的网络请求失败后都能重连,是有一定限制范围的,Okhttp内部会帮助我们检测网络异常,及响应码判断等,如果是在限制范围内的话,就可以根据条件进行网络请求的重连

  • 调用 ReallnterceptorChain.proceed(...)进行网络请求
  • 根据异常结果或者响应结果判断是否要进行重新请求
  • 调用下一个拦截器,对response进行处理,返回给上一个拦截器
@Override 
public Response intercept(Chain chain) throws IOException {
    ...
    //重连不是无限制的,超过20个重连请求,就不会再请求
    if (++followUpCount > MAX_FOLLOW_UPS) {
            throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }
    ...
}
复制代码

BridgeInterceptor 桥接拦截器

主要负责添加请求头部信息,包括内容长度、编码方式、压缩等,其中的keepAlive是连接复用的基础。

工作流程

  • 1.是负责将用户构建的一个Request请求转化为能够进行网络访问的请求

  • 2.将这个符合网络请求的Request 进行网络请求

  • 3.将网络请求回来的响应Response转化为用户可用的Response。

CacheInterceptor缓存拦截器

缓存put与get方法

put方法

get方法

缓存拦截器操作

ConnectInterceptor (连接拦截器)

连接池ConnectionPool

  • 每次http请求都会产生一个Transmitter对象

  • Transmitter对象的弱引用添加到RealConnection对象的transmitters集合

  • 从连接池中获取

ConnectionPool的回收

  1. OKHttp使用了GC回收算法
  2. Transmitter的数量会渐渐变成0
  3. 被线程池监测并回收,这样就可以保持多个健康的keep-alive连接

CallServerInterceptor

  • 发起真正的网络请求
  • 接收服务器的响应

主要包含以下步骤:

  1. 写入请求header
  2. 写入请求body
  3. 构建响应头
  4. 构建响应体
  5. 返回响应response
public final class CallServerInterceptor implements Interceptor {
  private final boolean forWebSocket;

  public CallServerInterceptor(boolean forWebSocket) {
    this.forWebSocket = forWebSocket;
  }

  @Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Exchange exchange = realChain.exchange();
    Request request = realChain.request();

    long sentRequestMillis = System.currentTimeMillis();

    //1. 写入请求的header
    exchange.writeRequestHeaders(request);

    boolean responseHeadersStarted = false;
    Response.Builder responseBuilder = null;
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      // 如果请求头的Expect为100-continue时,只发送请求头
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        exchange.flushRequest();
        responseHeadersStarted = true;
        exchange.responseHeadersStart();
        responseBuilder = exchange.readResponseHeaders(true);
      }

      if (responseBuilder == null) {
        if (request.body().isDuplex()) {
          // 准备body 一遍应用程序可以稍后发送正文
          exchange.flushRequest();
          BufferedSink bufferedRequestBody = Okio.buffer(
              exchange.createRequestBody(request, true));
          request.body().writeTo(bufferedRequestBody);
        } else {
          // 如果符合100-continue,写入body
          BufferedSink bufferedRequestBody = Okio.buffer(
              exchange.createRequestBody(request, false));
          request.body().writeTo(bufferedRequestBody);
          bufferedRequestBody.close();
        }
      } else {
        exchange.noRequestBody();
        if (!exchange.connection().isMultiplexed()) {
          exchange.noNewExchangesOnConnection();
        }
      }
    } else {
      exchange.noRequestBody();
    }

    //结束请求
    if (request.body() == null || !request.body().isDuplex()) {
      exchange.finishRequest();
    }

    if (!responseHeadersStarted) {
      exchange.responseHeadersStart();
    }

    //构建响应体
    //读取网络响应的头部信息
    if (responseBuilder == null) {
      responseBuilder = exchange.readResponseHeaders(false);
    }

    Response response = responseBuilder
        .request(request)
        .handshake(exchange.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    int code = response.code();
    if (code == 100) {
      response = exchange.readResponseHeaders(false)
          .request(request)
          .handshake(exchange.connection().handshake())
          .sentRequestAtMillis(sentRequestMillis)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();

      code = response.code();
    }

    exchange.responseHeadersEnd(response);

    //读取body信息
    if (forWebSocket && code == 101) {
      response = response.newBuilder()
          .body(Util.EMPTY_RESPONSE)
          .build();
    } else {
      response = response.newBuilder()
          .body(exchange.openResponseBody(response))
          .build();
    }

    if ("close".equalsIgnoreCase(response.request().header("Connection"))
        || "close".equalsIgnoreCase(response.header("Connection"))) {
      exchange.noNewExchangesOnConnection();
    }

    if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
      throw new ProtocolException(
          "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
    }

    return response;
  }
}

复制代码

Okhttp中一次网络请求的大致过程

  1. Call对象请求的封装
  2. dispatcher对请求的分发
  3. getResponseWithInterceptors()方法

面试相关

1. Android基础网络编程: socket、HttpClient和HttpURLConnection

  • Socket
    • 是一个对TCP /IP协议进行封装的编程调用接口
    • 成对出现,一对套接字:包括ip地址和端口号
  • Socket与Http
    • Http:采用请求一响应方式。HTTP协议属于应用层
    • Socket:采用服务器主动发送数据的方式,Socket属于传输层
  • HttpClient
    • 6.0之后已经被移除
  • HttpURLConnection
    class HttpConnectionTest {
        public void HttpConnectionGet() throws IOException {
            String getURL = "GET_URL" + " ?username= "
                    + URLEncoder.encode(" fat man ", "utf-8");
            URL getUrl = new URL(getURL);
            HttpURLConnection connection = (HttpURLConnection)getUrl.openConnection();
            connection.connect();
            //取得输入流,并使用Reader读取,这时才真正的把request发送至服务器端
            BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
            System.out.println(" ======== ");
            System.out.println(" Contents of get request ");
            System.out.println(" ======== ");
            String lines;
            while((lines = reader.readLine())!= null){
                System.out.println(lines);
            }
            reader.close();
            //断开连接,底层就是关闭socket连接
            connection.disconnect();
            System.out.println(" ======== ");
            System.out.println(" Contents of get request ends ");
            System.out.println(" ======== ");
        }
    }
    
    复制代码

2. 了解websocket吗?知道和socket的区别吗?okhttp是如何处理websocket的相关问题的?

  • 通过client.newWebSocket(request,listener)建立socket建立连接
  • 在listener中监听和发送消息
  • 记得要调用shutdown()关闭socket连接
        public void OkhttpSocketTest(){
            OkHttpClient client = new OkHttpClient.Builder().build();
            Request request = new Request.Builder().url("www.webSocket.com").build();
            WebSocketListener listener = new WebSocketListener() {
    
                @Override public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) {
                    super.onOpen(webSocket, response);
                    webSocket.send("hello");
                    webSocket.send(ByteString.decodeHex("absd"));
                    webSocket.close(1000,"bye");
                }
    
                @Override public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) {
                    super.onMessage(webSocket, text);
                    //和UI交互,需要使用hander
                    setText("onMessage: " + bytes);
                    handler.sendEmptyMessage(0);
    
                }
    
                @Override public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString bytes) {
                    super.onMessage(webSocket, bytes);
                    setText("onMessage byteString: " + bytes);
                    handler.sendEmptyMessage(0);
                }
    
                @Override public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t,
                        @Nullable Response response) {
                    super.onFailure(webSocket, t, response);
                    setText("onFailure:"+ t.getMessage());
                }
    
                @Override
                public void onClosing(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
                    super.onClosing(webSocket, code, reason);
                    webSocket.close(1000, null);
                    setText("onClosing:"+ code + "/" + reason);
                }
    
                @Override
                public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
                    super.onClosed(webSocket, code, reason);
                    setText("onClosed:"+ code + "/" + reason);
                }
    
            };
            client.newWebSocket(request,listener);
            client.dispatcher().executorService().shutdown();
        }
    复制代码

3. WebSocker&轮询相关

  • 轮询是在特定的的时间间隔,由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。
  • 短轮询:Client每间隔一段时间向服务端发送请求,并且接收服务器响应
    • 缺点:在某个时间段 server没有更新数据,但client仍然每隔一段时间发送请求来询问,所以这段时间内的询问都是无效的冗余数据,浪费带宽(如直播评论短轮询,永远请求第一页的数据,返回大量重复数据)
  • 长轮询:服务端接收到请求后,不会立即返回response,当数据更新时,才会把response返回,如果没有数据更新,则会一直保持请求连接
  • 轮询缺点:
    • 浪费带宽(HTTP HEAD 是比较大的,有效信息比较少)
    • 消耗服务器cpu占用
  • webSocket通信模型
  • WebSocket与Http区别
    • 与Http同等的网络协议
    • 双向通信协议
  • WebSocket与Socket,完全没有关系
    • Socket其实并不是一个协议
    • WebSocket是一种协议
  • WebSocket总结
    • 1.本质上是一个基于TCP的协议
    • 2.向服务器发起一个HTTP请求/ “Upgrade:WebSocket"
    • 3.服务器端解析这些附加的头信息

4. Http如何处理缓存?Okhttp如何处理缓存相关问题?

  • 强制缓存对比缓存两种
  • 强制缓存:分为Expires和Cache-Control
    • Expires
      • Expires的值为服务端返回的到期时间,如果小于到期时间,则使用缓存
      • Http1.0经常使用,浏览器默认是1.1,所以基于已弃用
      • 问题:到期时间是服务器返回的
    • Cache-Control(重要)
      • Cache-control是由服务器返回的Response中添加的头信息
      • 取值:
        • private:客户端可以缓存
        • public:客户端和代理服务器都可以缓存
        • max-age:缓存内容将在多少秒之后失效
        • no-cache:对比缓存中用到
        • no-store:所有内容都不缓存
  • 对比缓存
    • 首先需要进行比较判断是否可以使用缓存。
    • ETag / If-None-Match:成对出现
      • Etag:服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识
      • If-None-Match:再次请求服务器时,通过此字段通知服务器客户段缓存数据的唯一标识
    • Last-Modified / If-Modified-Since
      • Last-Modified:服务器在响应请求时,告诉浏览器资源的最后修改时间
      • If-Modified-Since:再次请求服务器时,通过此字段通知服务器上次请求时,服务器返回的资源最后修改时间
  • 缓存图示

5. 断点续传的原理?如何实现?okhttp中如何实现相关问题?

  • 断点续传:从文件已经下载的地方开始继续下载
  • 原理是在头部添加RANGE信息
  • 都会创建RandomAccessFile对象并调用其seek方法

如图所示:

6. 多线程下载原理okhttp如何实现?

  • 多线程下载:每个线程只负责下载文件的一部分
  • 首先要获取文件大小totalSize以及每个线程要下载的文件大小partSize = totalSize/threadCount
  • 在每个线程中创建请求并创建RandomAccessFile对象,调用seek方法进行指定位置的写入,最后关闭IO流

图示:

7. 文件上传如何做?原理?okhttp如何完成文件上传

  • 指定Http头部的ContentType
  • 由Content-Type属性指定请求和响应的HTTP内容类型
  • 如下图的例子,如果文件太长,一个包无法发送完毕,需要boundry分隔符来实现分片上传
  • Okhttp中,主要是通过MultiPartBody来构建请求

图示:

8. json数据如何解析?okhttp如何解析json类型数据?

  • json:文本形式的数据交换格式
  • json 比 XML 更轻量,也比二进制更容易阅读
  • json解析
    • 传统的jsonObject解析
    • Gson
    • FastJson
  • Gson解析
    Gson mGson = new Gson();
    List<User> userList = mGson.fromJson(jsonArray.toString(), new TypeToken<List<User>>(){
                        } .getType());
    复制代码
    方式二
    JsonParser parser = new JsonParser();
    JsonArray jsonArray = parser.parse(json1).getAsJsonArray();
    Gson gson = new Gson();
    Arraylist<UserBean> userBeanList = new Arraylist<>();
    for (JsonElement user : jsonArray) {
        UserBean userBean = gson.fromJson(user,UserBean.class);
        userBeanList.add(userBean);
    }
    复制代码

9. okhttp如何处理https?

  • Https是一种基于SSL/TLS的Http协议

  • 不对称VS对称加密

    • 对称加密所使用的密钥我们可以通过非对称加密的方式发送出去
    • 不对称加密速度上不如对称加密,所以实际传输过程中https使用的是对称加密
  • Https算法

    • 不对称加密:SSL/TLS握手阶段
    • 对称加密:对传输数据进行加密
    • Hash:用于验证客户端发送给服务端数据完整性
  • Https通信过程总结:

    • 服务端用RSA生成公钥和私钥,把公钥放在证书里发送给客户端,私钥自己保存
    • 客户端接收到公钥后,首先向一个权威的服务器检查证书的合法性,如果证书合法,客户端产生一段随机数,这个随机数就作为通信的密钥,我们称之为对称密钥,用公钥加密这段随机数,然后发送到服务器
    • 服务器用密钥解密获取对称密钥,然后,双方就已对称密钥进行加密解密通信了

文章分类
Android
文章标签