iOS7から追加された Multipeer Connectivity は、複数台での擬似的なP2P通信を行うための仕組みです。これにより近距離でのデータ通信を、サーバを介さずに行うことができます。

 この Multipeer Connectivity は、NSData 型のデータを送信することができます。そのため、シリアライズ・デシリアライズの機構さえきちんと作成することができれば、画像や音声など、実質どのようなデータでも送ることが可能です。

 これらのメリットを活かす事により、リアルタイムのチャット機能や、更にはホワイトボードの画面を共有するようなアプリ、簡単な資料を共有することだってできてしまうでしょう。

 では早速この新機能を試してみましょう!

Multipeer Connectivityで通信を行うまでの流れ

   Multipeer Connectivity を利用して通信を行う方法について説明します。

 はじめに、この機能が発信するイベントを受け取れるよう、以下のデリゲートを実装します。

  • MCSessionDelegate
  • MCBrowserViewControllerDelegate
  • MCNearbyServiceAdvertiserDelegate
  • MCNearbyServiceBrowserDelegate

 各デリゲートについては、後ほど詳しく説明します。

 次に、データの送受信を行うまでの手続きを行います。

  1. 通信を行う基礎となる Session を作成する
  2. 招待側と受ける側に分かれそれぞれ初期設定を行う
  3. 招待側では見つけた Peer に対して招待を行う
  4. 受ける側では、招待を受け取った時にその招待された Session に参加するかどうかの返答をする 

 各手順について詳しく見て行きましょう。  

Sessionを作成する

 Multipeer Connectivity における送受信は、MCSession クラスのインスタンスを介して行います。
 生成、初期化のフローをいかに示します。

 //Peer を作成
 MCPeerID *mPeerID = [[MCPeerID alloc] initWithDisplayName: name];

 //セッションを初期化
 MCSession mSession = [[MCSession alloc] initWithPeer:mPeerID];   

 //デリゲートを設定
 mySession.delegate = self;

 まずは、Peer を作成します。そのため使用することができるメソッドが次になります。

  • initWithDisplayName:

 メソッドに渡す引数には文字列を指定します。この文字列は、どの Peer であるかの識別をすることができるものであれば何でもかまいません。
 ここで作成した PeerID を使用して Session を作成します。この時に、Session を作成する方法が次の2種類になります。

  • initWithPeer
  • initWithPeer:securityIdentity:encryptionPreference:

 この2つのうち initWithPeer は先ほど作成した Peer を引数として渡すだけです。
 initWithPeer:securityIdentity:encryptionPreference:は、認証を行うための情報を配列として securityIdentity に、暗号化を使用するかどうかを決定するための列挙子を encryptionPreference にそれぞれ渡すことで、セキュアな通信を行うことのできる Session を作成するメソッドです。

 基本的には initWithPeer: を使用すればよいでしょう。

オブジェクト 説明
Session 送受信を行うための機構
Peer 自身や相手を特定するために使うIDのようなもの

 ここで作成した Peer と Session は表で書いたように、データの送受信を行う時や、他のクラスのメソッドを使用する時などにも必要となるので大事にとっておいて下さい。
 作成した Session のインスタンスに MCSessionDelegate を設定します。
 このデリゲートを設定することで、セッションの状態変更時や、データ受信時に、特定のメソッドが呼ばれるようになります。データの送受信については後ほど詳しく説明します。ここでは、自分のセッションの状態を管理するために呼ばれるデリゲートメソッドについて説明します。

