RetryAndFollowUpInterceptor

RealCall中的传值

1
2
3
4
5
6
private RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    this.client = client;
    this.originalRequest = originalRequest;
    this.forWebSocket = forWebSocket;
    this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);
}

RealCall初始化时,会创建这个RetryAndFollowUpInterceptor拦截器,并且传递了OkHttpClient

观察RealCall中的所有默认拦截器,RealCall对于这个拦截器的处理优先级是最高的,甚至在构造方法中就初始化了,而且在众多内置拦截器中,RealCall单只给RetryAndFollowUpInterceptor传递了OkhttpClient,可见这个拦截器的重要性!

RetryAndFollowUpInterceptor的处理过程

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
public final class RetryAndFollowUpInterceptor implements Interceptor {

  private static final int MAX_FOLLOW_UPS = 20;

  private final OkHttpClient client;
  private final boolean forWebSocket;
  private volatile StreamAllocation streamAllocation;
  private Object callStackTrace;
  private volatile boolean canceled;

  public RetryAndFollowUpInterceptor(OkHttpClient client, boolean forWebSocket) {
    this.client = client;
    this.forWebSocket = forWebSocket;
  }

  // 取消,不需要看了
  public void cancel() {
    canceled = true;
    StreamAllocation streamAllocation = this.streamAllocation;
    if (streamAllocation != null) streamAllocation.cancel();
  }

  @Override public Response intercept(Chain chain) throws IOException {
    
    Request request = chain.request();
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Call call = realChain.call();
    EventListener eventListener = realChain.eventListener();

    // 初始化数据流分配器
    StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
        createAddress(request.url()), call, eventListener, callStackTrace);

    this.streamAllocation = streamAllocation;

    // 重定向次数
    int followUpCount = 0;

    // 前一个response
    Response priorResponse = null;

    // 又是个永动机
    while (true) {

      Response response;
      boolean releaseConnection = true;
      try {
        // 调用下一个拦截器,最终获取到response
        response = realChain.proceed(request, streamAllocation, null, null);
        releaseConnection = false;
      } catch (RouteException e) {
        //发现异常就重试
        if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
          throw e.getLastConnectException();
        }
        releaseConnection = false;
        continue;
      } catch (IOException e) {
        //发现异常就重试
        ...
        continue;
      } finally {
        // 释放连接
        if (releaseConnection) {
          streamAllocation.streamFailed(null);
          streamAllocation.release();
        }
      }

