HttpsUrlConnection and keep-alive

33

我在当前的项目中使用了com.sun.net.httpserver.HttpsServer,该项目涉及客户端认证等功能。目前它只打印客户端的地址/端口,以便我可以检查一个 TCP 连接是否用于多个请求(keep-alive),或者每个请求是否建立一个新的连接(因此每次都要进行 SSL 握手)。当我使用 Firefox 对服务器发出多个请求时,可以看到 keep-alive 工作正常。所以服务器部分对 GET 和 POST 请求工作正常。

如果我使用 HttpURLConnection 向服务器发出请求(在这种情况下不使用 SSL),keep-alive 也可以工作:只为多个连续启动的请求建立一条连接。

但如果我使用 HttpsURLConnection (使用完全相同的代码,但使用 SSL),那么 keep-alive 将不再工作。因此,每个请求都会建立一个新的连接,尽管我使用相同的 SSLContext (和 SSLSocketFactory):

// URL myUrl = ...
// SSLContext mySsl = ...
HttpsURLConnection conn = (HttpsURLConnection) myUrl.openConnection();
conn.setUseCaches(false);
conn.setSSLSocketFactory(mySsl.getSocketFactory());

conn.setRequestMethod("POST");
// send Data
// receive Data

我如何强制HttpsURLConnection使用keep-alive,因为许多请求会导致许多SSL握手,这是一个真正的性能问题?

更新(2012-04-02): 而不是每次调用mySsl.getSocketFactory(),我尝试缓存SSLSocketFactory。但是没有任何改变。问题仍然存在。


HttpsURLConnection使用一种服务提供程序来确定要使用的SSLSocketFactory实际实现。在某些上下文中运行(如应用程序服务器)时,将使用不同的实现。您在测试中使用哪个jdk版本?环境是什么类型? - Tim Jones
另外,在运行代码时尝试设置“-Djava.net.debug=ssl”。可能会提供额外的信息。 - Tim Jones
代码作为普通的Java 7应用程序在Sun的JRE/JDK上运行。我已经设置了-Djava.net.debug=ssl,但是没有发现任何问题。不过下次上班时我会再次检查。 - Biggie
8个回答

25

我遇到了完全相同的问题,并经过深入调试后终于找到了解决方案。

Http(s)UrlConnection默认处理Keep-Alive,但是套接字必须处于非常特定的状态才能被重用。

这些状态包括:

  • 输入流必须被完全消耗。你必须调用输入流的read方法直到它返回-1,并关闭它。
  • 底层套接字的设置必须使用完全相同的对象。
  • 当使用完毕时,应该调用Http(s)URLConnection的disconnect方法(是的,这很反直觉)。

上述代码中的问题是:

conn.setSSLSocketFactory(mySsl.getSocketFactory());
在初始化期间将 getSocketFactory() 的结果保存到静态变量中,然后将其传递给 conn.setSSLSocketFactory,可以允许套接字被重复使用。

最终解决了过多保持连接套接字的问题 - 必须调用断开连接! - 700 Software
@bill-healey - 你是在Android上运行这个程序吗?这两个实现(Android vs Oracle)对此的文档说明有所不同,并且行为也不同 - Android指出disconnect()可能无法摆脱资源,而Oracle则更加明确。 - Ben Cox
2
相信断开连接并不必要(甚至可能适得其反)。只需关闭输出流并消耗输入和错误流即可。(在JDK7上测试过) - mitchnull

5

我使用HttpsUrlConnection无法使其工作。但是Apache的HTTP客户端非常好地处理了带有SSL连接的持久连接。


在我看来,您需要在客户端上启用ssl会话。请参见https://dev59.com/sW035IYBdhLWcg3wErvM。但是,由于答案不足,我已经在另一个上下文中再次提出了这个问题:http://stackoverflow.com/q/15946228/194609。 - Karussell
1
HttpClient在API级别22中已被弃用。http://developer.android.com/reference/org/apache/http/client/package-summary.html - Idrizi.A
顺便说一下,我在使用HttpsUrlConnection时遇到了问题,由于Apache的HTTP客户端已经被弃用,所以我尝试了OkHttp,它完美地解决了问题。对于那些遇到麻烦的人来说,值得一试。 - Joshua Pinter

3

SSL连接的建立对于服务调用或从浏览器获取多个资源来说都非常昂贵。

Java Http(s)UrlConnection默认处理HTTP(S) Keep-Alive

我没有找到默认SSLSocketFactory的源代码,可能是在那里实现了keep-alive机制。为了确认,请使用自定义信任存储在javax.net.ssl.trustStore中禁用自己的SSLSocketFactory实现进行测试,以便接受您的自签名证书。

根据使用ServerConfigServerImpl实现的OpenJDK 7,您使用的HttpsServer默认发出一个保持活动状态的链接,超时时间为5分钟。

我建议您将服务器端的属性sun.net.httpserver.debug设置为true,以获取详细信息。

请注意,您的代码不要添加头Connection: close,这将禁用keep-alive机制。


