APNsの概要と関連ツール群

Apple Push Notification Service(APNs)は、ソフトウェア開発者(プロバイダ)から受け取ったメッセージを安全な方法でデバイスにプッシュ通知するサービスである。

プッシュ通知までの流れ

  1. プロバイダはデバイストークンとペイロードから成る通知メッセージを作る。
  2. プロバイダはその通知メッセージをAPNsに送信する。
  3. APNsは受け取った通知メッセージのデバイストークンから配信先のデバイスを特定し、通知メッセージを配信する。

接続を確立するまでの流れ

  1. APNsとデバイス間で認証を行う(システムによって行われるため、開発者が実装する必要はない)。
  2. APNsとプロバイダ間で認証を行う。
    1. プロバイダがAPNsからサーバ証明書を取得し、検証する。
    2. プロバイダがプロバイダ証明書をAPNsに送信する。
  3. デバイストークンを生成しプロバイダと共有する。
    1. アプリケーションがリモート通知の登録を行う。
    2. システムがリモート通知の設定を行い、デバイストークンをアプリケーションデリゲートに渡す。
    3. アプリケーションがデバイストークンをプロバイダに送信する。
  4. プロバイダからの通信すべてにデバイストークンを添付させる。

APNs関連ツール

AFNetworkingでおなじみのmatttさんが様々な関連ツールを開発している。それぞれのツールが上で説明した全体像の中でどのような位置づけなのか整理した。

houston

https://github.com/nomad/houston

プロバイダからAPNsに向けて通知メッセージを送るためのクライアント。Ruby製。上記の通り、プロバイダがAPNsにメッセージを送るには、(1)APNsとの間で認証を行う、(2)配信先のデバイストークンを取得する必要がある。なので、houstonでもメッセージを送る際にAPNsのサーバー証明書と配信先のデバイストークンを設定する必要がある。

rack-push-notification

https://github.com/mattt/rack-push-notification

iOSアプリからデバイストークンを受け取りDBに保存するRackアプリケーション(Sinatraベース)。上述の通り、iOSアプリはAPNsからデバイストークンを取得したあと、プロバイダと共有する必要がある。rack-push-notificationはそのデバイストークンを受け取るためのAPIを用意する。

Orbiter

https://github.com/mattt/Orbiter

iOSアプリからデバイストークンを送信するためのクライアント。今までの2つのツールはプロバイダ側のツールだったが、OrbiterはiOSアプリ側のツールである。取得したデバイストークンをプロバイダに送信する処理を簡略化できるようだ。

まとめ

これらの関連ツールを使ったプッシュ通知のフローは以下のようになる。

  1. iOSアプリはリモート通知の登録を行い、APNsからデバイストークンを取得する。
  2. iOSアプリはOrbiterを使ってデバイストークンをプロバイダに送信する。
  3. プロバイダはrack-push-notificationを使って用意したAPIからデバイストークンを受け取りDBに保存する。
  4. プロバイダはhoustonを使ってプッシュ通知をAPNsに送信する。
  5. APNsはプロバイダ証明書とデバイストークンからプッシュ通知を転送するデバイスを特定しプッシュ通知を送る。

参考

技術メモの管理

今後ちゃんと学んだことをメモに残しておこうと思い直し、メモを管理する仕組みを整理した。

保存場所

Dropbox/Documents/notes/以下。複数のPC間で簡単に共有したいのでDropboxで管理する。

メモを書く

まず以下のような関数を用意した。

note() {
    local note_path=$HOME/Dropbox/Documents/notes/$1
    if [ ! -e $note_path ]; then
        touch $note_path
    fi
    open $note_path
}

note <ファイル名>でメモを書き始めることができる。