-(void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state{

    //sessionのステータスが変更された時の処理を行う。

}

 このデリゲートメソッドは、先ほど作成した Session の状態が変更された時に呼ばれます。この時に渡ってくる MCSessionState は次の値を持つ列挙子になっています。

列挙子 説明
MCSessionStateNotConnected session は接続されていない
MCSessionStateConnecting session は接続中である
MCSessionStateConnected session の接続が完了した

 表を見てもわかるように、MCSessionStateConnected が呼ばれると接続に成功したということになります。よって、この didChangeState が MCSessionStateConnected になるまでこのメソッドを見張る必要があります。

ホストとクライアント

 Multipeer Connectivity を使った通信では、Peer を探し自分の Session へ招待する側(ホスト)と、招待される側(クライアント)に分かれます。各々を設定するための方法を順に説明します。

 まずは、ホストになる場合の例です。

NSString mServiceType=@"hoge_fuge";


MCNearbyServiceBrowser *nearbyBrowser = [[MCNearbyServiceBrowser alloc]
                                        initWithPeer:mPeerID
                                         serviceType:mServiceType];

nearbyBrowser.delegate = self;

[nearbyBrowser startBrowsingForPeers];

 ホスト側の設定を行う時に利用するクラスが MCNearbyServiceBrowser です。このクラスのインスタンスを作成するときに必要なメソッドが initWithPeer:serviceType: になります。

引数名 引数の型 説明
initWithPeer MCPeerID Session を作成するときに使用した MCPeerID のインスタンス
serviceType NSString Peer を検索する際に使用する文字列

 ホストは、ここで指定した ServiceType を使用して、クライアントの Peer を検索することになります。    インスタンスを作成したらそこにデリゲートを設定します。ここでは MCNearbyServiceBrowserDelegate を設定します。これにより、次の2種類のデリゲートメソッドが呼ばれるようになります。

メソッド 説明
browser:foundPeer:withDiscoveryInfo: Peer を発見した際に呼ばれるメソッド
browser:lostPeer: Peer を見失った際に呼ばれるメソッド

 これらのメソッドにより、Session に接続している Peer の管理を行うことができるようになリます。そのためデリゲートを設定したらこれらのメソッドも実装しておきましょう。このメソッドで行う詳しい処理内容に関しては後ほど Peer への招待状の項で説明したいと思います。

 最後に、startBrowsingForPeers を呼び出し、実際に Peer の検索を始めます。

  • ブラウザビューコントローラ

 Session の検索中、ホスト側に表示させる画面を Multipeer Connectivity は提供してくれています。そのクラスが MCBrowserViewController になります。
 この画面は、発見した peer を自動的に追加し、招待を送ることができます。
 ブラウザビューコントローラを表示させるためには次のように記述します。

//ブラウザビューコントローラのインスタンスを作成
MCBrowserViewController *browserController = [[MCBrowserViewController alloc]
                                        initWithServiceType:serviceType                   
                                                    session:mSession];
//デリゲートを設定                                                                                       
browserController.delegate = self;

//ブラウザビューコントローラを表示
[self presentViewController:browserController animated:YES completion:nil];

 まずは、MCBrowserViewController のインスタンスを作成します。そのために initWithServiceType:session: メソッドを呼び出します。

引数 引数の型 説明
serviceType NSString ホスト側の設定を行う際に使用したものと同じ文字列
session MCSession 前項で作成した自分自身のセッション

 次にブラウザビューコントローラへ MCBrowserViewControllerDelegate を設定します。
 ブラウザビューコントローラを表示させると、画面上部に Cancell と Done 2つのボタンが表示されます。これらのボタンがタップされた時に、対応したデリゲートメソッドが呼ばれます。

 

ブラウザビューコントローラ

メソッド 説明
browserViewControllerDidFinish: Done ボタン押下
browserViewControllerWasCancelled: Chancell ボタン押下

 デリゲートを設定したら、ブラウザビューコントローラの挙動を制御するためにこのメソッドを実装しておきましょう。

 最後に、ブラウザビューコントローラを表示します。この時に使用するメソッドは、MultipeerConnectivity に特別に用意されているものではありません。単純に作成したビューに画面遷移します。よって、実装環境等により差異はあるかと思いますが、presentViewController 等のメソッドを使用し、画面を表示します。

 次にクライアントになる場合について説明します。

NSString mServiceType=@"hoge_fuge";


MCNearbyServiceAdvertiser *nearbyAdvertiser = [[MCNearbyServiceAdvertiser alloc]
                                                initWithPeer:mPeerID
                                               discoveryInfo:nil
                                                 serviceType:mServiceType];

nearbyAdvertiser.delegate = self;

[nearbyAdvertiser startAdvertisingPeer];

 クライアント側の設定を行う時に利用するクラスが、MSNearbyServiceAdvertiser です。このクラスのインスタンスを作成するためのメソッドが、initWithPeer:discorveryInfo:serviceType: になります。

引数 引数の型 説明
initWithPeer MCPeerID Session を作成する際に使用した MCPeerID のインスタンス
discoveryInfo NSDictionary ブラウザ側で使用する情報。特に情報が無いならば、nil。
serviceType NSString ホスト側が Peer の検索を行う際に対象とする文字列

  serviceType には、検索するホスト側で設定しているものと同じ文字列を指定します。これにより、ホスト側から探すことができるようになります。
  今回は説明の都合上クラス内で定義した変数を使用していますが、Manager クラスなどに public な変数として置いておくのがいいと思います。
 インスタンスを作成したら、 MCNearbyServiceAdvertiserDelegate を設定します。これにより、次のデリゲートメソッドが呼ばれるようになります。

  • advertiser:didReceiveInvitationFromPeer:withContext:invitationHandler:

 このメソッドで行う詳しい処理内容については、後ほど招待状への返事の項で説明しようと思います。  
 最後に startAdvertisingPeer メソッドを使用すると、実際に告知が開始されます。

Peerへの招待状

 Multipeer Connectivity を用いて通信を行うには、ホスト側が発見した Peer を自分のセッションへ招待する必要があります。それには、先ほどホストの初期設定を行ったときに設定したデリゲートから呼び出されるメソッドを利用します。ホスト側が、Browsing を開始した後、Peer を発見した時にbrowser:foundPeer:withDiscoveryInfo: メソッドが呼び出されます。これの中で発見した Peer に対して招待を送ることで、自分のセッションへ Peer を追加することができます。

-(void)browser:(MCNearbyServiceBrowser *)browser foundPeer:(MCPeerID *)peerID withDiscoveryInfo:(NSDictionary *)info{

    //発見した Peer へ招待を送る
    [browser invitePeer:peerID toSession:mSession withContext:nil timeout:0];

}

 デリゲートメソッドを上記のように実装することで、発見した Peer へ招待を送ることができます。

 招待を送るために使用するメソッドが invitePeer:toSession:withContext:timeout: です。

  • invitePeer には、招待する対象の Peer を渡します
  • toSession には自分自身の Session を渡します
  • withContext は送る招待についてさらなる情報を peer へ提示するためのデータを渡すものですが、特に必要でない場合には nil でもかまいません
  • timeout には タイムアウトするまでの時間を設定します。これは秒単位で設定することができ正の値でなくてはなりません。 0 を設定した場合はデフォルトの値 (30秒) が適用されます。

招待状への返事

 ホスト側で招待を送る方法を説明しましたが、その招待はどこに届くのでしょうか?

 その答えは、クライアントの初期設定を行った時に設定したデリゲートから呼び出されるメソッド、advertiser:didReceiveInvitationFromPeer:withContext:invitationHandler: です。このメソッドの中で送られてきた招待へ返答する事ができます。

 実際に返答するには次のように記述します。

-(void)advertiser:(MCNearbyServiceAdvertiser *)advertiser didReceiveInvitationFromPeer:(MCPeerID *)peerID withContext:(NSData *)context invitationHandler:(void (^)(BOOL, MCSession *))invitationHandler
{

    //招待を受けるかどうかと自身のセッションを返す
    invitationHandler(YES,mSession);

}

 ここで渡される invitationHandler に自身のセッションのインスタンスと、招待された Session に参加するかどうかの Bool 値を与えることで、招待へ返答することができます。

 ここまでの流れを簡単にまとめた図が以下になります

 ホストクライアント間の接続シーケンス図

データの送受信

 これで通信を行う準備ができましたので、ようやくデータの送受信を行うことができます。ちなみにデータを送信する際には Session のインスタンスを、データの受信には SessionDelegate のデリゲートメソッドをそれぞれ使用することになります。

送信

 Multipeer Connectivity では、データの送信方法が3種類用意されています。

メソッド 説明
sendData:toPeers:withMode:error: NSData を送ることのできるメソッド
sendResourceAtURL:withName:toPeer:withCompletionHandler: NSURL オブジェクトを送信するメソッド
startStreamWithName:toPeer:error: ストリームとしてデータを送るときに使用するメソッド

 送るファイルによってこれらのメソッドを使い分けることでいろいろなデータを送信することができます。
 今回は、sendData:toPeers:withMode:error: を使用し、データを送信する方法について説明したいと思います。

NSError *error = nil;
//送信する文字列を作成
NSString *str = self.mUITextField.text;

//NSData へ文字列を変換
NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding];