我已经检查了连接头。它从未设置为“close”。如果存在,它将设置为“keep-alive”。当我打开httpserver-debugging时,我会收到以下消息:“java.io.IOException:引擎已关闭”。我已经使用自签名证书的自定义信任/密钥存储。目前我正在使用Apache的HttpClient,它可以很好地处理自签名客户端/服务器证书和保持活动连接(使用相同的SSLContext和代码)。但是,知道我是否在HttpsURLConnection方面做错了什么也是不错的。;) - Biggie
在我看来,它应该是可以工作的。错误来自于http://www.docjar.com/html/api/sun/net/httpserver/SSLStreams.java.html,明显是由一个被早期关闭的代码引起的。请将您的项目发布到某个地方或提供一个SSCCE以便我们进一步调查。 - Yves Martin
Java 7的try (InputStream is = ...)等语句会自动关闭HttpsUrlConnection的流,但这应该没问题:链接。它说:“当应用程序完成读取响应正文或在URLConnection.getInputStream()返回的InputStream上调用close()时,JDK的HTTP协议处理程序将尝试清理连接,如果成功,则将连接放入连接缓存中以供未来的HTTP请求重用。”目前我在家里,没有代码。 - Biggie
你也测试过Java 6了吗? - Yves Martin
我有同样的问题,我正在使用Java 6。我无法让HttpsUrlConnection类重用连接,但我可以让Apache HTTP Client库这样做。我还通过自定义SSLSocketFactory使用客户端证书。 - Ryan
Java HttpsUrlConnection 没有实现连接池,但你可以通过设置 keep-alive 并在同一连接上发送多个查询来解决这个缺陷。顺便说一下,我推荐使用 Apache HTTP 客户端。 - Yves Martin

0

我们可以设置一个Apache Web服务器,添加以下指令来查看Apache的access.log是否对http客户端有保持连接(keep-alive connection)。

LogFormat "%k %v %h %l %u %t \"%r\" %>s %b" common
CustomLog "logs/access.log" common 

http://httpd.apache.org/docs/current/mod/mod_log_config.html

"%k" 此连接处理的保持活动请求数量。如果正在使用KeepAlive,则此信息很有趣,例如,'1'表示初始请求后的第一个保持活动请求,'2'表示第二个请求,以此类推;否则,此值始终为0(表示初始请求)。


0
据我所理解,HTTP/1.1HTTPS协议,以及文档hereKeep-Alive不是端到端的头部,而是跳到跳的头部。由于SSL在每个新连接中涉及“不同跳”(例如CA和服务器)之间的多个握手步骤,我认为Keep-Alive可能不适用于SSL上下文。因此,这可能是使用HTTPS连接时忽略Keep-Alive头部的原因。根据这个问题,您可能需要确保使用一个HTTP连接实例来保证Keep-Alive观察。此外,在这个问题中,似乎Apache HTTPClient已经成为更好的解决方案。

0
除了@Bill Healey的答案之外,HostnameVerifier也必须声明为静态的。 我尝试了几种模式,有些关闭了输入流和连接,但对我来说没有任何改变。唯一重要的是所提到的属性的静态声明。
/**
SSLSocketFactory and HostnameVerifier must be declared static in order to be able to use keep-alive option
*/
private static SSLSocketFactory factory = null;
private static HostnameVerifier hostnameVerifier = new HostnameVerifier() {
    @Override
    public boolean verify(String s, SSLSession sslSession) {
        return true;
    }
};
public static void prepareForCustomTrustIfNeeded(HttpsURLConnection connection) {
        try {
            if(factory == null) {
                SSLContext sslc = SSLContext.getInstance("TLS");
                sslc.init(null, customTrustedCerts, new SecureRandom());
                factory = sslc.getSocketFactory();
            }
            connection.setSSLSocketFactory(factory);
            connection.setHostnameVerifier(hostnameVerifier);
        } catch (Exception e) {
            e.printStackTrace();
        }
}

0

我遇到了同样的问题,Bill Healey是正确的。 我使用了几个https库来测试下面的示例代码。 HttpsURLConnection和OKHTTP的行为完全相同。 Volley在会话恢复时有些不同,但几乎相同的行为。 希望这能有所帮助。

public class SampleActivity extends Activity implements OnClickListener {

    // Keep default context and factory
    private SSLContext mDefaultSslContext;
    private SSLSocketFactory mDefaultSslFactory;

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        findViewById(R.id.button_id).setOnClickListener(this);

        try {
            // Initialize context and factory
            mDefaultSslContext = SSLContext.getInstance("TLS");
            mDefaultSslContext.init(null, null, null);
            mDefaultSslFactory = mDefaultSslContext.getSocketFactory();
        } catch (NoSuchAlgorithmException | KeyManagementException e) {
            Log.e(TAG, e.getMessage(), e);
        }

    }

    @Override
    public void onClick(View v){
        SSLContext sslcontext;
        SSLSocketFactory sslfactory;

        try {
            // If using this factory, enable Keep-Alive
            sslfactory = mDefaultSslFactory;

            // If using this factory, enable session resumption (abbreviated handshake)
            sslfactory = mDefaultSslContext.getSocketFactory();

            // If using this factory, enable full handshake each time
            sslcontext = SSLContext.getInstance("TLS");
            sslcontext.init(null, null, null);
            sslfactory = sslcontext.getSocketFactory();
        } catch (NoSuchAlgorithmException | KeyManagementException e) {
            Log.e(TAG, e.getMessage(), e);
        }

        URL url = new URL("https://example.com");
        HttpsURLConnection = conn = (HttpsURLConnection) url.openConnection();
        conn.setSSLSocketFactory(sslfactory);
        conn.connect();
    }
}

更新:

共享SSLSocketFactory可以实现保持连接。 共享SSLContext并在每个请求中获取工厂可实现会话恢复。虽然不知道TLS堆栈的工作原理,但已经通过一些移动设备证实了这些连接行为。

如果想在多个类之间启用保持连接,应使用单例模式共享SSLSocketFactory实例。

如果要启用会话恢复,请确保服务器端的会话超时设置足够长,例如SSLSessionCacheTimeout(apache)、ssl_session_timeout(nginx)。


-3
尝试添加以下代码:
con.setRequestProperty("Connection", "Keep-Alive");
con.setRequestProperty("Keep-Alive", "header");

7
第一个是默认设置,第二个不存在。-1 - user207421

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接