重用TCP连接与HttpsUrlConnection

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


背景

在检查我正在使用Wireshark的Android应用程序的networkingstream量时,发现每个请求都会导致build立新的TCP连接,并执行新的TLS握手。 这会导致相当长的延迟时间,特别是在3G / 4G的情况下,每次往返都需要相当长的时间。 然后我没有TLS(即HttpUrlConnection )尝试相同的scheme。 在这种情况下,我只看到一个单独的TCP连接正在build立,然后重新用于后续的请求。 所以build立新的TCP连接的行为是特定于HttpsUrlConnection

下面是一些示例代码来说明问题(真正的代码显然有证书validation,error handling等):

 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(); } 

注意:在我真正的代码中,我使用POST请求,所以我使用输出stream(写请求主体)和inputstream(读取响应主体)。 但是我想保持这个例子简单而简单。

如果我反复调用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 ... 

我是否调用conn.disconnect对行为没有影响。

所以我最初虽然“好吧,我会创build一个HttpsUrlConnection对象池,并尽可能重用build立的连接”。 不幸的是,没有骰子,因为Http(s)UrlConnection实例显然不意味着被重用。 事实上,读取响应数据会导致输出stream被closures,尝试重新打开输出stream会触发java.net.ProtocolException并显示错误消息"cannot write request body after response has been read"

接下来我要做的是考虑设置HttpsUrlConnection与设置HttpsUrlConnection不同,即创build一个SSLContext和一个SSLSocketFactory 。 所以我决定让这两个static和共享他们的所有请求。

这似乎工作得很好,因为我有连接重用。 但是在某些Android版本中,除了第一个请求之外的所有请求都需要很长时间才能执行。 经过进一步的检查,我发现getOutputStream的调用会阻塞一段时间,等于setReadTimeout设置的超时时间。

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

快进了几个星期,我决定在运行最新工厂映像(6.0.1(MMB29S))的Nexus 5上testing我的代码。 现在我看到了同样的问题, getOutputStream将在除了第一个请求之外的每个请求的readTimeout期间阻塞。

更新1:正在build立的所有TCP连接的副作用是,在某些Android版本(4.1-4.3 IIRC)中,可能会遇到操作系统(?)中的错误,您的进程最终将耗尽文件描述符。 这在现实世界中不太可能发生,但是可以通过自动testing来触发。

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

这个问题

对我来说连接重用不应该“正常工作”似乎是不可能的,所以我猜测我做错了什么。 有没有人设法可靠地让HttpsUrlConnection重用Android上的连接,并能发现我正在犯的任何错误? 我真的想避免诉诸任何第三方图书馆,除非这是完全不可避免的。
请注意,无论您想到什么想法,都需要使用16的minSdkVersion

Solutions Collecting From Web of "重用TCP连接与HttpsUrlConnection"