Ruby で Apple Push Notification (APN)

みなさんこんにちは。菊池です。

街角ではジングルベルのメロディーが流れている今日この頃、皆さんいかがお過ごしでしょうか?この記事が公開される金曜日、僕らのユニットでは忘年会を予定しています。クリスマス前なので「みんなでプレゼント交換しよう!(予算1000円)」ということになっているのですが、みんながどんなプレゼントを用意してくるのか!?ネタに走るのでしょうか!笑わせてくれるのでしょうか!今から楽しみです!

63166_10151156338979624_1979036374_n

個人的に今年の仕事を振り返ってみると Objective-CiOS アプリを書いたり、Rails でサーバーサイドを実装したり、Redis にザクザク入れてみたりと、なかなか楽しい一年でした。やりたいことはまだまだあるので、来年も楽しめそうです。

さて、iOS アプリを作ると欲しくなる機能の一つにプッシュ通知(Apple Push Notification)があります。アプリに対してメッセージを送ったり、アイコン横に赤丸で数字のバッヂを表示したりするアレです。プッシュ通知メッセージを受信したときの音を指定することもできます。

この Apple Push Notification(以下APN)を使って端末にメッセージを送信するには、ざっくり概要ですが、こんな流れになります:

アプリ側の実装
  • アプリ起動時に通知用のデバイストークンを取得
  • 取得したデバイストークンを自前サーバーに送信

自前サーバー側の実装
  • アプリから受け取ったデバイストークンと送信したいメッセージをAPNサービスに送信
  • 必要に応じて通知に失敗したデバイストークンのフィードバックデータを取得

※実際の端末への配信は APN サービス側で面倒をみてくれるようになっています。

自前サーバーと APN サービス間の通信には、アプリ毎に作成した SSL 証明書とセキュアーな TLS (または SSL )を使います。最初はここら辺の手順をややこしく感じてしまうかもしれませんので、機会がありましたら後ほど別の記事にまとめてみたいと思います。

今回は自前サーバーから APN サービスに対して、メッセージを通知する部分の実装について記事にしてみようと思います。で、ここ最近はサーバー側を Rails で書いているので APN 関連の Gem を探してみたのですが、そんな中でこれいいなぁと思った Gem を二つほど紹介します。

まずは、

 Grocer <https://github.com/highgroove/grocer>
これは単体で動作して、インタフェースが素直で分かりやすいのがいいなぁと思いました。

簡単に使い方を紹介してみたいと思います。
まずは、APN サービスに接続するところです:

require 'grocer'

pusher = Grocer.pusher(
  certificate: "/path/to/cert.pem",
  passphrase:  "",
  gateway:     "gateway. sandbox.push.apple.com",
  port:        2195,
  retries:     3
)

パラメータの意味とか見たまんまなんで、特に説明は書きません。ちなみに certificate で指定する証明書を String で渡したい場合は certificate: StringIO.new(証明書の文字列) として指定できるようになっています。こういうところも、気が利いていていいですね。

実際にメッセージを作成して送信するところはこんな感じです:

notification = Grocer::Notification.new(
  device_token: "0b807784fac2f2dca7fce1..........",
  alert:        "Hello!",
  badge:        6,
  sound:        "default",
  expiry:       Time.now + 60*60,
  identifier:   1234
)

pusher.push(notification)

pusher で作った接続に対して notification を push するということで、これも見たまんまなので分かりやすいですね。APN サービスへの接続を使い回すのも、そのまま書けますね。

つづきまして、

ApnMachine <https://github.com/jnak/apnmachine>
これはさっきの Grocer とは違って、配信用のデーモンと Redis を別途動かすようになっています。プログラムから送られたメッセージは一旦 Redis に入ります。配信用のデーモンは Redis からメッセージを読み込んで APN サービスに送信する、という仕組みになっています。なので、APN サービスのサーバーや配信デーモンが落ちていても、メッセージは Redis にキューされているので再送できますよ!ということですね。

これも簡単にですが使い方を紹介してみます。
まずは、APN サービスにメッセージを送るデーモンの起動オプションです:

