执行摘要:我正在使用 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();
}
注意:在我的实际代码中,我使用 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 ...
是否拨打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 class有公众号
setHandshakeTimeout
可用于指定与 readTimeout 分开的握手超时的方法。但是,由于此方法适用于套接字而不是 HttpsUrlConnection
调用它有点棘手。即使有可能这样做,在这一点上,您依赖于类的实现细节,这些类可能会或可能不会因打开 HttpsUrlConnection
而被使用。 .问题
对我来说,连接重用不应该“正常工作”似乎不太可能,所以我猜我做错了什么。有没有人设法可靠地获得
HttpsUrlConnection
在 Android 上重用连接并能发现我犯的任何错误?我真的很想避免求助于任何 3rd 方库,除非这是完全不可避免的。请注意,您可能想到的任何想法都需要使用
minSdkVersion
16。
最佳答案
我建议您尝试重复使用 SSLContexts
而不是每次都创建一个新的并更改 HttpURLConnection
的默认值同上。它肯定会以您拥有的方式禁止连接池。
备注 getAcceptedIssuers()
不允许返回空值。
关于java - 使用 HttpsUrlConnection 重用 TCP 连接,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34748479/