2016-01-12 25 views
21

要約:私はAndroidアプリケーションでHttpsUrlConnectionクラスを使用して、TLS経由で一連の要求をシリアルに送信しています。すべての要求は同じタイプで、同じホストに送信されます。最初は、各リクエストに対して新しいTCP接続を取得します。私はそれを修正することができましたが、readTimeoutに関連する一部のAndroidバージョンでは他の問題を引き起こすことはありませんでした。私は、TCP接続の再利用を実現するより堅牢な方法が存在することを願っています。HttpsUrlConnectionとのTCP接続の再利用


背景

私はすべての要求が確立された新しいTCP接続をもたらすことを観察Wiresharkのに働いているAndroidアプリのネットワークトラフィックを検査し、新しいTLSハンドシェイクが行われています。この結果、かなりの遅延が発生します。特に、3G/4Gの場合、各往復に比較的長い時間がかかる場合があります。 その後、TLSなしで同じシナリオを試しました(つまり、HttpUrlConnection)。この場合、私は単一のTCP接続が確立されているのを見ただけで、後続の要求に再利用しました。新しいTCP接続が確立されている場合の動作は、HttpsUrlConnectionに固有です。私の実際のコードでは、私は、POSTリクエストを使用するので、私は両方を使用します。

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();   
} 

注:ここでは

は、問題を説明するためにいくつかのサンプルコードです(実際のコードは明らかに証明書の検証、エラー処理、などがあります) (要求本体を書き込むための)出力ストリームと(応答本体を読み込むための)入力ストリームとの両方を含む。しかし私は、この例を短く単純なものにしたいと思っていました。私は何度 testRequestメソッドを呼び出して、私はWiresharkの中で、次で終わる場合

(簡略):私はconn.disconnectを呼び出すかどうかは

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 ... 

は、行動に影響を与えません。

だから私は、最初は「OK、私はHttpsUrlConnectionオブジェクトのプールを作成し、可能な場合に確立された接続を再利用しましょう」けれども。残念ながら、サイコロはありません。Http(s)UrlConnectionのインスタンスは、明らかに再利用されるものではありません。実際には、応答データを読み取ると出力ストリームが閉じられ、出力ストリームを再オープンしようとすると、エラーメッセージ"cannot write request body after response has been read"java.net.ProtocolExceptionがトリガーされます。

私は次のことは、あなたがSSLContextSSLSocketFactory作成すなわちことHttpsUrlConnectionHttpUrlConnectionを、設定は異なり設定する方法を検討することでした。だから、私はそれらの両方をstaticにして、すべての要求に対してそれらを共有することに決めました。

これは、接続の再利用があるという意味で問題なく動作するようです。しかし、特定のAndroidバージョンでは、最初のもの以外のすべてのリクエストが実行に非常に時間がかかるという問題がありました。さらに検査すると、getOutputStreamへの呼び出しが、setReadTimeoutで設定されたタイムアウトに等しい時間量をブロックすることに気付きました。

これを修正する私の最初の試みは、レスポンスデータを読み終えた後にもう一度setReadTimeoutの呼び出しを追加することでしたが、それはまったく効果がないようでした。
私がしたことは、はるかに短い読み取りタイムアウト(数百ミリ秒)を設定し、すべてのデータが読み込まれるか、元々意図されたタイムアウトに達するまで、応答データを繰り返し読み取ろうとする独自の再試行メカニズムを実装しました。
悲しいかが、私はいくつかのデバイスでTLSハンドシェークタイムアウトを取得していました。だから私はgetOutputStreamを呼び出す前にかなり大きな値を持つsetReadTimeoutへの呼び出しを追加してから、応答データを読み取る前に読み取りタイムアウトを数百msに変更して戻しました。これは実際はしっかりとしていて、私はそれを8または10の異なるデバイス上でテストし、異なるAndroidバージョンを実行し、それらのすべてに対して望ましい動作を得ました。

早送りに2週間ほどかかって、最新の工場イメージ(6.0.1(MMB29S))を実行しているNexus 5でコードをテストすることにしました。今私はgetOutputStreamが最初のものを除くすべてのリクエストで私のreadTimeoutの期間ブロックされる同じ問題を見ています。

アップデート1:確立されているすべてのTCP接続の副作用は、いくつかのAndroidのバージョンであることである - あなたのプロセスは、最終的に実行される場所(4.1 4.3 IIRC)、それはOSのバグに遭遇することも可能です(?)ファイル記述子の不足これは現実の状況下では起こりそうにないが、自動テストによって引き起こされる可能性がある。

アップデート2:OpenSSLSocketImpl classはreadTimeoutから分離されたハンドシェイクタイムアウトを指定するために使用することができる公共setHandshakeTimeout方法を有しています。しかし、このメソッドはHttpsUrlConnectionではなくソケットのために存在するので、それを呼び出すのはややこしいことです。そのようにすることは可能ですが、その時点でHttpsUrlConnectionを開いた結果として使用されるクラスと使用されないクラスの実装の詳細に頼っています。

質問

接続の再利用は、「ただ働き」べきではないと私にはありえないようなので、私は私が何か間違ったことをやっていることを推測しています。 Androidで接続を再利用するためにHttpsUrlConnectionを確実に取得できた人はいますか?私が作っている間違いを見つけ出すことができますか?私は本当にそれが完全に避けられない限り、第三者の図書館に頼らないようにしたいと思っています。どんなアイデアあなたがminSdkVersion 16の

+1

なぜokHTTP実装を試してみませんか?リンクhttp://square.github.io/okhttp/ –

+0

を参照してください。GoogleがOpenJDKソースを使用して開始するまで待ってください。それは自動的に起こります。 – EJP

+0

@EJP:おそらく、私はそれに私の人生を賭けていないだろうが。 Android Nはまだまだ離れており、一部の端末ではアップグレードが行われないため、すぐに問題を解決することはできません。これはクライアントのパフォーマンスが悪いだけではありません。実際には1または2だけが必要なときに、すべてのクライアントが多くの接続を確立している場合、高負荷時にサーバーの問題になる可能性があります。 – Michael

答えて

1

と連携する必要性を考えるかもしれないこと
注意私はあなたの代わりに新しいものを毎回作成し、HttpURLConnection同上のデフォルトを変更するのでSSLContextsを再利用しようと示唆しています。あなたが持っている方法で接続プーリングを禁止するのは間違いありません。

NB getAcceptedIssuers()はnullを返すことはできません。

+0

"毎回新しいものを作成するのではなく、' SSLContexts'を再利用することをお勧めします。 _ "次に、私は、HttpsUrlConnectionを設定する方法が' HttpUrlConnection'の設定と異なる、つまり 'SSLContext'と' SSLSocketFactory'を作成する方法を検討することでした。 "_" getAcceptedIssuers() 'はnullを返すことができません" _ "実際のコードは明らかに証明書の検証、エラー処理などを持っています" _ – Michael

+0

@Michael Iあなたが投稿したコードにコメントしています。それが実際のコードではない場合、あなたの質問は役に立たない。あなたの質問を修正してください。 – EJP

+0

私はアプリケーションコードの著作権を所有していないので、誰とでも共有することはできません。問題のコードは、誰かがそれをテストすることに興味があれば、接続の再利用が全くないという元の問題を再現する方法の最小例です。私は、私が変更しようとしたすべての事柄と、その変更の結果について説明します。これらの説明と例が十分にはっきりしていないと感じる場合は、元の例とそれらの変更を組み合わせた別の例をまとめることができます。しかし、それは月曜日まで待たなければならないでしょう。 – Michael

関連する問題