Tech Blog

グローバルな家族アプリFammを運営するTimers inc (タイマーズ) の公式Tech Blogです。弊社のエンジニアリングを支える記事を随時公開。エンジニア絶賛採用中!→ https://timers-inc.com/engineering

Pairy : Ratchet PHP & ZeroMQ でリアルタイム通信

CTOの椎名アマドです。
昨日弊社Pairyは1億円調達の発表を行ないました!
色々な方から嬉しいメッセージなどが届いて嬉しい限りです。
ちなみにエンジニア採用を本格的に行なってるので、興味ある人は http://timers-inc.com を見てみてください!



さて、今回はリアルタイム通信に関してです。
前々から我々は Pairy にwebsocket使ったリアルタイム通信を導入したいねと言っていて、最近やっと導入に成功しました。
Ratchet と ZeroMQ という2つのライブラリの組み合わせによって、比較的簡単に実装できてます。

設計の概要

設計はざっくりと:

f:id:timers-tech:20131219190633p:plain

1. ネイティブアプリAが投稿などのアクションを行ない、webサーバーにリクエスト送信
2. webサーバーがDBサーバーに書き込み
3. webサーバーがsocketサーバーにメッセージをZeroMQ経由で送信
4. socketサーバーが、ZeroMQメッセージ内で指定された宛先にsocketメッセージを送信
5. socket接続済みのネイティブアプリBがsocketメッセージを受信して、更新等の処理を行なう

こんな感じで組んでます。
では Ratchet と ZeroMQ とは実際どんなものなのか?見ていきましょう。



Ratchet, Websocket について

Ratchetとは、PHPのWebsocketライブラリです。
比較的簡単にWebsocketサーバーが立てられる上に、WAMP(後述)インターフェースやセッション管理(Symphonyのセッションしか対応してない?)
等も用意してるので、out of the box でいきなり作れるのが良いですね。
公式サイト上でのチュートリアルが充実してるのも嬉しいです。
むしろZeroMQ活用はほぼここのチュートリアルから引っ張ってきました。

我々はWAMPというWebsocketプロトコルを利用します。
WAMPには RPC と Pubsub という2種類のパターンが含まれているのですが、今回は Pubsubパターンのみを使います。
Pubsubとは単純な Publish(配信) & Subscribe(購読)のパターンですね。
誰かがあるトピックにメッセージをpublish(配信)をするたびにそのトピックのsubscriber(購読者)全員にメッセージが配信されるというパラダイムです。

我々の場合は、Pubsubと言いつつ、1ユーザーにつき1トピックを割り当てています。
もともとは1カップル毎に1トピックにsubscribeさせる形で考えていたのですが、送信するメッセージに柔軟性を持たせたい場合に不都合が起きてきそうだなと思い1ユーザー1トピックにしました。


さて、これがあれば、あとはネイティブアプリに組み込んで終わりですよね。
ネイティブアプリで Websocket接続を確立させて、チャット等の投稿をするたびに相手のトピックにメッセージをpublishさせていく、と。
……我々の場合はその方向には進みませんでした。

ネイティブ同士で直接やり取りさせるだけで成立するアプリもあるのは確かですが、
我々はこのpublishをネイティブが直接行なう実装はしたくありませんでした。
理由としては、我々は既にサーバー側で通知処理をがっつり組んでるためです。
Pairyでは、何かのアクションが行なわれると、そのペアリング相手に対して:
1. ヘッダー上のお知らせ数字がインクリメントされる
2. お知らせ情報のDBにお知らせ内容を挿入
3. プッシュ通知(相手によってAPNSかGCMかを出し分け)を行なう

という処理が走ります。

f:id:timers-tech:20131219190736p:plain

ここでSocket通信だけはこの通知ライブラリを介さずにネイティブ同士が直接行なうと、通知関連の処理がクライアントとサーバーで分散してしまい、今後のメンテ性や柔軟性のことを考えると望ましくないです。
仮に先々PC版やスマートフォンWeb版を作りたいとなった時も、クライアント毎に通知処理を書き直すことなりますし。

というわけで、「サーバーから直接クライアントにsocketメッセージを送りたいな」と思ったのですが、Webサーバーがsocket接続を常に確立しておくのは意味わからないですよね。
そうなると、Webサーバーがsocketサーバーに何かしらの方法でメッセージを送ってsocketサーバーがクライアントにpublish、という解が適切です。

ここで登場するのが、ZeroMQです。


