Okhttp3框架中的那些坑点记录

Okhttp3作为主流的网络请求框架,我们在日常使用过程中,会使用其中的OkhttClient作为请求调用客户端,利用自定义拦截器来实现自定义的一些功能。这里我就梳理出来自身平时使用Okhttp3框架中遇到的一些坑和问题点,提供给各位阅读者参考:

坑1:请求头压缩问题

一般我们希望请求体能够实现压缩功能,会在请求头中添加请求头Accept-Encoding为gzip,deflate,但是此时通过Okhttp3进行网络请求时返回的网页是乱码。

这是由于在Okhttp中,如果在请求头添加addHeader("Accept-Encoding", "gzip, deflate"),Okhttp不会帮你处理Gzip的解压,需要你自己去处理。而在Okhttp3的桥接拦截器中会自动帮助我们进行Accept-Encoding请求头的添加操作:

boolean transparentGzip = false;
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
  transparentGzip = true;
  requestBuilder.header("Accept-Encoding", "gzip");
}

故我们在实际使用中无需添加Accept-Encodinggzip,只需要去除该请求头的添加,就可以解决上面出现的问题了~

坑2:SSL证书忽略问题

默认http3会对https的SSL安全证书进行验证,此时测试服务端可能只是配置了本地证书,而该证书并不能被Okhttp3认证通过,就会报java.io.IOException: Hostname was not verified异常,此时,我们如果需要解决该异常问题,就需要通过重新配置注册OkhttpClient来解决这个问题。

首先我们来看一个简单的OkhttpClient的Bean注册代码:

public OkHttpClient okHttpClientWithRedirect() {
  OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .followRedirects(true)
    .connectTimeout(15, TimeUnit.SECONDS)
    .writeTimeout(15, TimeUnit.SECONDS)
    .readTimeout(15, TimeUnit.SECONDS)
    .retryOnConnectionFailure(false)
    .build();
  return okHttpClient;
}

此时,我们要想实现SSL证书的忽略认证,第一步则是构建一个SSLSocketFactory

public static SSLSocketFactory getSSLSocketFactory() {
  try {
    SSLContext sslContext = SSLContext.getInstance("SSL");
    sslContext.init(null, getTrustManager(), new SecureRandom());
    return sslContext.getSocketFactory();
  } catch (Exception e) {
    throw new RuntimeException(e);
  }
}

private static TrustManager[] getTrustManager() {
  return new TrustManager[]{
    new X509TrustManager() {
      @Override
      public void checkClientTrusted(X509Certificate[] chain, String authType) {
      }
      @Override
      public void checkServerTrusted(X509Certificate[] chain, String authType) {
      }
      @Override
      public X509Certificate[] getAcceptedIssuers() {
        return new X509Certificate[]{};
      }
    }
  };
}

然后,获取X509TrustManager

public static X509TrustManager getX509TrustManager() {
  X509TrustManager trustManager = null;
  try {
    TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    trustManagerFactory.init((KeyStore) null);
    TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
    if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
      throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
    }
    trustManager = (X509TrustManager) trustManagers[0];
  } catch (Exception e) {
    e.printStackTrace();
  }
  return trustManager;
}

最后,在构建OkHttpClient中,添加对应的SSLSocketFactoryHostnameVerifier即可

public OkHttpClient okHttpClientWithRedirect() {
  OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .followRedirects(true)
    .connectTimeout(15, TimeUnit.SECONDS)
    .writeTimeout(15, TimeUnit.SECONDS)
    .readTimeout(15, TimeUnit.SECONDS)
    .retryOnConnectionFailure(false)
    .sslSocketFactory(getSSLSocketFactory(), getX509TrustManager())
    .hostnameVerifier(((s, sslSession) -> true))
    .build();
  return okHttpClient;
}

坑3:307/308重定向问题

我们都知道,Okhttp3有一个很牛逼的拦截器链,其中重定向和重试则是在它的RetryAndFollowUpInterceptor中事项的。这里简单看一下其中的一段源码:

