アプリを開いていないのに、スマートフォンに通知が届く。普段は当たり前に使っているこの仕組みは、裏側ではどう動いているのでしょうか。今回はメルカリが公開し、すでにGitHub上でアーカイブされたGo製のプッシュ通知サーバー「Gaurun」を入口に、元メンテナのcatatsuyがスマートフォンのプッシュ通知の仕組みを掘り下げます。
話はまず、iPhoneやAndroidにプッシュ通知が届く仕組みから。端末がAppleやGoogleのサーバーと張り続けているコネクションの話や、APNsが5223番、FCMが5228番というHTTPSとは別のポートを使っている話をhentekoが素朴に掘り下げます。そこからAPNsへ。HTTP/2しか話せずHTTP/1.1にフォールバックしない、コネクションを維持しながら大量の通知を送る必要がある、100万人に送るなら基本的には100万の配送先を扱う必要がある、といった独特の事情から、PHPのWebアプリケーションから直接大量のプッシュ通知を送るのが厳しく、Gaurunのような専用サーバーが必要だった背景が見えてきます。
さらに話はGoのHTTP/2実装まで降りていきます。APNsの証明書認証のためにTLSClientConfigを設定するとHTTP/2が自動で有効にならず、golang.org/x/net/http2を明示的に使う必要があった時代がありました。しかしGo本体にもHTTP/2の実装がbundleされているため、異なるバージョンのHTTP/2実装が同居し、実際の障害につながります。最終的にはGo 1.13で追加されたForceAttemptHTTP2によって、Gaurunからgolang.org/x/net/http2への直接依存を外せるようになりました。Appleの証明書更新とJWTを使ったトークンベース認証など、「プッシュ通知を送る」裏側にある実装と運用の面倒さも語られます。
後半は「今から作るなら」の話。現在はFCMからAPNsへ通知を送ることもでき、Topicを使えば大量のfan-outまでGoogle側に任せられます。Gaurunが自前で担っていた仕事のかなりの部分を、今ではFCMがManaged Serviceとして引き受けています。
実はcatatsuy自身も、GaurunをFCM HTTP v1 APIに対応させるPull Requestを出していました。しかし当時はLegacy APIがまだ使えており、急いで移行する理由が薄かったためclose。その後Legacy APIは終了し、Gaurunもアーカイブされました。外部サービスのAPIの変化によって、OSSの役割がどう変わっていくのかという話にもつながります。
さらに、既存のAPNs tokenをFCM側へ移す仕組み、registration tokenからFIDへの移行、中国のグレートファイアウォールといった少し厄介なケースまで掘り下げます。
プッシュ通知を実装したことがある人はもちろん、「そもそもスマートフォンの通知ってどうやって届いているの?」という人にも聞いてほしい回です。
## 関連リンク
- Gaurun (mercari/gaurun): https://github.com/mercari/gaurun
- gaurunとGoのHTTP/2事情について: https://engineering.mercari.com/blog/entry/2019-12-13-110000/
- Gaurun FCM HTTP v1 API 対応 PR #109: https://github.com/mercari/gaurun/pull/109
- Gaurun ForceAttemptHTTP2 対応 PR #130: https://github.com/mercari/gaurun/pull/130
- Gaurun APNs トークンベース認証対応 PR #138: https://github.com/mercari/gaurun/pull/138
- Apple Push Notification service (APNs) - Sending notification requests to APNs: https://developer.apple.com/documentation/usernotifications/sending-notification-requests-to-apns
- APNs - Establishing a token-based connection: https://developer.apple.com/documentation/usernotifications/establishing-a-token-based-connection-to-apns
- APNs - Establishing a certificate-based connection: https://developer.apple.com/documentation/usernotifications/establishing-a-certificate-based-connection-to-apns
- Firebase Cloud Messaging (FCM): https://firebase.google.com/docs/cloud-messaging
- FCM HTTP v1 API リファレンス: https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages
- FCM トピックメッセージング: https://firebase.google.com/docs/cloud-messaging/topic-messaging
- Firebase Admin SDK: https://firebase.google.com/docs/admin/setup
- Instance ID API(APNs トークンの FCM トークンへのインポート): https://developers.google.com/instance-id/reference/server
- golang.org/x/net/http2: https://pkg.go.dev/golang.org/x/net/http2
- Go net/http Transport(ForceAttemptHTTP2): https://pkg.go.dev/net/http#Transport
- Go: https://go.dev/
- nginx: https://nginx.org/
─────────────
YouTube: https://youtu.be/l5N_fS0527g
Web: https://yaoyorozu-oss.henteko07.com/
X: https://x.com/yaoyorozu_oss