在项目中对第三方服务的调用,使用了OkHttp进行http请求,这当中踩了许多坑。本篇博文将对OkHttp使用过程遇到的问题进行总结记录。
在刚开始使用时,没有将OkHttpClient单例化,造成的后果就是服务器OOM异常,can’t create native thread…
为什么会出现OOM,提示无法创建本地线程?
通过查看资料文档发现,每个client对象都有自己的线程池和连接池,如果为每个请求都创建一个client对象,自然会出现内存溢出。所以官方建议OkHttpClient应该单例化,重用连接和线程能降低延迟和减少内存消耗。
OkHttpClients should be shared
OkHttp performs best when you create a single OkHttpClient instance and reuse it for all of your HTTP calls. This is because each client holds its own connection pool and thread pools. Reusing connections and threads reduces latency and saves memory. Conversely, creating a client for each request wastes resources on idle pools.
官方介绍了三种创建client的方式:
okHttpClient = new OkHttpClient.Builder()
.connectTimeout(50L, TimeUnit.SECONDS)
.readTimeout(60L, TimeUnit.SECONDS)
.build();
You can customize a shared OkHttpClient instance with newBuilder(). This builds a client that shares the same connection pool, thread pools, and configuration. Use the builder methods to configure the derived client for a specific purpose.
OkHttpClient myClient = okHttpClient.newBuilder()
.readTimeout(80L, TimeUnit.SECONDS).build();
通过该方式解决了OkHttpClient针对不同请求设置不同参数的问题。在这篇文章中HTTP客户端连接,选择HttpClient还是OkHttp?作者对OkHttpClient和HttpClient作了性能比较,可以看出OkHttpClient的性能要好于HttpClient。 其中在非单例比较时,就单纯的建立连接耗时,OkHttpClient也优于HttpClient。
之所以使用OkHttp是因为它能高效率,高吞吐量的进行http请求,优于HttpClient。它的高效率,官方给出了四个原因:
以上可以看出,OkHttp支持HTTP/1.1和HTTP/2协议。通过这篇文章一文读懂 HTTP/1HTTP/2HTTP/3,我们可以了解下HTTP协议不同版本的特点。
通过了解可以知道,HTTP/1.1和HTTP/2的http请求都是建立在TCP连接基础之上的,所以他们的性能极大的依赖于TCP连接的配置。通过我之前的文章计算机网络,可以对TCP进行了解。
每个TCP连接都会进行三次握手,并且因为TCP的拥塞控制使用的滑动窗口和慢开始算法导致网络带宽利用率不高。所以,在HTTP/2不可用时,OkHttp使用了连接池,避免为每个请求都创建连接。
OkHttp在创建连接时,默认创建长连接,这是因为在HTTP1.1中Connection: keep-alive是默认设置的,而HTTP1.0是Connection: close, 这表示每次请求完都会关闭连接,并且OkHttp不支持HTTP1.0。所以,OkHttp使用连接池复用这些长连接。
复用长连接,避免了创建TCP连接的三次握手和慢开始,我们简单了解下HTTP请求头中Connection和Keep-Alive。
Connection作为请求头的通用参数,其控制了请求连接在当前事务完成后是否继续保持。如果其值被设置为keep-alive, 则表示该连接是长连接,允许之后的相同地址的请求复用。还可以设置为close,每次请求结束后关闭连接。但是该参数在HTTP/2中不再使用。
Keep-Alive同样作为请求头通用参数,其允许发送方设置连接的使用规则。参数timeout设置了空闲连接的最小存活时间,单位为秒(s);参数max设置了一个连接最多可以进行多少个请求。
然而在实际使用中,服务器会时不时抛出java.net.SocketException: Connection reset异常。通过这篇文章 Connection reset原因分析和解决方案,可以知道在使用OkHttp时,客户端和服务端连接应该保持一致,要么都是长连接,要么都是短连接。对此,服务端我们无法设置,只能是设置Connection为close。
并且在查资料时,碰见了两个问题:
Headers headers = firstResponse.headers();
requestBuilder = requestBuilder.addHeader("Cookie", headers.get("Set-Cookie"));
通过文档,我们可以了解OkHttp是如何建立连接的:
如果服务器地址不可达,OkHttp则会重新选择路由进行重试恢复连接。并且当响应被接收后,连接会被放回连接池等待复用,当连接空闲时长超过配置值时,则会被关闭。
通过源码,可以看到在创建OkHttpClient对象时,其通过内部静态类Builder构造器进行创建配置。在Builder类中新建了ConnectionPool对象,但该连接池没有配置连接池的大小,而是设置了最大空闲连接数maxidleConnection和存活时长keepAliveDuration。
public ConnectionPool() {
this(5, 5L, TimeUnit.MINUTES);
}
public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
....
}
那OkHttp是如何控制连接池的大小呢?在此之前,我们先了解下OkHttp的同步请求和异步请求。
请看下篇文章《OkHttp使用踩坑记录总结(二):OkHttp同步异步请求和连接池线程池》