//送信先の Peer を指定する
NSArray *peerIDs = mSession.connectedPeers;

[self.manager.session sendData:data
                      toPeers:peerIDs
                      withMode:MCSessionSendDataReliable
                             error:&error];

 送信する時に使用可能なデータの型が NSData であるため、送信するデータを NSData へ変換します。

 NSArray 型のデータとして作成している peerIDs には送信先となる複数の Peer を設定します。チャット系のアプリを作成しているとすべての Peer にデータを送信することがあると思いますが、接続中の Peer を1つずつ NSArray に追加するのは面倒です。
 そこで、MCSession クラスには、今現在接続しているすべての PeerID を含む NSArray を保持しているプロパティがあります。

  • connectedPeers

 プロパティ自体は readonly になっているため、直接ここにデータを書き込むことはできません。このプロパティを使用することで、全 Peer へのデータ送信を比較的簡単に処理することができます。

 今回使用している sendData:toPeers:withMode:error: には以下の様な引数を渡します。

引数 引数の型 説明
sendData NSData エンコーディング済みの送信データ
toPeers NSArray 送信先のMCPeerを格納した配列
withMode MCSessionSendDataMode データの重要度を表す列挙子
error NSError 送信失敗時に取得するエラーメッセージを入れるポインタ

  withMode は、以下の2つから選ぶことができます。

