-
现象
服务器访问微信的时候,零零散散会报微信服务器443端口无响应的错误。异常如下
java.lang.RuntimeException: org.apache.http.NoHttpResponseException: api.weixin.qq.com:443 failed to respond
at me.chanjar.weixin.mp.api.impl.AbstractWxMpServiceImpl.executeInternal(AbstractWxMpServiceImpl.java:314)
at me.chanjar.weixin.mp.api.impl.AbstractWxMpServiceImpl.execute(AbstractWxMpServiceImpl.java:251)
at me.chanjar.weixin.mp.api.impl.AbstractWxMpServiceImpl.post(AbstractWxMpServiceImpl.java:241
at me.chanjar.weixin.mp.api.impl.WxMpTemplateMsgServiceImpl.sendTemplateMsg(WxMpTemplateMsgServiceImpl.java:34)
org.apache.http.NoHttpResponseException: api.weixin.qq.com:443 failed to respond
at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:143)
at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:57)
-
原因:
在网上看了很多文章,也和公司内部的其他团队碰了一下,确定原因是是因为微信端的http keepalive时长是20s,当一次http请求完成后,假设下次使用这个连接正好是在20s,微信端其实已经抛弃了这个连接,如果继续使用这个连接发送请求,就会报443的错误。【属于服务端主动抛弃链接】。
当然,有人会问下次使用这个连接的时间超过20s,是不是也会报这个错呢? 答案是,不一定。后面看源码的时候,会说详细的原因。
-
解决方案:
目标是希望在连接池中的连接被复用的时候,微信端还没有关闭这个连接。当然,指望微信等我们是不靠谱的。我们能做的其实是,在客户端使用连接的时候进行检查,如果连接保持的时间快到20s了,我们就抛弃掉这个连接。不用了,咱们用新连接,微信挑不出理了吧。 因此,我们需要修改http参数来约定keep_alive的时间:
/**
* 传输超时毫秒
*/
private final static int SOCKET_TIMEOUT = 15000;
/**
* 对连接池进行设置
*/
connManager.setDefaultSocketConfig(socketConfig);
RequestConfig requestConfig = RequestConfig.custom().setConnectionRequestTimeout(REQUEST_CONNECT_TIMEOUT)
.setConnectTimeout(CONNECT_TIMEOUT).setSocketTimeout(SOCKET_TIMEOUT).build();
网上有些朋友遇到这个问题后说升级了httpClient版本就好了,不知道是不是因为升级以后socketTime的时间落在了20s以前就恰好解决了这个问题。
高版本的HttpClient还有一个检查空闲连接的机制,同样建议设置成15s。不好的地方是会增加检查的频次。就像老版本的isStaleConnectionCheckEnabled()。
connManager = new PoolingHttpClientConnectionManager(registry);
// 可用空闲连接过期时间,重用空闲连接时会先检查是否空闲时间超过这个时间,如果超过,释放socket重新建立
connManager.setValidateAfterInactivity(15000);
-
一些背景知识和源码:
1: http协议建立和销毁都有代价【握手】。
2: 为了减少这种代价,出现了连接池。undertow/tomcat这种web容器都有连接池。 http连接池减少代价的方式为连接请求以后不销毁,下次可以复用。
3:为了保证复用的连接是有效的,不会过期,http在从连接池拿到连接以后会校验对象本身[CPoolProxy]和socket链路都是没问题的,才会复用。过期的【stale】或者是socket不通的连接,客户端是会抛弃掉旧连接,建立新的。
我们通过源码看一下这几个过程:
- 从http连接池拿出连接
- 检验有效性
- 发起请求
-
http使用连接池建立连接
httpClient是通过一系列的executor组成的request executor chain发起连接的。
我们要看的这部分代码在最后一个executor:MainClientExec中。省略无关代码。
//1 : 创建一个ConnectionRequest,并将获取连接的操作封装在ConnectionRequest中。
final ConnectionRequest connRequest = connManager.requestConnection(route, userToken);
// 2: 连接HttpClientConnection对象 最后发请求execute的参数
final HttpClientConnection managedConn;
try {
final int timeout = config.getConnectionRequestTimeout();
managedConn = connRequest.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS);
} catch(final InterruptedException interrupted) {
Thread.currentThread().interrupt();
throw new RequestAbortedException("Request aborted", interrupted);
} catch(final ExecutionException ex) {
Throwable cause = ex.getCause();
if (cause == null) {
cause = ex;
}
throw new RequestAbortedException("Request execution failed", cause);
}
//3: 检查是否过期 stale==过期 新版本httpClient默认关闭
if (config.isStaleConnectionCheckEnabled()) {
// validate connection
if (managedConn.isOpen()) {
this.log.debug("Stale connection check");
if (managedConn.isStale()) {
this.log.debug("Stale connection detected");
managedConn.close();
}
}
}
final ConnectionHolder connHolder = new ConnectionHolder(this.log, this.connManager, managedConn);
//4: 如果连接不可用 重新建立连接 establishRoute
if (!managedConn.isOpen()) {
this.log.debug("Opening connection " + route);
try {
establishRoute(proxyAuthState, managedConn, route, request, context);
} catch (final TunnelRefusedException ex) {
if (this.log.isDebugEnabled()) {
this.log.debug(ex.getMessage());
}
response = ex.getResponse();
break;
}
}
//5: 为连接设置soTimeOut
final int timeout = config.getSocketTimeout();
if (timeout >= 0) {
managedConn.setSocketTimeout(timeout);
}
//6: 发起http请求
response = requestExecutor.execute(request, managedConn, context);
-
从连接池中拿出连接并校验连接的有效性
判断有效性需要判断两步。首先判断连接本身是否有效【CPoolEntry】,其次是连接本身是否能通。
- 检查连接本身是否有效
try {
//每一个route都有一个连接池,这里获取指定route的连接池
final RouteSpecificPool<T, C, E> pool = getPool(route);
E entry = null;
//循环取,直到超时
while (entry == null) {
Asserts.check(!this.isShutDown, "Connection pool shut down");
for (;;) {
//从连接池中去一个空闲的连接,优先取state相同的。state默认是null
entry = pool.getFree(state);
//如果没有符合的连接,则调出,创建一个新连接
if (entry == null) {
break;
}
//如果连接超时,则关闭
if (entry.isExpired(System.currentTimeMillis())) {
entry.close();
//如果是永久连接,且最近周期内没有检验,则校验连接是否可用。不可用的连接需要关闭
} else if (this.validateAfterInactivity > 0) {
if (entry.getUpdated() + this.validateAfterInactivity <= System.currentTimeMillis()) {
if (!validate(entry)) {
entry.close();
}
}
}
//如果连接已经关闭了,则释放掉,继续从池子中取符合条件的连接
if (entry.isClosed()) {
this.available.remove(entry);
pool.free(entry, false);
} else {
break;
}
}
//entry不为空,则修改连接池的参数,并返回。
if (entry != null) {
this.available.remove(entry);
this.leased.add(entry);
onReuse(entry);
return entry;
}
// New connection is needed
//获取池子的最大连接数,如果池子已经超过容量了,需要把超过的资源回收
//如果池子中连接数没有超,空闲的连接还比较多,就先从别人的池子里借一个来用
......
//不能借,就自己动手了。新建并返回。
final C conn = this.connFactory.create(route);
entry = pool.add(conn);
this.leased.add(entry);
return entry;
}
throw new TimeoutException("Timeout waiting for connection");
} finally {
this.lock.unlock();
}
}
- 检查连接是否能通
//org.apache.http.impl.conn.CPool
protected boolean validate(final CPoolEntry entry) {
return !entry.getConnection().isStale();
}
//org.apache.http.impl.BHttpConnectionBase
//判断连接是否不可用(go down)
public boolean isStale() {
//没有打开,即socket为空,则不可用
if (!isOpen()) {
return true;
}
try {
//socket链路有了,测试链路是否可用
//这里的测试方法是查看很短的时间内(这里是1ms),是否可以从输入流中读到数据
//如果测试结果返回-1说明不可用
final int bytesRead = fillInputBuffer(1);
return bytesRead < 0;
} catch (final SocketTimeoutException ex) {
//注意这里SocketTimeoutException时,认为是可用的
return false;
} catch (final IOException ex) {
//有I/O异常,不可用
return true;
}
}
- 发起请求
response = requestExecutor.execute(request, managedConn, context);
-
参考: