初老のボケ防止日記

おっさんのひとりごとだから気にしないようにな。

AndroidのHttpURLConnectionのPOSTでハマる(原因がわかってスッキリ)

原因がわかったところで対処方法は変わらないのである。

以前、このような記事を書いたのですが、

osa030.hatenablog.com

初のコメントを頂いたので色々と検証していたら、

HTTPサーバのKeepAliveタイムアウト後のPOST通信の処理に問題がありそうだということが判明。

結論から述べると、

結論

問題内容

「HttpURLConnectionクラスを使って、HTTP(POST)を連続で実行すると2回目以降にIOException食らう場合がある。」*1

発生パターン

APIレベル19以前のHttpURLConnectionをKeep-Aliveを有効のままにしておくと、以下の場合にPOST通信に失敗する。

  1. HTTPサーバ側が"HTTP/1.0"応答かつHTTPサーバ側がレスポンス送信後にTCPセッションをクローズしない場合
  2. 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

ハーフクローズってなんじゃいという人はこの図を見るとなんとなくわかると思われ。

koseki.hatenablog.com

  • APIレベル19以降のHttpURLConnectionの実装クラスである、"com.android.okhttp.internal.http.HttpURLConnectionImpl"では発生しない
  • ICS以降しか試してないのでそれ以前のバージョンで同様の事象がでるかは未確認

回避策

  1. Keep-Aliveを無効にする(方法は前回記事を参照)
  2. HttpURLConnectionを使わず、OkHttpなどのOSSライブラリを使う
  3. どっちもダメなら自前のリトライ処理を実装*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が色々とやってるんだよねと改めて実感。

マスタリングTCP/IP 応用編

マスタリングTCP/IP 応用編

実践 パケット解析 第2版 ―Wiresharkを使ったトラブルシューティング

実践 パケット解析 第2版 ―Wiresharkを使ったトラブルシューティング

  • 作者: Chris Sanders,高橋基信,宮本久仁男,岡真由美
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2012/11/17
  • メディア: 大型本
  • 購入: 1人 クリック: 10回
  • この商品を含むブログ (9件) を見る
Javaネットワークプログラミングの真髄

Javaネットワークプログラミングの真髄

*1:調査結果を踏まえて前回記事から若干文言を修正

*2:PythonのHTTPサーバ(SimpleHTTPServer)をデフォルトの"HTTP/1.0"で動作させた場合。

*3:コード見たわけじゃないので実際のところは不明

*4:Wi-Fi<->LTEでガンガンIP変わるスマートフォンアプリなら送信失敗のリトライが入ってそうなので実は既に発生しているのに気付かないだけかもしれない

*5:タイムアウト後のPOST Requestの応答がACKじゃなくてRST応答になるだけ

*6:Apache2のみ確認

*7:例外が送出されてこないので内部的に再接続をしていると判断

*8:同上