使用HttpsUrlConnection重用TCP连接

Mic*_*ael 23 java android tcp httpsurlconnection

执行摘要:HttpsUrlConnection在Android应用程序中使用该类以串行方式通过TLS发送大量请求.所有请求都是相同类型的,并发送到同一主机.首先,我会为每个请求获得一个新的TCP连接.我能够解决这个问题,但并非没有在与readTimeout相关的某些Android版本上引起其他问题.我希望有一种更强大的方法来实现TCP连接重用.


背景

在检查我正在使用Wireshark的Android应用程序的网络流量时,我发现每个请求都会导致建立新的TCP连接,并且正在执行新的TLS握手.这导致了相当大的延迟,特别是如果您使用的是3G/4G,每次往返都需要相对较长的时间.然后我尝试了没有TLS(即HttpUrlConnection)的相同场景.在这种情况下,我只看到一个TCP连接正在建立,然后重新用于后续请求.因此,建立新TCP连接的行为是特定的HttpsUrlConnection.

下面是一些示例代码来说明问题(真正的代码显然有证书验证,错误处理等):

class NullHostNameVerifier implements HostnameVerifier {
    @Override   
    public boolean verify(String hostname, SSLSession session) {
        return true;
    }
}

protected void testRequest(final String uri) {
    new AsyncTask<Void, Void, Void>() {     
        protected void onPreExecute() {
        }

        protected Void doInBackground(Void... params) {
            try {                   
                URL url = new URL("https://www.ssllabs.com/ssltest/viewMyClient.html");

                try {
                    sslContext = SSLContext.getInstance("TLS");
                    sslContext.init(null,
                        new X509TrustManager[] { new X509TrustManager() {
                            @Override
                            public void checkClientTrusted( final X509Certificate[] chain, final String authType ) {
                            }
                            @Override
                            public void checkServerTrusted( final X509Certificate[] chain, final String authType ) {
                            }
                            @Override
                            public X509Certificate[] getAcceptedIssuers() {
                                return null;
                            }
                        } },
                        new SecureRandom());
                } catch (Exception e) {

                }

                HttpsURLConnection.setDefaultHostnameVerifier(new NullHostNameVerifier());
                HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();

                conn.setSSLSocketFactory(sslContext.getSocketFactory());
                conn.setRequestMethod("GET");
                conn.setRequestProperty("User-Agent", "Android");

                // Consume the response
                BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                String line;
                StringBuffer response = new StringBuffer();
                while ((line = reader.readLine()) != null) {
                    response.append(line);
                }
                reader.close();
                conn.disconnect();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }

        protected void onPostExecute(Void result) {
        }
    }.execute();        
}
Run Code Online (Sandbox Code Playgroud)

注意:在我的实际代码中,我使用POST请求,因此我使用输出流(写入请求主体)和输入流(以读取响应主体).但我想让这个例子简短而简单.

如果我testRequest反复调用该方法,我最终会在Wireshark中删除以下内容(删节):

TCP   61047 -> 443 [SYN]
TLSv1 Client Hello
TLSv1 Server Hello
TLSv1 Certificate
TLSv1 Server Key Exchange
TLSv1 Application Data
TCP   61050 -> 443 [SYN]
TLSv1 Client Hello
TLSv1 Server Hello
TLSv1 Certificate
... and so on, for each request ...
Run Code Online (Sandbox Code Playgroud)

我是否打电话conn.disconnect对行为没有影响.

所以我最初虽然"好的,我会创建一个HttpsUrlConnection对象池,并在可能的情况下重用已建立的连接".不幸的是,没有骰子,因为Http(s)UrlConnection实际上并不意味着可以重复使用.实际上,读取响应数据会导致输出流关闭,并且尝试重新打开输出流会触发java.net.ProtocolException错误消息"cannot write request body after response has been read".

我接下来要做的就是考虑设置一个HttpsUrlConnection不同于设置一个的方式HttpUrlConnection,即你创建一个SSLContext和一个SSLSocketFactory.所以我决定制作这两个static并分享所有请求.

这似乎在我获得连接重用的意义上工作正常.但是某些Android版本存在一个问题,除了第一个请求之外的所有请求都需要很长时间才能执行.经过进一步检查,我注意到调用getOutputStream会阻塞一段时间,该时间等于设置的超时时间setReadTimeout.

我第一次尝试修复它是在setReadTimeout读完响应数据后再添加一个非常小的值,但这似乎根本没有效果.
我所做的是设置一个更短的读取超时(几百毫秒)并实现我自己的重试机制,尝试重复读取响应数据,直到所有数据都已被读取或达到最初预期的超时.
唉,现在我在某些设备上获得了TLS握手超时.所以我所做的就是setReadTimeout在调用之前添加一个具有相当大值的调用getOutputStream,然后在读取响应数据之前将读取超时更改回几百毫秒.这实际上看起来很稳固,我在8或10个不同的设备上测试它,运行不同的Android版本,并在所有这些设备上获得所需的行为.

快进几周,我决定在运行最新工厂映像(6.0.1(MMB29S))的Nexus 5上测试我的代码.现在我看到同样的问题,getOutputStream除了第一个请求之外,每个请求都会阻塞readTimeout的持续时间.

更新1:正在建立的所有TCP连接的副作用是在某些Android版本(4.1 - 4.3 IIRC)上,可能会遇到操作系统(?)中的错误,其中您的进程最终会耗尽文件描述符.这不太可能在真实条件下发生,但可以通过自动测试触发.

更新2:OpenSSLSocketImpl类有一个公共的setHandshakeTimeout,可以被用来指定握手超时即从分开readTimeout方法.但是,由于这个方法存在于套接字而不是HttpsUrlConnection调用它有点棘手.即使有可能这样做,那时你仍然依赖于可能会或可能不会因为打开而使用的类的实现细节HttpsUrlConnection.

这个问题

对我来说似乎不太可能连接重用不应该"正常工作",所以我猜我做错了.有没有人能够可靠地HttpsUrlConnection在Android上重用连接,并且可以发现我正在制造的任何错误?我真的想避免诉诸任何第三方图书馆,除非这是完全不可避免的.
请注意,您可能想到的任何想法都需要使用minSdkVersion16.

use*_*421 1

我建议您尝试重用,SSLContexts而不是每次创建一个新的并更改同上的默认值HttpURLConnection。它肯定会按照您的方式抑制连接池。

NBgetAcceptedIssuers()不允许返回 null。