Pixel Watch 3
に朝起こしてもらってますが、起こしてくれない時があった!!
どうしたんかなって見てみたら、オーバーヒートでシャットダウンしてたらしい。腕を怪我する前に守ってくれた模様、ありがと~~
本題
Android
ではHTTP
クライアントライブラリにOkHttp
をよく使うのですが、私の家の回線環境のせいなのか、
ときたま、IPv4 / IPv6
両方に対応したサーバー(ちなAmazon CloudFront
)にあるファイルのダウンロードが全く進まなくなる時がありました。
今回はこれの調査をしました。
先に結論
OkHttp
チームがHappy Eyeballs
機能を実装してくれたため、OkHttp
のバージョンを5系
(記述時時点アルファ版です・・)にすると直るはずです。
https://square.github.io/okhttp/changelogs/changelog/#version-500-alpha11
もしくは、OkHttp
のDNS
でIPv4
を優先する方法でもいいらしいです(バージョンアップできない場合)
詳しくは最後!→ #okhttp-アップデート以外で修正したい
環境
家の固定回線です。今のところ携帯回線(ギガ使うやつ)は再現しないですね、、
あとPixel
系はなってない気がします。気のせいかも。
なまえ | あたい |
---|
たんまつ | Xperia 1 V / OnePlus 7 Pro / Xiaomi Mi 11 Lite 5G |
サーバー | AWS (CloudFront + S3) |
事の発端
Android
アプリでOkHttp
を使い、ファイルダウンロード機能を作ってたんですが、なんだか一向にダウンロードが進まない。
と思って端末変えるとダウンロード出来たり、時間によってはダウンロード出来たり、Wi-Fi
のON/OFF
を試すと動いたりと不安定です。
ちなみにブラウザではダウンロード出来るので、これもまた謎。
最低限のコード
コードはこんな感じで、時間によっては動くから間違いはないはず。
この状態になるとonFailure
もonResponse
も呼ばれないので、本当にずっっっと読み込み中表示になっちゃうことになります。
Logcat
がこう
調べる
ちょうどパソコンの前に座ってるタイミングで発症したので、重い腰を上げて調査することにした。
何でかは知りませんがGoogle Pixel
以外のAndroid
端末でcurl
使えます(何でだろう)。Pixel
だとなんかエラーになってしまう。Xperia
にはある。
持っててよかったPixel
以外
curl
でOkHttp
のリクエストと同じ内容を投げてみます。が、なぜか通ります。
たまたま直ったかと思ってOkHttp
で試してみたけどだめだった、、、
IPv6 が悪い?
ここでcurl -v
付きで詳細を出してもらおうと思いました。
すると興味深い内容が出てきました。
(このブログのCloudFront
のIPアドレス
を例示して良いのか知らんので適当に予約済みIPアドレス
で代替しました。)
(CloudFront
からの借り物だし自分のですって言えない気がする。)
(URL
も適当です)
なんだか IPv4 にフォールバックしています。
curl
はIPv6
を使うことを諦めている説がある
OkHttp の IPv6 周りが怪しい
というわけで見てみたところ、こちらです。
IPv4
とIPv6
のどちらか良い方を使う機能、Happy Eyeballs
って名前がついているらしい。
https://github.com/square/okhttp/issues/506
OkHttp
は既にアルファ版でこの機能が使えるらしい!
解決策としては、OkHttp
をv5
系にして、OkHttpClient.Builder
でfastFallback(true)
を呼び出せば良いらしいです! → どこかのアルファ版からデフォルトtrue
になったっぽい!
https://github.com/square/okhttp/issues/506#issuecomment-1024256588
再現させる
再現させたい方向け。調べてる感じ回線によるので、多分ならない回線だと再現できない。
再現させるにはIPv4 / IPv6 デュアルスタック
なサーバーを用意できれば良いはず。
ちなみにこのブログもAmazon CloudFront (IPv4 / IPv6 両方行ける)
で配信してるので適当に画像をリクエストするでも良いはず。https://takusan.negitoro.dev/icon.png
サーバーを用意する
VPS
とか借りるの面倒なんで、今回はAmazon CloudFront
のディストリビューションを作ることにします。
CloudFront
はIPv4 / IPv6
どっちでも接続できるはず。(手元の端末の回線都合は知らん)
オリジンもS3
にします。S3
のコンテンツをCloudFront
で配信するようにして、これをサーバーとします。
S3
適当にバケットを作ります。
できたら、開いて、適当にファイルをバケットに入れておきます。
このファイルをHappy Eyeballs
を有効にしたOkHttp
からダウンロード出来るか試します。
CloudFront
ディストリビューションを作成します。
オリジンには、さっき作ったS3
を選びます。
オリジンアクセスにはOAC
を使います。
Origin access control settings (recommended)
を選んで、Create new OAC
を押し、そのままにして作成します。
こんな警告が出るので、後で対応します。
あとここを選んで、作成すれば良いはず。
作成後、S3
の設定を変更するよう言われるので、コピーボタンを押して、リンクを押します。
アクセス許可を選び
バケットポリシーを押し、貼り付けます(コピーボタンを押したらクリップボードにコピーされるので、あとは貼り付ければ良い)。
疎通確認
CloudFront
のディストリビューションに戻って、ディストリビューションドメイン名
をコピーし、ブラウザのアドレス欄に打ち込みます。
そのあと、スラッシュ入れて、アップロードしたファイルの名前(今回はtakusan23_icon.png
)を入れて Enter
できた!!!
OkHttp を使う Android アプリを用意する
適当にJetpack Compose
を使うプロジェクトを作ってください。
そのあと、OkHttp
ライブラリを追加します。app
フォルダの方のbuild.gradle (.kts)
を開き、以下を足す。
次にAndroidManifest.xml
で、インターネット権限を追加します。
最後にMainActivity
で適当に画面を作って終わり。
Happy Eyeballs
のON/OFF
用スイッチを付けました。
サンプルのためにダウンロード処理をUI (Compose)
に書いていますが、本当はViewModel
に処理を書くべきです。画面回転を超えられないので。
再現した
タイミング良く回線ハズレを引き当てました!!!
Happy Eyeballs
なしの場合はコールバックが一向に呼ばれないので、Kotlin Coroutines
のwithTimeout { }
のタイムアウトが作動して、タイムアウトになっています。
一方回線ハズレを引いている状態でも、Happy Eyeballs
が有効だとちゃんとダウンロードできました。
OkHttp アップデート以外で修正したい
アルファ版だからアップデートは心配という場合は、DNS
部分をカスタマイズすれば一応は回避できるらしい。
https://github.com/square/okhttp/issues/506#issuecomment-765899011
差分を貼り付けるの面倒なので全部張りますが、
IPv4
を優先するスイッチを付けました。有効にすると、上記のPriorityIpv4Dns
が使われるようにしてみました。
これでも一応動きますが、多分Happy Eyeballs
を使えるアルファ版を使うほうが良いような気がする。
というのもReddit
チームいわく、よく動いているみたい。なので
https://www.reddit.com/r/RedditEng/comments/v1upr8/ipv6_support_on_android/
おわりに
今回使った検証アプリのソースコード置いておきます
https://github.com/takusan23/OkHttpHappyEyeballs
以上です、お疲れ様でした 8888888888888