Java 使用HttpsUrlConnection重用TCP连接

Java 使用HttpsUrlConnection重用TCP连接,java,android,tcp,httpsurlconnection,Java,Android,Tcp,Httpsurlconnection,执行摘要:我在Android应用程序中使用HttpsUrlConnection类通过TLS以串行方式发送大量请求。所有请求都属于同一类型,并发送到同一主机。首先,我会为每个请求获得一个新的TCP连接。我能够解决这个问题,但在一些Android版本上,也会出现与readTimeout相关的其他问题。我希望有一种更健壮的方法来实现TCP连接重用 背景 在检查我与Wireshark合作的Android应用程序的网络流量时,我发现每个请求都会导致建立新的TCP连接,并执行新的TLS握手。这会导致相当大

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


背景 在检查我与Wireshark合作的Android应用程序的网络流量时,我发现每个请求都会导致建立新的TCP连接,并执行新的TLS握手。这会导致相当大的延迟,特别是当您使用3G/4G时,每次往返可能需要较长的时间。 然后,我在没有TLS的情况下尝试了相同的场景(即
HttpUrlConnection
)。在本例中,我只看到建立了一个TCP连接,然后将其重新用于后续请求。因此,正在建立的新TCP连接的行为特定于
HttpsUrlConnection

下面是一些示例代码来说明这个问题(实际代码显然有证书验证、错误处理等):

我是否调用
conn.disconnect
对行为没有影响

因此,我最初认为“好的,我将创建一个
HttpsUrlConnection
对象池,并在可能的情况下重用已建立的连接”。不幸的是,没有骰子,因为
Http(s)UrlConnection
实例显然不打算重用。实际上,读取响应数据会导致输出流关闭,尝试重新打开输出流会触发
java.net.ProtocolException
,并显示错误消息
“读取响应后无法写入请求正文”

我做的下一件事是考虑建立一个<代码> HtpSurl连接< /C> >不同于设置<代码> HTTURLCONNECT/<代码>,即创建一个<代码> SSLValue和<代码> SSLSocketFactory < /代码>。因此,我决定将这两个

都设置为静态
,并为所有请求共享它们

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

我的第一次尝试是在读取完响应数据后,用一个很小的值向
setReadTimeout
添加另一个调用,但这似乎没有任何效果。
然后,我设置了一个更短的读取超时(几百毫秒),并实现了我自己的重试机制,该机制尝试重复读取响应数据,直到所有数据都已读取或达到最初预期的超时。
唉,现在我在一些设备上得到了TLS握手超时。因此,我当时做的是在调用
getOutputStream
之前添加一个对
setReadTimeout
的调用,并在读取响应数据之前将读取超时更改回几百毫秒。这实际上看起来很可靠,我在8到10台不同的设备上测试了它,运行不同的Android版本,并在所有设备上都得到了预期的性能

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

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

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

问题 对我来说,连接重用不应该“只起作用”似乎是不可能的,所以我猜我做错了什么。是否有人能够可靠地获得
HttpsUrlConnection
以在Android上重用连接,并且能够发现我犯的任何错误?我真的希望避免求助于任何第三方库,除非这是完全不可避免的。

请注意,您可能想到的任何想法都需要使用16的
minSdkVersion

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


NB
getAcceptedAssuers()
不允许返回null。

为什么不尝试okHTTP实现?请参阅链接,等待Google开始使用OpenJDK源代码。然后它会自动发生。@EJP:也许吧,尽管我不会拿我的生命打赌。但这并不能解决我眼前的问题,因为安卓N还很遥远,有些设备永远也无法升级。这不仅仅是客户表现不佳的问题;在高负载期间,如果每个客户端
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();        
}
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 ...