在看 apollo 客户端的时候,里面有一个实现类 HttpUtil.java,看到 HttpURLConnection 在创建使用后,并没有调用 disconnect 方法去关闭连接,根据说明,是为了 keep-alive 保持会话。
private <T> HttpResponse<T> doGetWithSerializeFunction(HttpRequest httpRequest,
Function<String, T> serializeFunction) {
InputStreamReader isr = null;
InputStreamReader esr = null;
int statusCode;
try {
HttpURLConnection conn = (HttpURLConnection) new URL(httpRequest.getUrl()).openConnection();
conn.setRequestMethod("GET");
int connectTimeout = httpRequest.getConnectTimeout();
if (connectTimeout < 0) {
connectTimeout = m_configUtil.getConnectTimeout();
}
int readTimeout = httpRequest.getReadTimeout();
if (readTimeout < 0) {
readTimeout = m_configUtil.getReadTimeout();
}
conn.setConnectTimeout(connectTimeout);
conn.setReadTimeout(readTimeout);
conn.connect();
statusCode = conn.getResponseCode();
String response;
try {
isr = new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8);
response = CharStreams.toString(isr);
} catch (IOException ex) {
/**
* according to https://docs.oracle.com/javase/7/docs/technotes/guides/net/http-keepalive.html,
* we should clean up the connection by reading the response body so that the connection
* could be reused.
*/
InputStream errorStream = conn.getErrorStream();
if (errorStream != null) {
esr = new InputStreamReader(errorStream, StandardCharsets.UTF_8);
try {
CharStreams.toString(esr);
} catch (IOException ioe) {
//ignore
}
}
// 200 and 304 should not trigger IOException, thus we must throw the original exception out
if (statusCode == 200 || statusCode == 304) {
throw ex;
} else {
// for status codes like 404, IOException is expected when calling conn.getInputStream()
throw new ApolloConfigStatusCodeException(statusCode, ex);
}
}
if (statusCode == 200) {
return new HttpResponse<>(statusCode, serializeFunction.apply(response));
}
if (statusCode == 304) {
return new HttpResponse<>(statusCode, null);
}
} catch (ApolloConfigStatusCodeException ex) {
throw ex;
} catch (Throwable ex) {
throw new ApolloConfigException("Could not complete get operation", ex);
} finally {
if (isr != null) {
try {
isr.close();
} catch (IOException ex) {
// ignore
}
}
if (esr != null) {
try {
esr.close();
} catch (IOException ex) {
// ignore
}
}
}
throw new ApolloConfigStatusCodeException(statusCode,
String.format("Get operation failed for %s", httpRequest.getUrl()));
}
然后我就比较纳闷了,我之前所有的用法,都是会调用 disconnect 的,这不调用 disconnect 就可以 keep-alive 会话保持了么。
于是 ,就谷歌搜索一下 HttpURLConnection disconnect keep-alive,找到一篇 日文博客(link:https://kazuhira-r.hatenablog.com/entry/20171125/1511601958),讲得非常详细,我也做了一下试验,发现调用 disconnect,并不影响 keep-alive。
看不懂日文,没关系,谷歌翻译 一下就好,建议翻译成英文,翻译质量比较好。
照着博客做一遍,先准备好服务端代码 simple-httpd.groovy
import java.nio.charset.StandardCharsets
import java.time.LocalDateTime
import java.util.concurrent.Executors
import com.sun.net.httpserver.HttpHandler
import com.sun.net.httpserver.HttpServer
def responseHandler = { exchange ->
println("[${LocalDateTime.now()}] Accept: Client[$exchange.remoteAddress], Method[${exchange.requestMethod}] Url[$exchange.requestURI]")
try {
exchange.responseHeaders.with {
add("Content-Type", "text/plain; charset=UTF-8")
if (!exchange.requestHeaders['Connection'].contains('keep-alive')) {
add("Connection", "close")
}
}
def bodyText = "Hello Simple Httpd!!\n".stripMargin().getBytes(StandardCharsets.UTF_8)
exchange.sendResponseHeaders(200, bodyText.size())
exchange.responseBody.withStream { it.write(bodyText) }
} catch (e) {
e.printStackTrace()
}
}
def server =
HttpServer.create(new InetSocketAddress(args.length > 0 ? args[0].toInteger() : 8080), 0)
server.executor = Executors.newCachedThreadPool()
server.createContext("/", responseHandler as HttpHandler)
server.start()
println("[${LocalDateTime.now()}] SimpleJdkHttpd Startup[${server.address}]")
然后运行命令 groovy simple-httpd.groovy,就在 8080 端口启动好了服务了,下面测试。
先准备好 maven 的 pom.xml。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.xyz</groupId>
<artifactId>httpkeepalive</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.0.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.8.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.19</version>
<dependencies>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-surefire-provider</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.0.2</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
然后准备好代码
package cn.xyz.httpkeepalive;
import lombok.Cleanup;
import lombok.SneakyThrows;
import lombok.val;
import org.junit.jupiter.api.Test;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
public class HttpUrlConnectionTest {
private void doGet(String url) {
doGet(url, new HashMap<>());
}
@SneakyThrows
private void doGet(String url, Map<String, String> headers) {
@Cleanup("disconnect") val conn = (HttpURLConnection) new URL(url).openConnection();
for (val entry : headers.entrySet()) {
conn.setRequestProperty(entry.getKey(), entry.getValue());
}
conn.setDoInput(true);
@Cleanup val is = conn.getInputStream();
@Cleanup val reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
assertThat(reader.readLine()).isEqualTo("Hello Simple Httpd!!");
}
@Test
public void simpleUsage() {
doGet("http://localhost:8080");
}
@Test
public void request100() {
for (int i = 0; i < 100; i++) {
doGet("http://localhost:8080");
}
}
@Test
public void request100DisableKeepAlive1() {
for (int i = 0; i < 100; i++) {
doGet("http://localhost:8080", of("Connection", "close"));
}
}
@Test
public void request100DisableKeepAlive2() {
System.setProperty("http.keepAlive", "false");
for (int i = 0; i < 100; i++) {
doGet("http://localhost:8080");
}
}
@Test
public void request100SeparateUrl() {
for (int i = 0; i < 100; i++) {
doGet("http://localhost:8080/" + i);
}
}
private static Map<String, String> of(String key, String value) {
Map<String, String> map = new HashMap<>(1);
map.put(key, value);
return map;
}
}
1、先跑 simpleUsage 用例,跑单次连接,然后用命令 ss - an | grep 8080,mac 版本是 netstat -p tcp -van|grep 8080,可以看到创建了一个 tcp 连接,在 TIME_WAIT 状态,稍等一会再运行同样命令,会发现 TIME_WAIT 状态的连接已经消失了。
bingoobjca@bogon ~> netstat -p tcp -van|grep 8080
tcp46 0 0 *.8080 *.* LISTEN 131072 131072 62332 0 0x0100 0x00000006
tcp4 0 0 127.0.0.1.61090 127.0.0.1.8080 TIME_WAIT 408162 146988 62373 0 0x2031 0x00000000
2、再跑 request100 用例,连续请求 100 次。发现依然只有一个 tcp 连接,说明 keep-alive 生效了。
bingoobjca@bogon ~> netstat -p tcp -van|grep 8080
tcp46 0 0 *.8080 *.* LISTEN 131072 131072 62332 0 0x0100 0x00000006
tcp4 0 0 127.0.0.1.61566 127.0.0.1.8080 TIME_WAIT 394500 146988 62405 0 0x2031 0x00000000
3、再跑 request100DisableKeepAlive1,或者 request100DisableKeepAlive2,主动关闭 keep-alive,看网络状态,就会发现多了 TIME_WAIT 的连接 ,说明 keep-alive 没有生效。
bingoobjca@bogon ~> netstat -p tcp -van|grep 8080 |wc -l
103
bingoobjca@bogon ~> netstat -p tcp -van|grep 8080
tcp46 0 0 *.8080 *.* LISTEN 131072 131072 62332 0 0x0100 0x00000006
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62465 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62466 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62467 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62468 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62469 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62470 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62471 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62472 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62473 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62474 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62475 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62476 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62477 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62478 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62479 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62480 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62481 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62482 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62483 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62484 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62485 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62486 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62487 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62488 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62489 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62490 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62491 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62492 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62493 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62494 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62495 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62496 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62497 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62498 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62499 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62500 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62501 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62502 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62503 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62504 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62505 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62506 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62507 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62508 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62509 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62510 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62511 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62512 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62513 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62514 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62515 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62516 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62517 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.62517 127.0.0.1.8080 TIME_WAIT 408143 146988 62479 0 0x2031 0x00000000
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62518 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62519 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62520 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62521 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.62521 127.0.0.1.8080 TIME_WAIT 408143 146988 62479 0 0x2031 0x00000000
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62522 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
tcp4 0 0 127.0.0.1.8080 127.0.0.1.62523 TIME_WAIT 408151 146988 62332 0 0x2031 0x00000004
4、再跑 request100SeparateUrl 用例,每次改变 url,发现 keep-alive 依然生效,并不因为 url 的 path 部分变化而变化。
从相关源代码(HttpClient, KeepAliveCache, HttpURLConnection)来看,disconnect 只会释放空余的连接,而 keep-alive 的连接,会放到 KeepAliveCache 的缓存中,并且默认有5秒的缓存失效时间。因此,为了保持一致性,建议还是调用 disconnect,这样如果 keep-alive 不生效时,能及时释放链接 ,keep-alive 生效时,也不影响连接的重用。