private Request followUpRequest(Response userResponse) throws IOException {
    if (userResponse == null) throw new IllegalStateException();
    Connection connection = streamAllocation.connection();
    Route route = connection != null
        ? connection.route()
        : null;
    int responseCode = userResponse.code();
    final String method = userResponse.request().method();
    switch (responseCode) {
// 407 客户端使用了HTTP代理服务器,在请求头中添加 “Proxy-Authorization”,让代理服务器授权 case HTTP_PROXY_AUTH:
		case HTTP_PROXY_AUTH:
			Proxy selectedProxy = route != null
			? route.proxy()
            : client.proxy();
        if (selectedProxy.type() != Proxy.Type.HTTP) {
          throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using
proxy");
		}
		return client.proxyAuthenticator().authenticate(route, userResponse);
// 401 需要身份验证 有些服务器接口需要验证使用者身份 在请求头中添加 “Authorization” case HTTP_UNAUTHORIZED:
	case HTTP_UNAUTHORIZED:
		return client.authenticator().authenticate(route, userResponse); // 308 永久重定向
// 307 临时重定向
	case HTTP_PERM_REDIRECT:
	case HTTP_TEMP_REDIRECT:
// 如果请求方式不是GET或者HEAD,框架不会自动重定向请求
if (!method.equals("GET") && !method.equals("HEAD")) {
          return null;
        }
      // 300 301 302 303
      case HTTP_MULT_CHOICE:
      case HTTP_MOVED_PERM:
      case HTTP_MOVED_TEMP:
      case HTTP_SEE_OTHER:
// 如果用户不允许重定向,那就返回null
if (!client.followRedirects()) return null;
// 从响应头取出location
String location = userResponse.header("Location");
if (location == null) return null;
// 根据location 配置新的请求 url
HttpUrl url = userResponse.request().url().resolve(location);
// 如果为null,说明协议有问题,取不出来HttpUrl,那就返回null,不进行重定向
if (url == null) return null;
// 如果重定向在http到https之间切换,需要检查用户是不是允许(默认允许)
boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme()); if (!sameScheme && !client.followSslRedirects()) return null;
        Request.Builder requestBuilder = userResponse.request().newBuilder();
        /**
* 重定向请求中 只要不是 PROPFIND 请求,无论是POST还是其他的方法都要改为GET请求方式, * 即只有 PROPFIND 请求才能有请求体
*/
//请求不是get与head
if (HttpMethod.permitsRequestBody(method)) {
final boolean maintainBody = HttpMethod.redirectsWithBody(method); // 除了 PROPFIND 请求之外都改成GET请求
          if (HttpMethod.redirectsToGet(method)) {
            requestBuilder.method("GET", null);
          } else {
            RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
            requestBuilder.method(method, requestBody);
}
// 不是 PROPFIND 的请求,把请求头中关于请求体的数据删掉 if (!maintainBody) {
 享学课堂

             requestBuilder.removeHeader("Transfer-Encoding");
            requestBuilder.removeHeader("Content-Length");
            requestBuilder.removeHeader("Content-Type");
} }
// 在跨主机重定向时,删除身份验证请求头
if (!sameConnection(userResponse, url)) {
          requestBuilder.removeHeader("Authorization");
        }
        return requestBuilder.url(url).build();
// 408 客户端请求超时
case HTTP_CLIENT_TIMEOUT:
// 408 算是连接失败了,所以判断用户是不是允许重试 if (!client.retryOnConnectionFailure()) {
            return null;
        }
// UnrepeatableRequestBody实际并没发现有其他地方用到
if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
            return null;
        }
// 如果是本身这次的响应就是重新请求的产物同时上一次之所以重请求还是因为408,那我们这次不再重请求 了
        if (userResponse.priorResponse() != null
                        && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
            return null;
        }
// 如果服务器告诉我们了 Retry-After 多久后重试,那框架不管了。 if (retryAfter(userResponse, 0) > 0) {
            return null;
        }
return userResponse.request();
// 503 服务不可用 和408差不多,但是只在服务器告诉你 Retry-After:0(意思就是立即重试) 才重请求 case HTTP_UNAVAILABLE:
        if (userResponse.priorResponse() != null
                        && userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
            return null;
         }
         if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
            return userResponse.request();
}
         return null;
      default:
        return null;
    }
}

其中,会发现,当重定向CODE为307和308的时候,此时并未做对应的Location跳转处理,那么真实的业务场景中可能就存在对于307/308的重定向请求,需要我们获取对应的Location地址,进行请求的重定向转发,故这里okhttp3并未帮我们实现,我们就必须通过自定义拦截器来实现对应的功能:

private Request followUpRequest(Response response) {
  int responseCode = response.code(); //获取响应状态码
  switch (responseCode) {
    case StatusCode.PERM_REDIRECT: //307
    case StatusCode.TEMP_REDIRECT: //308
      //获取对应的Location请求头,重新构建HttpURL,进行重定向处理
      String location = response.header(HeaderType.LOCATION);
      if (location == null) {
        return null;
      }
      HttpUrl url = response.request().url().resolve(location);
      if (url == null) {
        return null;
      }
      //重新构建Request
      Request.Builder requestBuilder = response.request().newBuilder();
      return requestBuilder.url(url).build();
    default:
      return null;
  }
}

坑4:域名解析异常 Name does not Resolve

项目中使用Okhttp3,出现了UnknownHostException异常,此时排查方向存在两个大的方向:

