【发布时间】:2023-03-16 08:20:01
【问题描述】:
执行摘要:我在 Android 应用程序中使用 HttpsUrlConnection 类通过 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 实例显然不应该被重用。实际上,读取响应数据会导致输出流关闭,并且尝试重新打开输出流会触发带有错误消息"cannot write request body after response has been read" 的java.net.ProtocolException。
接下来我要做的是考虑设置HttpsUrlConnection 与设置HttpUrlConnection 的不同之处,即创建SSLContext 和SSLSocketFactory。所以我决定制作这两个 static 并为所有请求共享它们。
从我获得连接重用的意义上说,这似乎工作正常。但是在某些 Android 版本上存在一个问题,除了第一个请求之外的所有请求都需要很长时间才能执行。经过进一步检查,我注意到对getOutputStream 的调用将阻塞的时间等于setReadTimeout 设置的超时时间。
我第一次尝试解决这个问题是在我读完响应数据后,用一个非常小的值向setReadTimeout 添加另一个调用,但这似乎根本没有效果。
然后我所做的是设置一个更短的读取超时(几百毫秒)并实现我自己的重试机制,尝试重复读取响应数据,直到读取所有数据或达到最初预期的超时。
唉,现在我在某些设备上遇到了 TLS 握手超时。所以我当时所做的就是在调用getOutputStream 之前添加一个具有相当大值的setReadTimeout 调用,然后在读取响应数据之前将读取超时更改回几百毫秒。这实际上看起来很可靠,我在 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 方库,除非那是完全不可避免的。
请注意,您可能想到的任何想法都需要使用 16 个 minSdkVersion。
【问题讨论】:
-
为什么不试试okHTTP实现呢?见链接square.github.io/okhttp
-
等到 Google 开始使用 OpenJDK 源代码。然后它会自动发生。
-
@EJP:也许吧,虽然我不会赌上我的生命。而且它并没有解决我眼前的问题,因为 Android N 还很遥远,有些设备永远无法升级。这也不仅仅是客户端性能不佳的问题。如果每个客户端实际上只需要 1 或 2 个连接时,它们都在建立大量连接,那么在高负载期间服务器可能会出现问题。
-
@BNK:我不希望读取永远超时。如果要读取更多响应数据但我无法在 X 时间内读取它,那么我希望它超时。我不想要的是后续
HttpsUrlConnections正在重用现有的 TCP 连接必须等待 readTimeout 的持续时间才能执行 - 即使我已经关闭了输入流和输出流以前的HttpsUrlConnection。 -
@AkashKava 请提供证据。它们都使用相同的有线协议,并且都受网络限制并受到服务器速度的限制。没有理由为什么一个库会比另一个库快得多。另请提供您的“不推荐”来源。由谁?
标签: java android tcp httpsurlconnection