HttpURLConnection 的 disconnect 和 Keep-Alive

发布于 2022-09-22 22:47:00 字数 19957 浏览 5 评论 0

缘起

在看 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,找到一篇 日文博客,讲得非常详细,我也做了一下试验,发现调用 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 生效时,也不影响连接的重用。

https://www.wenjiangs.com/wp-content/uploads/2022/09/httpkeepalive.zip

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

列表为空,暂无数据

关于作者

0 文章
0 评论
0 人气
更多

推荐作者

淹不死的鱼

文章 0 评论 0

zhangMack

文章 0 评论 0

爱的故事

文章 0 评论 0

linces

文章 0 评论 0

早乙女

文章 0 评论 0

鸵鸟症

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击“接受”或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。