列挙子 説明
MCSessionSendDataReliable 必要に応じてデータを再送し、データの配信を保証しなくてはいけないような重要な送信を行う時に使用される。
MCSessionSsendDataUnreliable ソケットレベルのキューに入れることなく即時に送信する。リアルタイムのゲームデータなどを送信するときに使用される。

 これで、受信側へ自身のメッセージを送信することができるます。

受信

 MCSessionDelegate が呼び出すデリゲートメソッドには、先ほど送信のところで説明した3種類のメソッドに対応したものがそれぞれ存在します。そのため、送信に使用されたメソッドによって、呼び出されるデリゲートメソッドが異なります。

 そのデリゲートメソッドが次の4種類になります。

メソッド 対応する送信メソッド
session:didReceiveData:fromPeer: sendData:toPeers:withMode:error:
session:didStartReceivingResourceWithName:fromPeer:withProgress: sendResourceAtURL:withName:toPeer:withCompletionHandler:
session:didFinishReceivingResourceWithName:fromPeer:atURL:withError: sendResourceAtURL:withName:toPeer:withCompletionHandler:
session:didReceiveStream:withName:fromPeer: startStreamWithName:toPeer:error:

 今回送信側で使用しているsendData:toPeers:withMode:error: に対応するsession:didReceiveData:fromPeer: について説明を行いたいと思います。

