执行摘要:我在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
对行为没有影响。
所以我最初虽然&#34;好的,我会创建一个HttpsUrlConnection
个对象池,并在可能的情况下重用已建立的连接&#34;。不幸的是,没有骰子,因为Http(s)UrlConnection
个实例显然并不意味着可以重复使用。实际上,读取响应数据会导致输出流关闭,并且尝试重新打开输出流会触发java.net.ProtocolException
,并显示错误消息"cannot write request body after response has been read"
。
我接下来要做的是考虑设置HttpsUrlConnection
与设置HttpUrlConnection
的方式不同,即您创建SSLContext
和{{1} }。所以我决定制作这两个SSLSocketFactory
并分享所有请求。
这似乎在我获得连接重用的意义上工作正常。但是某些Android版本存在一个问题,除了第一个请求之外的所有请求都需要很长时间才能执行。在进一步检查后,我注意到对static
的调用会阻塞一段时间,该时间等于getOutputStream
设置的超时时间。
我第一次尝试解决这个问题,是在我读完回复数据之后,以非常小的值添加另一个setReadTimeout
来电,但这似乎根本没有效果。
我所做的是设置一个更短的读取超时(几百毫秒)并实现我自己的重试机制,尝试重复读取响应数据,直到所有数据都已被读取或达到最初预期的超时。
唉,现在我在某些设备上获得了TLS握手超时。所以我所做的就是在调用setReadTimeout
之前添加一个具有相当大值的setReadTimeout
调用,然后在读取响应数据之前将读取超时更改回几百毫秒。这实际上看起来很稳固,我在8或10个不同的设备上测试它,运行不同的Android版本,并在所有这些设备上获得所需的行为。
快进几周,我决定在运行最新工厂映像(6.0.1(MMB29S))的Nexus 5上测试我的代码。现在,我发现getOutputStream
在除第一个请求之外的每个请求上的readTimeout持续时间内都会阻塞相同的问题。
更新1:正在建立的所有TCP连接的副作用是在某些Android版本(4.1 - 4.3 IIRC)上可能遇到操作系统中的错误(?)您的进程最终耗尽文件描述符。这在现实条件下不太可能发生,但可以通过自动测试来触发。
更新2: OpenSSLSocketImpl class有一个公共getOutputStream
方法,可用于指定与readTimeout分开的握手超时。但是,由于这种方法对于套接字而不是setHandshakeTimeout
存在,因此调用它有点棘手。尽管可以这样做,但在此时您仍然依赖于打开HttpsUrlConnection
可能会或可能不会使用的类的实现细节。
我似乎不太可能认为连接重用不应该只是工作&#34;所以我猜测我做错了什么。有没有人设法可靠地让HttpsUrlConnection
重新使用Android上的连接,并且可以发现我正在制作的任何错误?我真的很想避免诉诸任何第三方图书馆,除非这完全是不可避免的。
请注意,您可能认为的任何想法都需要使用HttpsUrlConnection
16。
答案 0 :(得分:1)
我建议您尝试重复使用SSLContexts
,而不是每次都创建一个新的并更改HttpURLConnection
同上的默认值。它肯定会以你的方式禁止连接池。
不允许NB getAcceptedIssuers()
返回null。