iPhoneとsocket.ioサーバを常時接続させる2

前エントリ iPhoneとsocket.ioサーバを常時接続させる1 からの続きです。
フォアグラウンド/バックグラウンド時における接続断と回復方法について調べて、

  • 圏外などで一度接続を失った場合でも電波受信状況が回復したら自動で再接続する

というのが出来るようにしてみます。

フォアグラウンド

フォアグラウンドでアプリが表示されている間はなんら制約もないので特に困った事はありません。
NNSocketIOでは接続失敗時の試行と接続断時の再接続がデフォルトで有効になっているため接続が切れてもすぐに再接続されます。
以下のNNSocketIOOptionsのプロパティとして調整可能ですが、一旦すべてデフォルトのままでいきます。

プロパティ名 意味 デフォルト値
retry 接続失敗時に試行をするかどうか YES
retryDelay 試行するまでの待ち時間初期値 3(sec) ※実施の待ち時間は試行毎に2倍づつ増える
retryDelayLimit 試行待ち時間の最大 1800(sec)
retryMaxAttempts 最大試行回数 -1 ※マイナスは無限大
connectionRecovery 接続断時に再接続するかどうか YES

またiPhoneが圏外となっている場合の試行は無駄なので圏外である場合は自動的に試行を中断、ネットワークが利用可能になってから再度試行を開始できるようにNNReachabilityを使用します。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // socket.ioクライアントを作成
    NSURL* url = [NSURL URLWithString:@"http://ホスト名:ポート番号"];
    NNSocketIOOptions* opts = [NNSocketIOOptions options];
    opts.enableBackgroundingOnSocket = YES;
    io = [NNSocketIO io];
    client = [io connect:url options:opts];
    [client on:@"connect" listener:^(NNArgs* args) {
        NSLog(@"接続しました");
    }];
    [client on:@"disconnect" listener:^(NNArgs* args) {
        NSLog(@"切断しました");        
    }];
    // ネットワークの状態をチェック
    reachability = [NNReachability reachabilityForInternetConnection];
    [reachability start];
    return YES;
}

NNReachabilityを開始しておくと圏外やフライトモードなどiPhoneのネットワーク状態が変化した際、NNReachabilityChangedNotificationという種類のイベントをNSNotificationCenterへ通知します。
NNSocketIOは同イベントのオブサーバとなっておりネットワークが使用できない間は試行や再接続を中断、使えるようになった時に再開するようになります。

以上でフォアグラウンド時においてはsocket.ioの接続が極力維持されるようになりました。
続いてバックグラウンドです。

バックグラウンド

フォアグラウンドの時と同じように接続が回復されると思いきやそうではありませんでした。
例えばバックグラウンドで接続中にsocket.ioサーバのnodeプロセスを終了させます。
想定ではiPhone側は再度socket.ioサーバに接続を試みて接続が成功するまで試行するはずでしたが、試行を2回繰り返した後でアプリは停止してしまいました。
試しにアプリをフォアグラウンドに戻すとまた試行を再開し始めました。

なぜアプリが停止したか?

バックグラウンドにおけるアプリとスレッドの関係、スレッドとRunLoopの関係を理解できていないのでここからは完全に自分の推測です。(良い書籍、ドキュメント等あったら教えてください)

まずアプリが停止ってどういう状態なのかですが、NNSocketIOでは接続断を検出するとGCDのdispatch_afterを使用してmain queueに再接続処理を呼び出すBlockを投入しています。
フォアグラウンドではmain queueに投入したBlockは以降のループで実行されるわけですがバックグラウンドでは実行されなくなっています。
つまり接続断に伴い試行を2回ほど繰り返した後RunLoopが停止したように見えます。
ちなみにバックグラウンドで動けない通常アプリ(beginBackgroundTaskWithExpirationHandlerを呼ばず10分の延命権利をもらっていない場合)はバックグラウンドに回ると約5秒程度でRunLoopが停止します。
今回の試行2回というのも約5秒程度です。
この事からvoipタイプのアプリもバックグラウンドでvoip用の接続が切れてしまうとそれ以降の寿命は一般アプリに準ずるのではないかと考えました。
voip接続を維持している間はiOSにvoipの魔法をかけられて魔法が切れると一般アプリに逆戻り。シンデレラ状態です。

上記を検証するためにbeginBackgroundTaskWithExpirationHandlerを呼ぶようにします。
仮定が正しければバックグラウンドに入った後の10分間は接続断が起きても問題なく再接続でき、10分以降はRunLoopが停止して再接続処理は実行されるなくなるはずです。

UIBackgroundTaskIdentifier bgTask;

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
}

上記コードを組み込んで再度アプリを実行。一旦接続完了後にアプリをバックグラウンドにした後、nodeのプロセスを落としてみます。

予想通りバックグラウンドに回った後の10分間は再接続を試行し続けました。そして10分過ぎると試行は停止しました。

voipタイプのアプリはどうやって再接続すれば良いのか?

ではvoipタイプのアプリはバックグラウンドにおいてどうやって接続を回復したら良いのでしょうか?どこかにガラスの靴があるはずです。

実はvoipタイプのアプリにはiOSから以下二つの特権を与えられます。

  1. voip宣言した接続はバックグラウンドでも維持できる
  2. 10分に一回10秒だけ能動的に動くことができる

1については今までその恩恵を受けてきました。
そして2です。これがバックグラウンドでも接続断を回復するための方法のようです。

以下のようにsetKeepAliveTimeoutで10分に1回実行したい処理が記述できます。
intervalが600(sec)としていますが600秒以下は指定できないようです。
ここで再接続の処理を実行すればバックグラウンドでも接続を回復できるというわけです。

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    [application setKeepAliveTimeout:600 handler:^{
        ※接続状態が切断だった場合に再接続する処理をここで呼び出す。
    }];
}

setKeepAliveTimeoutでhandlerを登録しておくと10分に1回handlerのBlockが実行され、10秒間RunLoopが回ります。
NNSocketIOの場合、再接続の実施要求は接続断の検出時にmain queueに積まれており、あとはRunLoopが回り始めてくれれば良いだけの状態となっているためBlockに再接続のための処理を記述する必要はありません。

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    [application setKeepAliveTimeout:600 handler:^{
        // 処理の記述は不要
    }];
}

以上でバックグラウンドにおいても接続が回復できるようになりました。
でもこの10秒で再接続ができなかった場合どうなるのでしょうか?
この場合は次の10分まで待たないといけません。つまり1時間で再接続できるチャンスは6回だけ。
もし運悪くたまたまその6回とも圏外に居た場合、1時間ずっと接続されていない状態となります。

まあバッテリの事を考えると10分に1回というのは妥当だと思いますが、もうすこしあがいて
次回はバックグラウンドでの再接続の機会を10分に1回の時間ベースではなくネットワークの状態、Reachabilityの回復タイミングで出来ないか調べてみます。