itmale 2019-12-21
背景:
需要去监控某个网站,所以写了一个爬虫程序,被爬取的链接是Https,使得的是网上的代理,按ip使用量计费,该计费模式确实好用!
框架:httpClient 4.5.10
Java: Java 9
implementation ‘org.apache.httpcomponents:httpclient:4.5.10‘
问题:
然后问题出现了,因为是一个监控程序,所以需要不断的轮询,然后开了10个左右线程轮询,结果跑了半小时后,10个线程全部刮起,thread dump一下发现每个线程都如下:
"" daemon prio=5 tid=0x2a nid=NA runnable java.lang.Thread.State: RUNNABLE at java.net.SocketInputStream.socketRead0(SocketInputStream.java:-1) at java.net.SocketInputStream.socketRead(SocketInputStream.java:116) at java.net.SocketInputStream.read(SocketInputStream.java:171) at java.net.SocketInputStream.read(SocketInputStream.java:141) at sun.security.ssl.InputRecord.readFully(InputRecord.java:465) at sun.security.ssl.InputRecord.read(InputRecord.java:503) at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:975) - locked <0x1c8d> (a java.lang.Object) at sun.security.ssl.SSLSocketImpl.readDataRecord(SSLSocketImpl.java:933) at sun.security.ssl.AppInputStream.read(AppInputStream.java:105) - locked <0x1c8e> (a sun.security.ssl.AppInputStream) at org.apache.http.impl.io.SessionInputBufferImpl.streamRead(SessionInputBufferImpl.java:137) at org.apache.http.impl.io.SessionInputBufferImpl.fillBuffer(SessionInputBufferImpl.java:153) at org.apache.http.impl.io.SessionInputBufferImpl.readLine(SessionInputBufferImpl.java:280) at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:138) at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:56)
WTF?一直卡在 java.net.SocketInputStream.socketRead0 方法上,难道是我没有设置超时逻辑?于是赶紧去查找代码,加上逻辑:
HttpClient httpClient = HttpClientBuilder.create() .setConnectionManager(connManager) .setConnectionTimeToLive(20000L, TimeUnit.MILLISECONDS) .setRetryHandler(getRetryHandler()) .setDefaultCredentialsProvider(getProxyProvider()) .build();
自信的跑了一段时间后,无用还是挂着,于是上网看看,发现很多人都遇到过这种事,但没人说明是为啥?有几种可能:
解决:
因为是用的是框架,对框架也不熟,所以直觉告诉我需要先考虑是框架的问题,所以开始对httpClient的使用开始研究,太恶心了,这个框架每个版本的使用方法都不太一样;最终我发现两种设置超时的方法,一个是掌控应用层的RequestConfig,第二种是掌控Socket的SocketConfig,这个时候我已经猜到了问题原因,这次信心慢慢,于是我加入了以下代码,发现问题神奇的解决了。
BasicHttpClientConnectionManager connManager = new BasicHttpClientConnectionManager(); connManager.setSocketConfig(SocketConfig.custom().setSoTimeout(2000).build()); HttpClient httpClient = HttpClientBuilder.create() .setConnectionManager(connManager) .setConnectionTimeToLive(20000L, TimeUnit.MILLISECONDS) .setRetryHandler(getRetryHandler()) .setDefaultCredentialsProvider(getProxyProvider()) .build();
分析:
抱着知其所以然的心态,为什么还需要加上这个配置呢?我开始深思研究他们的源码:
创建一个 HttpClient 实例,这个实例需要调用 Dispose 方法释放资源,这里使用了 using 语句。接着调用 GetAsync,给它传递要调用的方法的地址,向服务器发送 Get 请求。