要約:私は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
がトリガーされます。
私は次のことは、あなたがSSLContext
とSSLSocketFactory
作成すなわちことHttpsUrlConnection
はHttpUrlConnection
を、設定は異なり設定する方法を検討することでした。だから、私はそれらの両方を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の
なぜokHTTP実装を試してみませんか?リンクhttp://square.github.io/okhttp/ –
を参照してください。GoogleがOpenJDKソースを使用して開始するまで待ってください。それは自動的に起こります。 – EJP
@EJP:おそらく、私はそれに私の人生を賭けていないだろうが。 Android Nはまだまだ離れており、一部の端末ではアップグレードが行われないため、すぐに問題を解決することはできません。これはクライアントのパフォーマンスが悪いだけではありません。実際には1または2だけが必要なときに、すべてのクライアントが多くの接続を確立している場合、高負荷時にサーバーの問題になる可能性があります。 – Michael