你以为的timeout,不⼀定是⽤户的timeout[转]
你以为的timeout,不⼀定是⽤户的timeout
⼩楼⼀夜听春⾬
61 ⼈赞了该⽂章
引⾔
最近在协助业务团队解决⼀些疑难问题,其中有⼀个就是有些⽤户反馈在进⾏某个特定的操作时,偶尔会遇到加载很久的情况,就好像是timeout不起作⽤⼀样,但是业务开发的同学明明将⽹络请求的timeout设置为30s,这是为什么呢?难道是okhttp有bug?还是说⽤户操作不当?
最终花费了3天时间,终于找到了问题的根因。
先说⼀下关键字: okio, 超时机制, 弱⽹,关键参数
1.确认问题
由于产品经理收集到的⽤户反馈⽐较模糊,为了准确定位问题存在,就需要拿数据说话,于是查看这个
请求的埋点数据,发现确实有⼏⼗个⽤户在这个请求上花费的时间超过30s,有些甚⾄达到了90s,这样的体验就⾮常差了。
那会不会是业务的童鞋在初始化OkHttpClient时timeout设置错误了呢,于是查看初始化代码,如下:
OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder()
.readTimeout(30, TimeUnit.SECONDS)
.connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.addInterceptor(new HeaderInterceptor())
显然,三个timeout值都设置成了30s,并没有问题。这样的话只能怀疑是okhttp有bug或者我们对于okhttp的使⽤不当了。
2.okhttp源码中timeout调⽤
在创建OkHttpClient时设置的timeout,会在何时使⽤呢?
readTimeout,connectTimeout和writeTimeout的使⽤有两个地⽅,⼀个是StreamAllocation,⼀个是在Http2Codec中,由于我们这个请求是http 1.1协议,所以Http2Codec就不⽤看了。
2.1 参数传递
在StreamAllocation中的newStream()⽅法中,timeout的使⽤如下:
public HttpCodec newStream(OkHttpClient client, boolean doExtensiveHealthChecks) {
int connectTimeout = tTimeoutMillis();
int readTimeout = adTimeoutMillis();
int writeTimeout = client.writeTimeoutMillis();
boolean connectionRetryEnabled = OnConnectionFailure();
try {
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);
HttpCodec resultCodec;
if (resultConnection.http2Connection != null) {
resultCodec = new Http2Codec(client, this, resultConnection.http2Connection);
} el {
resultConnection.socket().tSoTimeout(readTimeout);
resultConnection.source.timeout().timeout(readTimeout, MILLISECONDS);
resultConnection.sink.timeout().timeout(writeTimeout, MILLISECONDS);
resultCodec = new Http1Codec(
client, this, resultConnection.source, resultConnection.sink);
}
synchronized (connectionPool) {
codec = resultCodec;
return resultCodec;
}
} catch (IOException e) {
throw new RouteException(e);
}
}
可以看到这三个timeout都⽤于与连接有关的参数设置中,⾸先看findHealthyConnection()⽅法:
/**
* Finds a connection and returns it if it is healthy. If it is unhealthy the process is repeated
* until a healthy connection is found.
*/
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)
throws IOException {
while (true) {
RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
connectionRetryEnabled);
// If this is a brand new connection, we can skip the extensive health checks.
synchronized (connectionPool) {
if (candidate.successCount == 0) {
return candidate;
}
}
// Do a (potentially slow) check to confirm that the pooled connection is still good. If it
// isn't, take it out of the pool and start again.
if (!candidate.isHealthy(doExtensiveHealthChecks)) {
noNewStreams();
continue;
}
return candidate;
}
}
发现这个⽅法主要就是会循环调⽤findConnection()直到找到⼀个健康的连接,⽽findConnection()如下:
/**
* Returns a connection to host a new stream. This prefers the existing connection if it exists,
* then the pool, finally building a new connection.
*/
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
boolean connectionRetryEnabled) throws IOException {
Route lectedRoute;
synchronized (connectionPool) {
if (relead) throw new IllegalStateException("relead");
if (codec != null) throw new IllegalStateException("codec != null");
if (canceled) throw new IOException("Canceled");
RealConnection allocatedConnection = tion;
if (allocatedConnection != null && !NewStreams) {
return allocatedConnection;
}
// Attempt to get a connection from the pool.
RealConnection pooledConnection = (connectionPool, address, this);
if (pooledConnection != null) {
return pooledConnection;
}
lectedRoute = route;
}
if (lectedRoute == null) {
lectedRoute = ();
synchronized (connectionPool) {
route = lectedRoute;
refudStreamCount = 0;
}
}
RealConnection newConnection = new RealConnection(lectedRoute);
synchronized (connectionPool) {
acquire(newConnection);
Internal.instance.put(connectionPool, newConnection);
if (canceled) throw new IOException("Canceled");
}
routeDataba().ute());
return newConnection;
}
可以发现,就是在调⽤RealConnection的connect()⽅法时⽤到了三个timeout,该⽅法如下:
public void connect(int connectTimeout, int readTimeout, int writeTimeout,
List<ConnectionSpec> connectionSpecs, boolean connectionRetryEnabled) {
if (protocol != null) throw new IllegalStateException("already connected");
RouteException routeException = null;
ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
if (route.address().sslSocketFactory() == null) {
if (!ains(ConnectionSpec.CLEARTEXT)) {
throw new RouteException(new UnknownServiceException(
"CLEARTEXT communication not enabled for client"));
}
String host = route.address().url().host();
if (!().isCleartextTrafficPermitted(host)) {
throw new RouteException(new UnknownServiceException(
"CLEARTEXT communication to " + host + " not permitted by network curity policy"));
}
}
while (protocol == null) {
try {
if (quiresTunnel()) {
buildTunneledConnection(connectTimeout, readTimeout, writeTimeout,
connectionSpecSelector);
} el {
buildConnection(connectTimeout, readTimeout, writeTimeout, connectionSpecSelector);
}
} catch (IOException e) {
cloQuietly(socket);
cloQuietly(rawSocket);
socket = null;
rawSocket = null;
source = null;
sink = null;
handshake = null;
protocol = null;
if (routeException == null) {
routeException = new RouteException(e);
} el {
routeException.addConnectException(e);
}
if (!connectionRetryEnabled || !tionFailed(e)) {
throw routeException;
}
}
}
}
不需要⾛代理时,调⽤到buildConnection()⽅法:
/
** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
private void buildConnection(int connectTimeout, int readTimeout, int writeTimeout,
ConnectionSpecSelector connectionSpecSelector) throws IOException {
connectSocket(connectTimeout, readTimeout);
establishProtocol(readTimeout, writeTimeout, connectionSpecSelector);
}
这⾥就开始分开了,其中connectTimeout和readTimeout⽤于socket连接,⽽readTimeout和writeTimeout则是⽤于与http 2有关的设置。
2.2 connectSocket()分析
先看connectSocket()⽅法:
private void connectSocket(int connectTimeout, int readTimeout) throws IOException {
Proxy proxy = route.proxy();
Address address = route.address();
rawSocket = pe() == Proxy.Type.DIRECT || pe() == Proxy.Type.HTTP
address.socketFactory().createSocket()
: new Socket(proxy);
rawSocket.tSoTimeout(readTimeout);
try {
<().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
} catch (ConnectException e) {
ConnectException ce = new ConnectException("Failed to connect to " + route.socketAddress());
ce.initCau(e);
throw ce;
}
source = Okio.buffer(Okio.source(rawSocket));
sink = Okio.buffer(Okio.sink(rawSocket));
}
可以看到:
readTimeout最终被⽤于rawSocket.tSoTimeout(),⽽tSoTimeout()的作⽤是在建⽴连接之后,对于InputStream进⾏read()操作时的时间限制,所以这⾥采⽤readTimeout
connectTimeout则会最终根据不同的平台进⾏设置,在Android系统上最终会调⽤AndroidPlatform的connectSocket()⽅法,如下:
@Override public void connectSocket(Socket socket, InetSocketAddress address,
int connectTimeout) throws IOException {
try {
} catch (AsrtionError e) {
if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
throw e;
} catch (SecurityException e) {
// Before android 4.3, t could throw a SecurityException
// if opening a socket resulted in an EACCES error.
IOException ioException = new IOException("Exception in connect");
ioException.initCau(e);
throw ioException;
}
}
可见这⾥就是为socket设置连接超时,所以是使⽤connectTimeout.
2.3 establishProtocol()分析
再回到RealConnection的buildConnection()⽅法中,在调⽤完connectSocket()之后,就调⽤了establishProtocol()⽅法了: