【问题标题】:Reusing TCP connections with HttpsUrlConnection通过 HttpsUrlConnection 重用 TCP 连接
【发布时间】: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 的不同之处,即创建SSLContextSSLSocketFactory。所以我决定制作这两个 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


【解决方案1】:

我建议您尝试重复使用SSLContexts,而不是每次都创建一个新的并更改HttpURLConnection 的默认值。它肯定会以您拥有的方式抑制连接池。

NB getAcceptedIssuers() 不允许返回 null。

【讨论】:

  • “我建议你尝试重复使用SSLContexts 而不是每次都创建一个新的”。 “接下来我要做的是考虑设置HttpsUrlConnection 与设置HttpUrlConnection 的不同之处,即创建SSLContextSSLSocketFactory所以我决定把这两个static都做成,并为所有请求共享它们。" "getAcceptedIssuers() is not allowed return null" "真正的代码显然有证书验证,错误处理等”
  • @Michael 我正在评论您发布的代码。如果那不是真正的代码,那么您的问题就是徒劳的。请解决您的问题。
  • 我不拥有应用程序代码的版权,所以我不能与任何人分享。如果有人有兴趣对此进行测试,问题中的代码是如何重现根本没有连接重用的原始问题的最小示例。然后我会解释我试图改变的所有事情,以及这些改变的结果。如果您觉得这些解释+示例不够清楚,那么我想我可以组合另一个示例,将原始示例与这些更改结合起来。不过它必须等到星期一。
  • 不幸的是,很难找到合适的服务器来测试更新的示例。我试过"https://posttestserver.com/post.php",但它似乎在我关闭输入流后立即终止TCP连接(或者当我做conn.disconnect时,我不确定)。我还尝试了一些自托管的替代方案,例如openssl s_serverMockServer,但无法让它们正确响应 POST 请求(其中“正确”意味着返回响应代码 200 和包含一些数据的响应正文)。
  • (出于保密原因,我在实际代码中发出请求的服务器不能用于此处发布的任何示例)
猜你喜欢
  • 2011-06-10
  • 1970-01-01
  • 2012-04-26
  • 2018-02-05
  • 2017-01-13
  • 2013-02-19
  • 1970-01-01
  • 2016-09-27
  • 1970-01-01
相关资源
最近更新 更多