第一个方向:http1.1 支持 TCP 通道复用机制,http2.0 还支持了多路复用机制,所以有时候明明有网络,但是接口却返回 SocketTimeoutException,UnknownHostException,一般都是后台接口没有严格按照http1.1协议和http2.0协议来,导致服务器Socket关了,但是没有通知客户端,客户端下次请求,复用链路导致 SocketTimeoutException。此时我们的解决方式一般分为以下几种:

  • 服务端进行调整
  • Okhttp3关闭连接池:OkHttpClient.connectionPool().evictAll()
  • 客户端或者在Okhttp3中增加重试机制,失败了再重试几次(推荐)

第二个方向:域名解析问题,即我们服务所在的系统DNS出了问题,导致域名解析失败,这里也是存在几种解决途径:

  • 自定义实现HttpDNS,不走系统的DNS,因为本身系统自带的DNS会存在域名拦截等问题,目前主流的互联网公司都实现了自己的HttpDNS,不走服务商LocalDNS,直接通过http请求的方式进行域名解析,如果不存在再走系统的DNS解析。实现也很简单,只要实现Okhttp3的DNS接口,在lookup方法中实现自定义域名解析逻辑即可,目前主流的HttpDNS服务阿里云/腾讯云都有,可以直接去对应的产品创建对应的项目使用即可,当前你可以自己实现一个。
  • 也有的人排查问题发现说问题异常信息显示为IPV6解析问题,需要设置JVM参数优先使用IPV4进行解决;但是在我的业务场景中这样处理无法解决
-Djava.net.preferIPv4Stack=true
  • 第三种是由于服务部署时,你的容器的域名解析没有走宿主机,导致本地系统的域名解析范围太过狭窄,导致域名解析异常,我们可以通过查看etc目录下的resolv.conf进行dns配置信息的查看,其中主要就是看我们的nameserver参数是否和我们起服务的该台宿主机的IP是否保持一致即可

坑5:ResponseBody.string()无法重复获取

在使用OkhttpClient进行请求执行以后,会返回对应的响应,其中,如果我们需要知道响应体的内容,我们会调Response.ResponseBody.string(),如果我们想要知道请求体的内容,可以调Response.ResponseBody.string(),此时你会发现,ResponseBody.string()只能执行一次,再执行一次就报异常了,这是由于当我们执行ResponseBody.string()的时候,底层在finally代码块中会关闭资源:

public final String string() throws IOException {
  BufferedSource source = this.source();
  String var3;
  try {
    Charset charset = Util.bomAwareCharset(source, this.charset());
    var3 = source.readString(charset);
  } finally {
    Util.closeQuietly(source);
  }
  return var3;
}

故我们想要重复获取响应体中的内容,这里就需要通过复制流的方式来获取了:

private String getContentWithCloneResponseBodyStream(ResponseBody responseBody) throws IOException {
  BufferedSource source = responseBody.source();
  source.request(Long.MAX_VALUE);
  Buffer buffer = source.buffer();
  return buffer.clone().readString(Charset.forName(FieldConstant.UTF_8));
}

坑6:Headers.names()

如果我们想要获取响应结果中的响应头信息,就需要使用Response.Header()来获取。这里的Headers十分的神奇!!它有一个namesAndValues属性,其中当我们调用它的names()方法,就会获取一个Set集合,里面全是它的Key,此时神奇的地方来了,如果你想要把Headers放到一个Map中去,一般情况下是不是要这么写:

Map<String, String> map = new LinkedHashMap<>();
headers.names().stream().forEach(key -> {
  map.put(key, headers.get(key));
});

但是你如果要这样写,那你会发现有的时候,你的请求头会缺失,因为它的Headers的namesAndValues其实是这样的:

key1:value1
key2:value2
key2:value3
key2:value4
key3:value5
...

那么最后你的key为key2的请求头就会缺失value3和value4的属性了,所以不要被它的names()返回是Set迷惑了!!!正确的写法是:

Map<String, String> map = new LinkedHashMap<>();
headers.names().stream().forEach(key -> {
  map.put(key, String.join(";", headers.values(key)));
});

坑7:超时时间的真正意义

OkhttpClient默认超时时间分为三种:

  • connectTimeout
  • readTimeOut
  • writeTimeOut

初始不配置的情况下,单个默认是10秒,此时经常会发生读超时或者写超时的情况,故在业务场景中,建议connectTimeOut可以通过自定义配置,而readTimeOut和writeTimeOut设置得可以相对长一些,例如60S左右,避免容易发生读/写的情况。

坑8:User-Agent请求头

默认在未设置User-Agent请求头的情况下,Okhttp3会添加其系统自带的User-Agent请求头,故未避免发生校验错误,建议默认都增加你的项目中系统的User-Agent。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值