ZeroMQ について

ZeroMQは、非常に高速かつ軽量かつ柔軟なメッセージング通信用フレームワークです。
N対Nの通信パターンをいくつも(それこそPubsubなども)網羅していて、
キューイングや接続周りのややこしい処理も全て内部で面倒みてくれている、
プラットフォーム同士のメッセージのやり取りを行なうのに非常に使い勝手が良いフレームワーク&ライブラリです。

(出来る事を全て網羅するとかなりの量になるので、上記程度の簡単な説明にとどめておきます。)

今回は我々はこれの PUSH/PULL パターンを使って、WebサーバーからSocketサーバーにメッセージをばしばし投げ込んでいくようにします。 PUSH/PULL パターンは、Pubsubに似て受信登録をした人(PULL)に対して送信(PUSH)を行なうというモデルなのですが、
PULL接続を確立しているクライアントが複数いる場合、PUSHメッセージはラウンドロビン式でひとりずつしか受け取りません。
PUSHしたメッセージの受信者が必ず1人だけ、というのが保証されているパターンなんですね。

我々の場合はPUSHする側が多数(Webサーバーの数)で、PULLしているのが現状では1人(Socketサーバー1台)という構成です。


幸いPHPではZeroMQのPECLが用意されてるので、導入も利用もめっちゃ簡単です。
公式サイトに従ってぱぱっと pecl install すれば、実際の利用は

		$context = new ZMQContext();
		$socket = $context->getSocket(ZMQ::SOCKET_PUSH);
		$socket->connect($host);
		$socket->send($message);

こんな感じで済みます。
我々の場合は $message は欲しい情報を突っ込んだJSONにしてるので

        $payload = array(
            'user_id'  => $user_id,
            'target'    => 'chat' 
        );
        $message = json_encode($payload);

という具合で送ってます。
あとは、受け手側のSocketサーバーで、

    public function publishEvent ($event) {
        $eData = json_decode($event, true);

        $topic = $this->_getTopic($eData['user_id']);
        if ($topic) {
            $topic->broadcast($eData);
        } else {
            echo("no topic... ");
        }
    }
	
    protected function _getTopic ($user_id) {
        $topicKey = $this->keyPrefix.$user_id;
        $topic = $this->users[$topicKey];
        return $topic;
    }
		

このようにJSONをパースして、その中の値(この場合はuser_id)に基づいたトピックを拾って、そこにbroadcast() というpublish用メソッドでメッセージを送信しています。


あとがき

上記Socketメッセージまで送ってしまえば、あとはそれをクライアント(ネイティブアプリ)で受け取って好きにすれば良いです。
例では "target" というキーに「どの種類のアクションだったか」を入れているので、例えばここに入った情報に基づいてネイティブアプリはどのデータを更新すれば良いかわかりますよね。
ここにチャット文字列のデータを入れて直接ネイティブで受け取ったデータをすぐに表示させる、などの実装をしても素敵ですね。

これで通信処理ロジックも全てサーバーに集約できて、スムーズ且つ安定したリアルタイム通信の仕組みが出来ました。

気をつける事は、

  • Socketサーバーの取り扱い

- 同時接続クライアント数が多すぎたり、ZeroMQメッセージがたくさん来すぎて負荷がかからないかをきちんと監視して、プロセスが落ちた場合の再起動などを正しく行なうように

  • クライアントのSocket接続の取り扱い

- 電波状況や、バックグラウンドに遷移した時など、きちんと切断〜再接続の処理が働いているように

ぐらいです。
WebサーバーのZeroMQ送信処理に関しては、PECLライブラリが優秀そうなので今のところあまり問題には当たっていません。
むしろ、何か注意点などあれば是非コメントなどで教えて頂けると幸いです。



この仕組みは当たり前ですがWebアプリケーションでも使えるので、比較的簡単に自分のWebサイトにもリアルタイム更新の仕組みが導入できて素敵なんじゃないでしょうか。
この記事が参考になってくれると嬉しいです。


================

子育て家族アプリFamm、カップル専用アプリPairyを運営する Timers inc. では、現在エンジニアを積極採用中!急成長中のサービスの技術の話を少しでも聞いてみたい方、スタートアップで働きたい方など、是非お気軽にご連絡ください!
採用HP : http://timers-inc.com

Timersでは各職種を積極採用中!

急成長スタートアップで、最高のものづくりをしよう。

募集の詳細をみる