AndroidのHttpURLConnectionのPOSTでハマる(原因がわかってスッキリ)
原因がわかったところで対処方法は変わらないのである。
以前、このような記事を書いたのですが、
初のコメントを頂いたので色々と検証していたら、
HTTPサーバのKeepAliveタイムアウト後のPOST通信の処理に問題がありそうだということが判明。
結論から述べると、
結論
問題内容
「HttpURLConnectionクラスを使って、HTTP(POST)を連続で実行すると2回目以降にIOException食らう場合がある。」*1
発生パターン
APIレベル19以前のHttpURLConnectionをKeep-Aliveを有効のままにしておくと、以下の場合にPOST通信に失敗する。
- HTTPサーバ側が"HTTP/1.0"応答かつHTTPサーバ側がレスポンス送信後にTCPセッションをクローズしない場合
- HTTPサーバ側が"HTTP/1.1"応答かつHTTPサーバ側がKeep-AliveタイムアウトでTCPセッションをクローズした場合
1.のケースは、特定のHTTPサーバ*2でしか起こらないし、今どき"HTTP/1.0"なHTTPサーバなんて希少すぎるので省略。
2.のケースは、メジャーなHTTPサーバで試したら発生した。
発生理由
APIレベル19以前のHttpURLConnectionの実装クラスである、"libcore.net.http.HttpURLConnectionImpl"がPOST時だけハーフクローズ時の確認をしていないせい(だと思う)*3。
ハーフクローズってなんじゃいという人はこの図を見るとなんとなくわかると思われ。
- APIレベル19以降のHttpURLConnectionの実装クラスである、"com.android.okhttp.internal.http.HttpURLConnectionImpl"では発生しない
- ICS以降しか試してないのでそれ以前のバージョンで同様の事象がでるかは未確認
回避策
- Keep-Aliveを無効にする(方法は前回記事を参照)
- HttpURLConnectionを使わず、OkHttpなどのOSSライブラリを使う
- どっちもダメなら自前のリトライ処理を実装*4
では、ここからは原因解明編。
調査方法
以下のHTTPサーバとAndroidの組合せでHTTP通信を行い、HTTPサーバのLinux上でパケットキャプチャした結果を調査。
HTTPサーバ(Ubuntu14.01LTS)
Apache2 | 2.4.7(Ubuntu) |
Nginx | 1.4.6 (Ubuntu) |
Node.js | v0.10.25 |
Android(Genymotion)
Android 4.1.1 | (API16) |
Android 4.4.4 | (API19) |
発生有無の違いはAPIレベルによってHttpURLConnectionの実装クラスが異なるためだということは前回までに見切ったのでこの2台で十分と判断した。
- APIレベルによるHttpURLConnectionの実装クラス
API < 19 | libcore.net.http.HttpURLConnectionImpl |
API >=19 | com.android.okhttp.internal.http.HttpURLConnectionImpl |
アプリは前回記事で事象発生するコードをベースにKeepAliveの有無やメソッド(POST/GET)を変えて発生パターンを調査。
調査結果
事象発生有無は以下となった。
POST
APIレベル | Connection | HTPサーバ | 発生有無 |
API16 | Keep-Alive | Apache2(HTTP/1.0) | 発生しない |
API16 | Keep-Alive | Apache2(HTTP/1.1) | 発生する |
API16 | Keep-Alive | Nginx(HTTP/1.1) | 発生する |
API16 | Keep-Alive | Node.js(HTTP/1.1) | 発生する |
API19 | Keep-Alive | Apache2(HTTP/1.1) | 発生しない |
API19 | Keep-Alive | Nginx(HTTP/1.1) | 発生しない |
API19 | Keep-Alive | Node.js(HTTP/1.1) | 発生しない |
API16 | close | Apache2(HTTP/1.1) | 発生しない |
API16 | close | Nginx(HTTP/1.1) | 発生しない |
API16 | close | Node.js(HTTP/1.1) | 発生しない |
Apache2以外のHTTPサーバは、"HTTP/1.0"で強制応答する設定が不明だったので試していない。
GET
APIレベル | Connection | HTPサーバ | 発生有無 |
API16 | Keep-Alive | Apache2(HTTP/1.1) | 発生しない |
API19 | Keep-Alive | Apache2(HTTP/1.1) | 発生しない |
POSTで色々と試した結果KeepAliveタイムアウト時の挙動とわかっており、Apache2のGETで発生しなければ他のHTTPサーバでもでないと判断して試していない。
パケット解析してみる
以下を実行した時のパケットを眺めてみた。本当はACKが綺麗にわかれていなかったり再送したりしてるんだけどわかりやすいように整形している。
POST
[API16]Android(Connection:Keep-Alive) / Apache2(HTTP/1.1)
これが事象発生するパターン。Apache2以外のHTTPサーバもKeepAliveタイムアウト値が異なるだけで同様のシーケンス*5。
| Client HTTP/1.1 | Server HTTP/1.1 | |(Connection:Keep-Alive) | | SYN | | |(44260) ------------------> (80) | | SYN, ACK | | |(44260) <------------------ (80) | | ACK | | |(44260) ------------------> (80) | |<============[Connected]==============>| |<POST Request> | | | POST /test.json HTTP/1.1 | |(44260) ------------------> (80) | | ACK | | |(44260) <------------------ (80) | | HTTP/1.1 200 OK | |(44260) <------------------ (80) | | ACK | | |(44260) ------------------> (80) | |<POST Request> | | | POST /test.json HTTP/1.1 | |(44260) ------------------> (80) | | ACK | | |(44260) <------------------ (80) | | HTTP/1.1 200 OK | |(44260) <------------------ (80) | | ACK | | |(44260) ------------------> (80) | | |<KeepAlive TIMEOUT>| | FIN | | |(44260) <------------------ (80) | | ACK | | |(44260) ------------------> (80) | |<============[Half Close]==============| |<POST Request> | | | POST /test.json HTTP/1.1 | |(44260) ------------------> (80) | | ACK | | |(44260) <------------------ (80) | |<POST Response> | | |(44260) <xxxxxxxxxxxxxxxxxx (80) | |<IOException> | |
HTTPサーバ側より、"KeepAlive TIMEOUT"契機でFINが送信されて[Half Close]状態となる。
クライアント側はPOST送信前にソケット状態をチェックしないで処理を開始。ハーフクローズなので"POST Request"は成功するけど"POST Response"を読込しようとして例外"IOEXception"がでる。
[API19]Android(Connection:Keep-Alive) / Apache2(HTTP/1.1)
事象発生しないパターン。Apache2以外のHTTPサーバもKeepAliveタイムアウト値が異なるだけで同様のシーケンス。
| Client HTTP/1.1 | Server HTTP/1.1 | |(Connection:Keep-Alive) | | SYN | | |(55136) ------------------> (80) | | SYN, ACK | | |(55136) <------------------ (80) | | ACK | | |(55136) ------------------> (80) | |<============[Connected]==============>| |<POST Request> | | | POST /test.json HTTP/1.1 | |(55136) ------------------> (80) | | ACK | | |(55136) <------------------ (80) | |<POST Response> | | | HTTP/1.1 200 OK | |(55136) <------------------ (80) | | ACK | | |(55136) ------------------> (80) | |<POST Request> | | | POST /test.json HTTP/1.1 | |(55136) ------------------> (80) | | ACK | | |(55136) <------------------ (80) | |<POST Response> | | | HTTP/1.1 200 OK | |(55136) <------------------ (80) | | ACK | | |(55136) ------------------> (80) | | |<KeepAlive TIMEOUT>| | FIN | | |(55136) <------------------ (80) | | ACK | | |(55136) ------------------> (80) | |<============[Half Close]==============| |<POST Request> | | | FIN | | |(55136) ------------------> (80) | | ACK | | |(55136) <------------------ (80) | |<===========[Disconnected]============>| | SYN | | |(55141) ------------------> (80) | | SYN, ACK | | |(55141) <------------------ (80) | | ACK | | |(55141) ------------------> (80) | |<============[Connected]==============>| |<POST Request> | | | POST /test.json HTTP/1.1 | |(55141) ------------------> (80) | | ACK | | |(55141) <------------------ (80) | |<POST Response> | | | HTTP/1.1 200 OK | |(55141) <------------------ (80) | | ACK | | |(55141) ------------------> (80) |
HTTPサーバ側より、"KeepAlive TIMEOUT"契機でFINが送信されて[Half Close]状態となる。
クライアント側はPOST送信前にソケット状態をチェックして切断後に再接続するので、[Half Close]状態が解消されて問題無く動作。
[API16]Android(Connection:close) / Apache2(HTTP/1.1)
| Client HTTP/1.1 | Server HTTP/1.1 | |(Connection:close) | | SYN | | |(45551) ------------------> (80) | | SYN, ACK | | |(45551) <------------------ (80) | | ACK | | |(45551) ------------------> (80) | |<============[Connected]==============>| |<POST Request> | | | POST /test.json HTTP/1.1 | |(45551) ------------------> (80) | | ACK | | |(45551) <------------------ (80) | |<POST Response> | | | HTTP/1.1 200 OK | |(45551) <------------------ (80) | | ACK | | |(45551) ------------------> (80) | | FIN | | |(45551) <------------------ (80) | | FIN, ACK | | |(45551) ------------------> (80) | | ACK | | |(45551) <------------------ (80) | |<===========[Disconnected]============>|
クライアント側はPOSTのリクエストヘッダに「Connection:close」を設定して"POST Request"を送信。
HTTPサーバ側は"POST Response"応答後にFINを送信し、クライアント側もFINを返して切断状態となる。
なお、クライアントが「Connection:Keep-Alive」でHTTPサーバが"HTTP/1.0"で応答した場合も同じシーケンスになる*6。
GET
以前はHTTPサーバのKeepAliveタイムアウトを意識して試さなかったので念のため調査。
[API16]Android(Connection:Keep-Alive) / Apache2(HTTP/1.1)
| Client HTTP/1.1 | Server HTTP/1.1 | |(Connection:Keep-Alive) | | SYN | | |(55540) ------------------> (80) | | SYN, ACK | | |(55540) <------------------ (80) | | ACK | | |(55540) ------------------> (80) | |<============[Connected]==============>| |<GET Request> | | | GET /test.json HTTP/1.1 | |(55540) ------------------> (80) | | ACK | | |(55540) <------------------ (80) | |<GET Response> | | | HTTP/1.1 200 OK | |(55540) <------------------ (80) | | ACK | | |(55540) ------------------> (80) | |<GET Request> | | | GET /test.json HTTP/1.1 | |(55540) ------------------> (80) | | ACK | | |(55540) <------------------ (80) | |<GET Response> | | | HTTP/1.1 200 OK | |(55540) <------------------ (80) | | ACK | | |(55540) ------------------> (80) | | |<KeepAlive TIMEOUT>| | FIN | | |(55540) <------------------ (80) | | ACK | | |(55540) ------------------> (80) | |<============[Half Close]==============| |<GET Request> | | | GET /test.json HTTP/1.1 | |(55540) ------------------> (80) | | FIN, ACK | | |(55540) ------------------> (80) | | ACK | | |(55540) <------------------ (80) | |<===========[Disconnected]============>| | SYN | | |(55541) ------------------> (80) | | SYN, ACK | | |(55541) <------------------ (80) | | ACK | | |(55541) ------------------> (80) | |<============[Connected]==============>| | GET /test.json HTTP/1.1 | |(55541) ------------------> (80) | | ACK | | |(55541) <------------------ (80) | |<GET Response> | | | HTTP/1.1 200 OK | |(55541) <------------------ (80) | | ACK | | |(55541) ------------------> (80) |
HTTPサーバ側より、"KeepAlive TIMEOUT"契機でFINが送信されて[Half Close]状態となる。
クライアント側は"GET Request"送信に失敗して切断後に再接続するので、[Half Close]状態が解消されて問題無く動作*7。
[API19]Android(Connection:Keep-Alive) / Apache2(HTTP/1.1)
| Client HTTP/1.1 | Server HTTP/1.1 | |(Connection:Keep-Alive) | | SYN | | |(55587) ------------------> (80) | | SYN, ACK | | |(55587) <------------------ (80) | | ACK | | |(55587) ------------------> (80) | |<============[Connected]==============>| |<GET Request> | | | GET /test.json HTTP/1.1 | |(55587) ------------------> (80) | | ACK | | |(55587) <------------------ (80) | |<GET Response> | | | HTTP/1.1 200 OK | |(55587) <------------------ (80) | | ACK | | |(55587) ------------------> (80) | |<GET Request> | | | GET /test.json HTTP/1.1 | |(55587) ------------------> (80) | | ACK | | |(55587) <------------------ (80) | |<GET Response> | | | HTTP/1.1 200 OK | |(55587) <------------------ (80) | | ACK | | |(55587) ------------------> (80) | | |<KeepAlive TIMEOUT>| | FIN | | |(55587) <------------------ (80) | | ACK | | |(55587) ------------------> (80) | |<============[Half Close]==============| |<GET Request> | | | GET /test.json HTTP/1.1 | |(55587) ------------------> (80) | | FIN, ACK | | |(55587) ------------------> (80) | | ACK | | |(55587) <------------------ (80) | |<===========[Disconnected]============>| | SYN | | |(55588) ------------------> (80) | | SYN, ACK | | |(55588) <------------------ (80) | | ACK | | |(55588) ------------------> (80) | |<============[Connected]==============>| | GET /test.json HTTP/1.1 | |(55588) ------------------> (80) | | ACK | | |(55588) <------------------ (80) | |<GET Response> | | | HTTP/1.1 200 OK | |(55588) <------------------ (80) | | ACK | | |(55588) ------------------> (80) |
HTTPサーバ側より、"KeepAlive TIMEOUT"契機でFINが送信されて[Half Close]状態となる。
クライアント側は"GET Request"送信に失敗して切断後に再接続するので、[Half Close]状態が解消されて問題無く動作*8。
考察
試しにPOSTでリクエストボディを指定しない場合を試したんだけど、その場合はGETと同様に事象が出なかった。このことから、POSTの送信時のOutputStream関連で何か問題があるのではないかなと思っている。でもAPI19以降で既に解決されているので今更これ以上深追いしてもしょうがない。発生原因がわかってモヤモヤがスッキリしただけでよしとしましょう。
しかし、まさかHTTPのパケットを眺めることになるとは思わなかったけども、見えないところでTCPが色々とやってるんだよねと改めて実感。
- 作者: Philip Miller,苅田幸雄
- 出版社/メーカー: オーム社
- 発売日: 1998/05/01
- メディア: 単行本
- 購入: 7人 クリック: 70回
- この商品を含むブログ (38件) を見る
実践 パケット解析 第2版 ―Wiresharkを使ったトラブルシューティング
- 作者: Chris Sanders,高橋基信,宮本久仁男,岡真由美
- 出版社/メーカー: オライリージャパン
- 発売日: 2012/11/17
- メディア: 大型本
- 購入: 1人 クリック: 10回
- この商品を含むブログ (9件) を見る
- 作者: エズモンド・ピット,岩谷宏
- 出版社/メーカー: ソフトバンク クリエイティブ
- 発売日: 2007/04/28
- メディア: 大型本
- 購入: 9人 クリック: 100回
- この商品を含むブログ (25件) を見る