Jersey 服务器 CLOSE_WAIT 在客户端读取超时后泄漏
Jersey server CLOSE_WAIT leak after read timeout at the client
以下是(大大简化的)代码,用于使用 Jersey
从客户端计算机到 REST
服务器进行通信。开始时建立连接有 5 分钟超时,从底层套接字读取有 2 分钟超时。
public class JerseyClient {
private final Map<Action, WebResource> resources;
public JerseyClient(URI uri) {
this.resources = new EnumMap<Action, WebResource>(Action.class);
this.resources.put(Action.Put, getClient().resource(Action.Put.getURI()));
this.resources.put(Action.Query, getClient().resource(Action.Query.getURI()));
}
private String submit(Action action, String input) throws Exception {
WebResource resource = this.resources.get(action);
ClientResponse response = null;
synchronized (resource) {
try {
response = resource.accept(MediaType.APPLICATION_JSON_TYPE,
MediaType.TEXT_PLAIN_TYPE).type(MediaType.APPLICATION_JSON_TYPE).
post(ClientResponse.class, input);
String responseString = null;
// Handle the response and produce a response string...
return responseString;
} finally {
if (response != null) {
response.close();
}
}
}
}
private static Client getClient() {
Client client = Client.create();
client.setReadTimeout(2*60*1000);
client.setConnectTimeout(5*60*1000);
return client;
}
private enum Action {
Put, Query;
public URI getURI(){
switch (this) {
case Put:
return URI.create("PUT_URI");
case Query:
return URI.create("QUERY_URI");
default:
throw new InvalidStateException("Illegal action");
}
}
}
}
以上代码按预期工作,除非在客户端触发读取超时。在那种情况下,抛出一个 SocketTimeoutException
,因此上面 JerseyClient
class 的 submit()
方法中的响应对象仍然是 null
,所以底层套接字是从未完全关闭。
显然,客户端部分关闭了套接字,因为在另一端,服务器进入 CLOSE_WAIT
状态(即,它已从客户端接收到 FIN
数据包,根据 TCP
规范)。但是,由于它永远不会从客户端获得最终的 ACK
(如果调用 response.close()
则应发送),因此它将连接保持在 CLOSE_WAIT
(如 netstat
所示) ),因此每次来自客户端的超时 REST
调用都可能在服务器上创建悬空 CLOSE_WAIT
。
有没有办法解决这个问题,而不需要完全重新设计上面的代码?
你的描述没有意义。状态的名称是 CLOSE_WAIT,而不是 CLOSED_WAIT,它的(正确的)名称表示:它正在等待 local 应用程序关闭套接字,在从对等方接收到远程关闭后。
如果您的服务器正在进入 CLOSE_WAIT:
- 客户端已经关闭了套接字。
- 服务器没有关闭套接字。这是服务器中的错误。
- 服务器永远不会从客户端获得最终的 ACK,直到它通过关闭套接字发出 FIN。 ACK 是对 FIN 的确认。在服务器发出 FIN 之前,客户端无法确认。
- 它是从 CLOSE_WAIT 中获取它的关闭,而不是客户端 ACK。
- 在客户端调用
response.close()
与客户端发送最终ACK无关。
以下是(大大简化的)代码,用于使用 Jersey
从客户端计算机到 REST
服务器进行通信。开始时建立连接有 5 分钟超时,从底层套接字读取有 2 分钟超时。
public class JerseyClient {
private final Map<Action, WebResource> resources;
public JerseyClient(URI uri) {
this.resources = new EnumMap<Action, WebResource>(Action.class);
this.resources.put(Action.Put, getClient().resource(Action.Put.getURI()));
this.resources.put(Action.Query, getClient().resource(Action.Query.getURI()));
}
private String submit(Action action, String input) throws Exception {
WebResource resource = this.resources.get(action);
ClientResponse response = null;
synchronized (resource) {
try {
response = resource.accept(MediaType.APPLICATION_JSON_TYPE,
MediaType.TEXT_PLAIN_TYPE).type(MediaType.APPLICATION_JSON_TYPE).
post(ClientResponse.class, input);
String responseString = null;
// Handle the response and produce a response string...
return responseString;
} finally {
if (response != null) {
response.close();
}
}
}
}
private static Client getClient() {
Client client = Client.create();
client.setReadTimeout(2*60*1000);
client.setConnectTimeout(5*60*1000);
return client;
}
private enum Action {
Put, Query;
public URI getURI(){
switch (this) {
case Put:
return URI.create("PUT_URI");
case Query:
return URI.create("QUERY_URI");
default:
throw new InvalidStateException("Illegal action");
}
}
}
}
以上代码按预期工作,除非在客户端触发读取超时。在那种情况下,抛出一个 SocketTimeoutException
,因此上面 JerseyClient
class 的 submit()
方法中的响应对象仍然是 null
,所以底层套接字是从未完全关闭。
显然,客户端部分关闭了套接字,因为在另一端,服务器进入 CLOSE_WAIT
状态(即,它已从客户端接收到 FIN
数据包,根据 TCP
规范)。但是,由于它永远不会从客户端获得最终的 ACK
(如果调用 response.close()
则应发送),因此它将连接保持在 CLOSE_WAIT
(如 netstat
所示) ),因此每次来自客户端的超时 REST
调用都可能在服务器上创建悬空 CLOSE_WAIT
。
有没有办法解决这个问题,而不需要完全重新设计上面的代码?
你的描述没有意义。状态的名称是 CLOSE_WAIT,而不是 CLOSED_WAIT,它的(正确的)名称表示:它正在等待 local 应用程序关闭套接字,在从对等方接收到远程关闭后。
如果您的服务器正在进入 CLOSE_WAIT:
- 客户端已经关闭了套接字。
- 服务器没有关闭套接字。这是服务器中的错误。
- 服务器永远不会从客户端获得最终的 ACK,直到它通过关闭套接字发出 FIN。 ACK 是对 FIN 的确认。在服务器发出 FIN 之前,客户端无法确认。
- 它是从 CLOSE_WAIT 中获取它的关闭,而不是客户端 ACK。
- 在客户端调用
response.close()
与客户端发送最终ACK无关。