メモはすべてmarkdown形式で、エディタはvimを使うことにした。vim-templateというプラグインを使うことで、notes/*.mdにマッチするファイルを以下のようなテンプレートで開くようにした。

---
title: <%= expand('%:t:r') %>
date: <%= strftime('%Y-%m-%d') %>
---

<+CURSOR+>

<%=%>で囲われたコードはVim Scriptとして評価されて展開され、<+CURSOR+>の位置をカーソルの初期位置としてファイルが開く。このような設定はvim-templateのヘルプにあるのでそちらを参照してほしい。

メモの検索

agとpecoを使う。agは高速なgrepということで、任意の文字列を含むファイルを検索する。

onote() {
    echo $(ag -l $1 $HOME/Dropbox/Documents/notes/) | peco | xargs open
}

template内で日付を必ず入れるようにしているので、日付で検索することもできるようになった。

ChefでRaspberry Piをセットアップする

仕事で複数台のRaspberry Piをセットアップすることになったので、Chefを使ってセットアップを自動化することにした。Chef、Vagrant、Serverspecなどいろいろな周辺ツールの全体像を整理したり、それらを使ったワークフローを体験できてよかったので、ブログとして残しておく。

また、セットアップに使ったChefのレポジトリはgithubにホストしてあるので参考にどうぞ。

https://github.com/naoty/chef-repo

今回、Chefで自動化したのは以下の通り。

  • apt-getの更新
  • gitのインストール
  • rbenvを使ってRuby 2.0.0-p247をインストール
  • nodebrewを使って最新安定版のnode.jsをインストール
  • Wiringpi(GPIOを簡単に操作するためのライブラリ)のインストール
  • mjpg-streamer(Webカメラを使ったストリーミングのためのライブラリ)のインストール

1. Vagrantで仮想環境を用意する

いきなりRaspberry PiにChefを使って環境構築を行うのは失敗したときにやり直すのが大変。なので、Raspberry Piに近い仮想環境を用意して、そこでChefを使ったセットアップを試行錯誤したい。そういうときに便利なのがVagrant。Vagrantを使えば簡単に仮想環境を作ったり壊したりできるので、失敗してもすぐにやり直せる。

今回、重要だったのがRaspberry Piに近い仮想環境を用意することだった。Vagrantにはboxという仕組みがあって、CentOSとかUbuntuとかいろんなOS、CPUに合わせたひな形がたくさん用意されている。通常はここにあるboxを使えばいいんだろうけど、Raspberry Piに近いboxがなかった。Raspberry Piに近いboxを探したところ、これがよさそうだったので使うことにした。

$ git clone https://github.com/nickhutchinson/raspberry-devbox raspberry_pi
$ cd rasbperry_pi
$ vagrant up

以上、これだけでRaspberry Piに近い仮想環境を用意することができた。

2. Chefのセットアップ

ここはいろんなところで解説されてる通りに行っただけ。

$ vagrant ssh-config --host vm-raspberry_pi >> ~/.ssh/config
$ knife solo init chef-repo
$ knife solo prepare vm-raspberry_pi

3. クックブックの作成とテスト

ここから環境構築の手順をコードとして記述していく。クックブックの書き方については「入門ChefSolo」やOpscodeの公式ドキュメントを参考にした。このときの注意点としては、Raspberry PiはRubyやnode.jsのインストールに非常に時間がかかるため、timeoutをとても長くする必要がある。数時間はかかると考えた方がいい。

書いたクックブックを実行する前にVagrantをサンドボックスモードにしておく。こうすると、失敗したときに実行した部分だけやり直すこと(ロールバック)ができる。サンドボックスモードにするためにはsaharaというVagrantのプラグインが必要なのでインストールしておく。

サンドボックスモードをオンにしてクックブックを実行したあと、本当に期待した通りに環境構築できたかどうかをServerspecを使ってテストする。Serverspecにはいくつかテストを実行する方法があるようだけど、今回はSSHでログインしてテストを実行する形式を採った。テストを通らなかった場合は、saharaを使ってロールバックしてやり直す。テストが通った場合は、saharaを使って変更を確定させる(コミット)。

これをサイクルさせながら、どんどんクックブックを追加していく。以上をコマンドで表すとこんな感じ。

$ knife cookbook create ruby -o site-cookbooks
$ vi site-cookbooks/ruby/recipes/default.rb
$ vi nodes/vm-raspberry_pi.json
$ vi spec/vm-raspberry_pi/ruby_spec.rb
$ vagrant sandbox on
$ knife solo cook vm-raspberry_pi
$ rspec
$ vagrant sandbox commit

4. Raspberry PiをChefで環境構築する

仮想環境での環境構築が完了したら、いよいよ本物のRaspberry Piにクックブックを適用する。そのためにはnodes以下に本物用の設定を追加するだけでいい。

$ vi nodes/raspberry_pi.json
$ knife solo cook raspberry_pi

RubyのWebSocketサーバー「pingpong」を作った

最近、「Working with TCP Sockets」って本を読んだ。Rubyでソケットと戯れつつ、7つくらいのWebサーバーのアーキテクチャを概観できるいい本だった。で、その中にイベント駆動モデルの実装とかノンブロッキングIOの実装について紹介されてて面白かったので、練習がてらWebSocketサーバーを作ることにした。

PingPong

f:id:naoty_k:20131011013521g:plain

https://github.com/naoty/pingpong

卓球ハウスっぽい名前にした。数日で作ったので、他のクライアントへpush通知を行うことしかできない。たぶん大きいデータも送れない気がする。

WebSocketサーバーの実装とは

まずはRFC 6455のサーバーに関する部分を読んだ。最低限必要な部分をRubyで実装していった。例えば、以下のコードはHandshake(websocket接続の確立)の際にサーバーがクライアントに返すレスポンスヘッダーを作っている。

def response_headers
  [
    ["Upgrade", "websocket"],
    ["Connection", "Upgrade"],
    ["Sec-WebSocket-Accept", signature]
  ]
end

def signature
  value = @header["Sec-WebSocket-Key"] + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
  hash = Digest::SHA1.digest(value)
  Base64.strict_encode64(hash)
end

ご覧のとおりハードコーディングがたくさん出てくる。RFCを読むと、このヘッダーにはこの値を入れなさいって書いてあることが多い。なので、それぞれの値の意味はわからないけどとりあえずRFCに従ってハードコーディングしている。signatureというメソッドはあるヘッダーの値をRFCで以下のように定められた形式で生成している。(余談だけど、ここでBase64.encode64を使って小1時間ハマった。これは改行コードを入れるためここでは使えない。)

A |Sec-WebSocket-Accept| header field. The value of this header field is constructed by concatenating /key/, defined above in step 4 in Section 4.2.2, with the string "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", taking the SHA-1 hash of this concatenated value to obtain a 20-byte value and base64-encoding (see Section 4 of [RFC4648]) this 20-byte hash.

イベント駆動モデルとノンブロッキングIO

push通知はイベント駆動モデルというアーキテクチャを使って実装した。イベント駆動モデルはマルチプロセスやマルチスレッドとは違ってシングルスレッドで多数のリクエストを並行処理する。具体的には、websocket接続の確立に成功したソケットを配列に入れておき、ループ内でそれらのソケットにread/writeしていく。このとき、read/writeがブロッキングしてしまうとすべての処理がそこで止まってしまうので、read/writeの前にselect(2)等を使ってread/write可能なソケットだけ選択してread/writeを行う。これがノンブロッキングIOだと思う(だよね…?)。

実際のコードは以下の通り。

def start
  @sockets = {}
  @message_queue = []

  loop do
    to_read = @sockets.values << @server
    to_write = @sockets.values
    readables, writables, _ = IO.select(to_read, to_write)

    readables.each do |socket|
      if socket == @server
        establish_connection
      else
        begin
          request = socket.read_nonblock(CHUNK_SIZE)
          message = Frame::Request.new(request).message
          # the message may be passed to a web application.
          @message_queue << Message.new(socket.fileno, message)
        rescue EOFError
          @sockets.delete(socket.fileno)
        end
      end
    end

    message = @message_queue.shift
    next if message.nil? || message.empty?

    writables.each do |socket|
      if socket.fileno != message.from
        data = Frame::Response.new(message.body).data
        socket.write_nonblock(data)
      end
    end
  end
end

感想

WebSocket、イベント駆動モデル、ノンブロッキングIO…という言葉はよく耳にしてきたけど理解したとは言えなかった。実際にWebSocketサーバーを書いてみると、コードに基づいて何が行われているのか正確に理解することができた。push通知も何やら凄そうな響きがするけど、実際に実装してみると特に難しいことはしていなかった。また、websocketの弱点と言われている、CPUヘビーな処理がなぜ弱点なのかも合点がいった。シングルスレッドで処理しているので、例えばレンダリングのような重い処理がひとつでも走ると、全体に悪影響が出るということだと理解した。

iOS用グラフ描画ライブラリを書き始めた

卓球ハウスに来てから楽しすぎてコード書いてないことに気づいたので、なんか書こうと思った。最近はセンサーのデータをどうにかiOSに転送したくていろいろ試しているのだけど、送られたデータを表示するときに何かとグラフを描画したくなる。

iOSでグラフを描画したいとき、まずCorePlotを試してみる。だけど、ドキュメントがあんまりないし、見た目がダサい。githubじゃないのもなんかなぁと思ってすぐにやめてしまう。で、CorePlotはやめてwebviewでJSのグラフ描画ライブラリを使うことにする。highchartsが便利なのでよく使う。でも、やっぱりいちいちHTMLとJSのファイルを用意しなくちゃいけないのがダルいとは思っていた。

そこでせっかくだしグラフ描画ライブラリをObj-Cで実装してみることにした。

naoty/NTChartView

f:id:naoty_k:20130806230903p:plain

まだぜんぜんできてない。かろうじて上のような折れ線グラフを表示できるレベル。負値すらうまく表示できない。もちろんテストもない。地味に頭を使う実装が多い気がする。

将来的には折れ線グラフだけじゃなくて円グラフと棒グラフも実装したい。けっこう時間かかりそうなので、ヒマなときにゆっくり実装したい。