java - 使用 HttpsUrlConnection 重用 TCP 连接

标签 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();        
}
注意:在我的实际代码中,我使用 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/

相关文章:

python - 如何使用python一条一条地获取tcp消息

java - 如何在这里使用锁/同步

java - tomcat把web应用使用的txt文件存放在哪里

java - 签署 Eclipse 插件始终有效

java - 使用 GPS 时出现权限错误

linux - 关于拥塞下tcp行为的几个问题

java - 为什么在无限循环时 while 和 do-while 循环优于 for 循环?

android - 如何在请求忽略电池优化后手动更改电池优化设置?

java - 加密应用程序数据的正确方法

networking - c - netmap - Tun/tap 与 netmap/pf_ring/dpdk