Usage: apnmchined [switches]
 --pem
    path/to/pem

 --pem-passphrase path/to/pem
    path to pem passphrase

 --redis-host [127.0.0.1]
    bind address of proxy

 --redis-port [6379]
    port proxy listens on

 --redis-url [redis://username:pas127.0.0.1:]
    url of proxy

 --log "var/log/apnmachined.log"
    the path to store the log

 --daemon
    to daemonize the server (include full path for pem files then)

 --apn-host "gateway.push.apple.com"
    the apn server host (use 'sandbox' to use apple sandboxes)

 --apn-port "gateway.push.apple.com"
    the apn server port 

 --help
    this message

ApnMachine では証明書をデーモン起動時に指定するようになっているので、アプリ毎にデーモンと Redis を起動することになります。

ちなみに Redis の key は apnmachine.queue で、こんな感じの LIST が入ってました:

redis 127.0.0.1:6379> lrange apnmachine.queue 0 -1
1) "{\"aps\":{\"badge\":6,\"alert\":\"Hello World!\",\"sound\":\"default\"},\"device_token\":\"5e01672.....\"}"
2) "{\"aps\":{\"badge\":6,\"alert\":\"Hello World!\",\"sound\":\"default\"},\"device_token\":\"0b80778.....\"}"

動作確認用にデーモンを起動するときのサンプルはこんな感じです:

$ apnmachined --apn-host 'gateway.sandbox.push.apple.com' --pem /path/to/foobar.pem

実際に運用するときには --daemon オプションを付けることになると思います。このオプションを付けたときは --pem オプションで指定する証明書ファイルはフルパスで指定しないとダメっす。

続きまして、プログラム側になります。
まずは実際にメッセージを作成して送信する前に、Redis サーバーのクライアントを渡しときます:

require 'apnmachine'
require 'redis'

redis = Redis.new(:host => 'localhost', :port => 6379)
ApnMachine::Config.redis = redis
# ApnMachine::Config.logger = Rails.logger

 メッセージを送信する部分はこんな感じです:

notification = ApnMachine::Notification.new
notification.device_token = "0b807784fac2f2dca7fce1.........."
notification.alert = "Hello!"
notification.badge = 6 notification.sound = 'default' notification.push

これもみたまんまで分かりやすいですね。
どちらもシンプルなインタフェースは分かりやすくて気持ちがいいです。

ということで ApnMachine で動作確認してみたところ...
あれ、メッセージが届かないんだが!!!という事態発生!
デーモンがエラーメッセージを...

E, [2012-12-21T17:09:24.083389 #49651] ERROR -- : Unable to handle: undefined local variable or method `host' for #<ApnMachine::Server::Client:0x0000000180c4a0>

よくよく調べてみたら issue に上がっていました。
これ -> <https://github.com/jnak/apnmachine/issues/7>

Bundler 使ってるんで Gemfile に

gem 'apnmachine', git: 'https://github.com/jnak/apnmachine.git', branch: 'master'

書いてインストールしたら動くようになりました。同じエラーメッセージを見ちゃった方は参考までに。

と、記事を書きながら引き続き ApnMachine の動作確認していたら UTF-8 のメッセージが届かないっぽい... Grocer では送れたのでエンコーディングか pack がらみで何かありそうです。調べてるので、原因がわかったら後ほど更新したいと思います。パーティーに遅れてしまう...

ということで、

ちょっと前までは apn_sender を使っていたのですが、こういうのも便利そうなので軽くですが紹介してみました。

で、ちょっと思ったのですが、

デーモンが Bundle Identifier と証明書のペアを持つようにして、通知するプログラム側で Bundle Identifier とデバイストークンとメッセージを送るようすれば、配信デーモンをまとめられて運用が楽になるかなーなんて思うんですがどうでしょう?そういうのがあるかもしれないんですが、無かったら作ってみるのも楽しそうですね。

ということで、やってみたいことがまた一つ増えたところです...
これもまた一つのクリスマスプレゼントなのかも知れません。 

ということで、みなさんごきげんよう!またお会いしましょう。