      // 附加前一个response,这种都是没有body
      if (priorResponse != null) {
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                    .body(null)//没有body
                    .build())
            .build();
      }

      // 根据上一个response,生成重定向需要的request
      Request followUp = followUpRequest(response, streamAllocation.route());

      // 如果不再需要重定向,就直接返回response了
      if (followUp == null) {
        if (!forWebSocket) {
          streamAllocation.release();
        }
        return response;
      }

      // 关闭 response的body
      closeQuietly(response.body());

      // 如果重定向/重试次数大于20次就会报错
      if (++followUpCount > MAX_FOLLOW_UPS) {
        streamAllocation.release();
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }
      ...

      // 如果不是同一个主机地址,则连接不能复用,只能释放上一个,并且新建一个连接;否则将复用之前创建好的连接
      if (!sameConnection(response, followUp.url())) {
        // 用request重建连接
        streamAllocation.release();
        streamAllocation = new StreamAllocation(client.connectionPool(),
            createAddress(followUp.url()), call, eventListener, callStackTrace);
        this.streamAllocation = streamAllocation;
      } 

      request = followUp;
      priorResponse = response;
    }
  }

  // 拼装地址,包含主机、端口号、DNS等等
  private Address createAddress(HttpUrl url) {
    SSLSocketFactory sslSocketFactory = null;
    HostnameVerifier hostnameVerifier = null;
    CertificatePinner certificatePinner = null;
    // 判断是否https
    if (url.isHttps()) {
      sslSocketFactory = client.sslSocketFactory();
      hostnameVerifier = client.hostnameVerifier();
      certificatePinner = client.certificatePinner();
    }

    return new Address(url.host(), url.port(), client.dns(), client.socketFactory(),
        sslSocketFactory, hostnameVerifier, certificatePinner, client.proxyAuthenticator(),
        client.proxy(), client.protocols(), client.connectionSpecs(), client.proxySelector());
  }

  // 尝试从与服务器通信失败中恢复(重试)
  private boolean recover(IOException e, StreamAllocation streamAllocation,
      boolean requestSendStarted, Request userRequest) {

    streamAllocation.streamFailed(e);

    // 重试
    // return false - 应用层拒绝了重试,这是我们在初始化client时设置的
    if (!client.retryOnConnectionFailure()) return false;
    ...
    // 如果有更多的路由,比如DNS返回了多个CDN的ip地址,就切换线路重试,否则取消。
    if (!streamAllocation.hasMoreRoutes()) return false;
    ...
    return true;
  }

  // 判断连接是否可以恢复
  private boolean isRecoverable(IOException e, boolean requestSendStarted) {
    ...
  }

  // 根据responseCode判断,如果response需要重定向,则创建一个重定向所需的request
  private Request followUpRequest(Response userResponse, Route route) throws IOException {
    
    int responseCode = userResponse.code();

    final String method = userResponse.request().method();

    switch (responseCode) {

      ...
      case HTTP_UNAUTHORIZED:// OAuth2.0 授权码方式要求重定向到一个授权页面
        return client.authenticator().authenticate(route, userResponse);

      ...
      
      // 响应码是3xx系列就需要重定向处理
      case HTTP_MULT_CHOICE:
      case HTTP_MOVED_PERM:
      case HTTP_MOVED_TEMP:
      case HTTP_SEE_OTHER:
        // 客户端允许重定向吗?
        if (!client.followRedirects()) return null;

        // 原来response的head中有这么个东东
        String location = userResponse.header("Location");
        if (location == null) return null;

        // 根据location生成重定向的url
        // 这个resolve方法是真复杂,懒得看了
        HttpUrl url = userResponse.request().url().resolve(location);

        // 不会重定向到不支持的协议
        if (url == null) return null;

        // 不会在http和https之间重定向
        boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
        if (!sameScheme && !client.followSslRedirects()) return null;

        // 大多数的重定向都不需要requestBody
        // 其实这里就是依据条件来分别调用建造者模式,最终会构建一个符合条件的request
        Request.Builder requestBuilder = userResponse.request().newBuilder();
        
        if (HttpMethod.permitsRequestBody(method)) {
          final boolean maintainBody = HttpMethod.redirectsWithBody(method);
          if (HttpMethod.redirectsToGet(method)) {
            requestBuilder.method("GET", null);
          } else {
            RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
            requestBuilder.method(method, requestBody);
          }
          if (!maintainBody) {
            requestBuilder.removeHeader("Transfer-Encoding");
            requestBuilder.removeHeader("Content-Length");
            requestBuilder.removeHeader("Content-Type");
          }
        }

        // 如果重定向的不是之前的连接,就移除OAuth-token,安全性考量
        if (!sameConnection(userResponse, url)) {
          requestBuilder.removeHeader("Authorization");
        }
        // 完活返回
        return requestBuilder.url(url).build();

      case HTTP_CLIENT_TIMEOUT:
      case HTTP_UNAVAILABLE:
        //  如果客户端超时,会采用各种方式重试,重试失败会返回null
        ...
        return userResponse.request();
        ...
        return null;

      default:
        return null;
    }
  }

  private int retryAfter(Response userResponse, int defaultDelay) {
    String header = userResponse.header("Retry-After");

    if (header == null) {
      return defaultDelay;
    }

    // https://tools.ietf.org/html/rfc7231#section-7.1.3
    // currently ignores a HTTP-date, and assumes any non int 0 is a delay
    if (header.matches("\\d+")) {
      return Integer.valueOf(header);
    }

    return Integer.MAX_VALUE;
  }

  /**
   * Returns true if an HTTP request for {@code followUp} can reuse the connection used by this
   * engine.
   */
  private boolean sameConnection(Response response, HttpUrl followUp) {
    HttpUrl url = response.request().url();
    return url.host().equals(followUp.host())
        && url.port() == followUp.port()
        && url.scheme().equals(followUp.scheme());
  }
}

总结

RetryAndFollowUpInterceptor的职责:

本来根据单一原则,不应该把三个功能耦合到一个拦截器,但是为什么JW大神还是如此来设计呢?

而且我还有个疑问就是,重试次数竟然是固定不超过20次,不能由我们自己来设置。

解决不能自定义重试次数的问题

方式一:自定义重试拦截器

首先关闭OkHttp自带的重试机制

1
2
3
OkHttpClient.Builder()
            .retryOnConnectionFailure(false)
            .build()

然后自定义一个重试拦截器,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class RetryIntercepter implements Interceptor {

  public int maxRetry;//最大重试次数
  private int retryNum = 0;//假如设置为3次重试的话,则最大可能请求4次(默认1次+3次重试)

  public RetryIntercepter(int maxRetry) {
      this.maxRetry = maxRetry;
  }

  @Override
  public Response intercept(Chain chain) throws IOException {
      Request request = chain.request();
      System.out.println("retryNum=" + retryNum);
      Response response = chain.proceed(request);
      while (!response.isSuccessful() && retryNum < maxRetry) {
          retryNum++;
          System.out.println("retryNum=" + retryNum);
          response = chain.proceed(request);
      }
      return response;
  }
}

上面是我从网上找到的例子,其实他写的并不好,但是可以作为一个思路。

方式二 RxJava的retryWhen操作符