-(void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID{

    //ここにデータを受信したときに行う処理を記述

}

 送信側メソッドから送られてくるのは NSData型のデータになります。その他には送り元の peer の情報が一緒に送られて来ます。よって、チャットアプリを作成するときにはだれが送ったメッセージなのか、簡単に表示させることができます。

 これら受信用のメソッドは、送信されたデータを UI 上へ表示したり、内部的に使用するなどの処理を行うためにも実装しておくと良いでしょう。

MultipeerConnectivityを使用する上で

 ここまで説明をしてきた Multipeer Connectivity ですが、これを使用する上で知っておくと良い事が幾つかありますので、紹介したいと思います。

イベントハンドリングが行われるメソッド

 今まで説明してきたいくつかのデリゲートメソッドですが、実はメインスレッドで呼ばれるものではなく、すべてバックグラウンド側のスレッドで呼ばれることになります。よって、データを受信メソッドで受け取って即 UI に反映させようとした時に不具合が生じる事がありますので気を付けましょう。  

送信の順番

 チャットアプリを作成すると仮定した時に、特に気を付ける必要があるのがメッセージの順番です。他人が送信したデータと自分が送信するデータをほぼ同時に UI 画面へ反映させようとした時に、順番がずれてしまうことがあります。そのため、自分の画面と他者の画面でメッセージ順に食い違いが生じる事になります。

 よって、メッセージを送る順番や、リストなどに入れる順番をキューなどで制御する機構をしっかりと作成する必要があります。

Multipeer Connectivity の可能性

 Multipeer Connectivity は端末同士を接続し、その輪を広げていくことでP2Pのようなネットワークを構築することができると思います。そうなると、理論上 1km 先の端末と、サーバを介すことなく通信ができるようになります。

 今回はそれを実証するための実験を行いました。その方法は次の通りです。

  • ホストとなる端末を起動し、ある場所へおいておきます。
  • クライアント1として使用する端末を、ホストとBluetooth通信をギリギリ行える距離におきます。
  • クライアント2として使用する端末を、ホストと通信できない場所へ持っていきます。
  • ホストとクライアント2が、クライアント1を中継して通信を行います。

これを図で表すと以下のようになります。

実験の概要

 結果 : 残念ながらうまくいきませんでした。どうも現段階の実装方法では、ホストの通信範囲外へ出てしまうと中継する端末が間にいようと関係なく、繋がらなくなってしまうようです。

 結論 : Multipeer Connectivity はホストが中心にいるスター型の接続を行うことで通信を行うことができる機能であり、バケツリレー式に遠い場所にある端末と接続できるということでは無いということが言えると思います。それによりホストの通信範囲内でしかこの機能を使用することができないということになりそうです。

 しかし、別な実証を行うことは出来ました。それは、Wifiにしかつながっていない端末と、Bluetooth でしか通信できない端末の間にホストが入ることで、この両者を接続することが可能になるということです。普段では絶対につながるはずのない2つの端末をつなげることができるというのはなかなか面白い結果ではないでしょうか。

 今回の実験ではこのような結果になってしまいましたが、図のクライアント1がホストとクライアント両方の機能を実装し、かつ発見したクライアント2の情報を、ホストへ送ることができればバケツリレー式の接続をすることもできるのではないでしょうか。

 いつか 1km 先の端末と通信できることを夢見て、これからもこの検証と実験を続けて行きたいと思います。

おわりに

 今回の特集はいかがだったでしょうか。
 この MultipeerConnectivity を使用することで得られる、近距離通信の簡略化のメリットはかなり大きい物になると思います。うまく使用することでかなり面白いサービスを展開することができるのでは無いでしょうか。

 もちろんちょっとした事が便利になるかもしれません。例えば、カメラで撮影した画像を、周りの人と簡単に共有することができたり、会議に使用する資料をペーパーレスで配布することもできるでしょう。
 面白そうな使い方を考えるだけで、夢が広がりそうな感じがしますね!

 それでは次回の特集もお楽しみに!