通过 HttpsUrlConnection 重用 TCP 连接背景问题

2022-09-02 02:05:50

摘要:我在 Android 应用中使用该类,以串行方式通过 TLS 发送多个请求。所有请求都属于同一类型,并发送到同一主机。首先,我会为每个请求获得一个新的TCP连接。我能够解决这个问题,但并非没有在某些Android版本上引起与readTimeout相关的其他问题。我希望会有一种更强大的方法来实现TCP连接重用。HttpsUrlConnection


背景

在检查我正在使用Wireshark的Android应用程序的网络流量时,我观察到每个请求都会导致建立新的TCP连接,并执行新的TLS握手。这会导致相当多的延迟,特别是如果您使用的是3G / 4G,每次往返可能需要相对较长的时间。然后,我尝试了没有TLS的相同方案(即)。在这种情况下,我只看到一个TCP连接正在建立,然后重新用于后续请求。因此,建立新的 TCP 连接的行为特定于 。HttpUrlConnectionHttpsUrlConnection

这里有一些示例代码来说明这个问题(真正的代码显然有证书验证,错误处理等):

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请求,所以我同时使用输出流(写入请求正文)和输入流(读取响应正文)。但我想让这个例子保持简短和简单。

如果我反复调用该方法,我最终会在Wireshark(删节)中得到以下内容:testRequest

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

因此,我最初会说“好吧,我将创建一个对象池,并在可能的情况下重用已建立的连接”。不幸的是,没有骰子,因为实例显然不是要重用的。实际上,读取响应数据会导致输出流关闭,并且尝试重新打开输出流会触发 带有错误消息的 。HttpsUrlConnectionHttp(s)UrlConnectionjava.net.ProtocolException"cannot write request body after response has been read"

我做的下一件事是考虑设置 a 与设置 不同的方式,即您创建 一个 和 一个 .因此,我决定同时制作这两个,并针对所有请求共享它们。HttpsUrlConnectionHttpUrlConnectionSSLContextSSLSocketFactorystatic

从我得到连接重用的角度来看,这似乎工作得很好。但是在某些Android版本上存在一个问题,除了第一个请求之外,所有请求都需要很长时间才能执行。经过进一步的检查,我注意到调用 to 将阻塞一段时间,该时间等于 用 设置的超时时间。getOutputStreamsetReadTimeout

我第一次尝试解决这个问题是在我读完响应数据后,用一个非常小的值添加另一个调用,但这似乎根本没有效果。
然后,我所做的是设置一个更短的读取超时(几百毫秒),并实现我自己的重试机制,该机制尝试重复读取响应数据,直到读取所有数据或达到最初预期的超时。
唉,现在我在某些设备上遇到TLS握手超时。因此,我当时所做的是在调用之前添加一个具有相当大的值的调用,然后在读取响应数据之前将读取超时更改回几百毫秒。这实际上看起来很可靠,我在8或10个不同的设备上进行了测试,运行不同的Android版本,并在所有这些设备上获得了所需的行为。setReadTimeoutsetReadTimeoutgetOutputStream

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

更新 1:所有正在建立的TCP连接的副作用是,在某些Android版本(4.1 - 4.3 IIRC)上,可能会在OS(?)中遇到一个错误,您的进程最终会耗尽文件描述符。这在实际条件下不太可能发生,但可以通过自动测试触发。

更新 2:OpenSSLSocketImpl 类有一个公共方法,可用于指定与 readTimeout 分开的握手超时。但是,由于此方法适用于套接字而不是套接字,因此调用它有点棘手。即使可以这样做,此时您也依赖于类的实现细节,这些类可能会或可能不会作为打开 .setHandshakeTimeoutHttpsUrlConnectionHttpsUrlConnection

问题

在我看来,连接重用不应该“只是工作”,所以我猜我做错了什么。有没有人设法可靠地在Android上重用连接,并能够发现我所犯的任何错误?我真的想避免诉诸任何第三方库,除非这是完全不可避免的。
请注意,无论您想到什么想法,都需要使用16。HttpsUrlConnectionminSdkVersion


答案 1

我建议你尝试重用,而不是每次都创建一个新的,并更改同上的默认值。它肯定会像您拥有的那样抑制连接池。SSLContextsHttpURLConnection

NB 不允许返回空值。getAcceptedIssuers()


答案 2

推荐