[iOS 7] P2P 通信を手軽に実現する Multipeer Connectivity Framework を使ってみる
Multipeer Connectivity Framework
Multipeer Connectivity Framework は、Wifi や Bluetooth を利用した近距離にある iOS 端末間の Peer to Peer の通信を手軽に実現するためのフレームワークです。
このフレームワークでは、下記の3種類の通信方式を利用して通信を行います。
- 同一 LAN 内 での Wifi による通信
- ピア同士の Wifi による通信
- ピア同士の Bluetooth による通信
従来は、同一 LAN 内にいる iOS 端末間での通信を行うことができましたが、このフレームワークによる通信は端末が同一 LAN 内にいる必要がないところが大きな特徴です。
Multipeer Connectivity Framework の API
Multipeer Connectivity Framework の API は下記のクラス群から構成されます。
MCPeerID
ピアの識別子を表すクラスです。このクラスのインスタンスを利用してピアを一意に特定します。
自デバイスを表す MCPeerID のインスタンスはアプリ側で生成する必要がありますが、他のピアの PMPeerID のインスタンスはフレームワーク側が生成してくれます。
displayName
displayName はピアの表示名を表すプロパティです。MCPeerID クラスの生成時には必ず表示名を紐づける必要があります。displayName は UTF-8 の文字列で、63 byte 以内という制限が存在します。
MCSession
Peer to Peer 通信のセッションをコントロールするクラスです。ピア同士の接続が確立した後は、このクラスのインスタンスを介して通信を行うことになります。
MCAdvertizerAssistant
Multipeer Connectivity Framework において、ピア同士の接続を確立するには、ピアを他のピアからその存在を発見できるようにする必要があります。これを実現するため、ピアが Peer to Peer 通信によって提供できる機能をサービスとして他のピアに公開する仕組みがあり、これをアドバタイズと言います。MCAdvertizerAssistant はアドバタイズを簡単に行うためのヘルパークラスです。
また、このクラスは後述の MCBrowserViewController もしくは MCNearbyServiceBrowser による接続要求に対する処理も自動的に行ってくれます。
serviceType
ピアがアドバタイズするサービスの種類を表す NSString 型のプロパティです。このプロパティは、ピアの検索の際にアドバタイズされているサービスを識別するために利用します。このため、他のアプリも含めて一意になるように設定する必要があります。
フォーマットは下記の通りです。
- 1〜15文字の文字列
- 利用できる文字は、ASCII の小文字、数字、ハイフンのみ
なお、これは Bonjour で利用されている service type と同一のフォーマットです。
discoveryInfo
アドバタイズ時に他のピアに公開する情報を設定する NSDictionary 型のプロパティです。
MCBrowserViewController
近くにあるアドバタイズ中のピアの検索と、発見したピアに対する接続要求を簡単に行うためのヘルパークラスです。このクラスは UIViewController のサブクラスで、検索により発見したピアのリストを表示する UI が提供されます。リストからピアを選択すると、そのピアに対する接続要求を送信します。さらに、相手のピアが接続を承認した場合には接続の確立までの処理を自動的に行ってくれます。
MCNearbyServiceAdvertiser
このクラスも MCAdvertizerAssistant と同様に、アドバタイズと接続要求受信時の処理のためのヘルパークラスです。MCAdvertizerAssistant は、アドバタイズや接続要求受信時の処理などを自動的に行ってくれて便利である反面、細かい制御は一切できません。そこで、接続要求受信時などに任意の処理を行いたい場合には、こちらのクラスを利用するとある程度制御ができるようになります。
MCNearbyServiceBrowser
このクラスも MCBrowserViewController と同様に、ピアの検索と接続要求の送信処理のためのヘルパークラスです。MCAdvertizerAssistant に対する MCNearbyServiceAdvertiser の関係と同じで、このクラスも MCBrowserViewController で自動的に行われる処理をある程度制御したい場合に利用します。このクラスでは UI は提供されません。
セッションの作成と他のピアとの接続確立までの流れ
セッションの作成から他のピアとの接続を確立するまでの流れの概要については下記の通りです。
- MCPeerID を生成して、それを使って MCSession を生成する。
- 周辺に存在するピアを探す。ピアの検索やアドバタイズの処理は、Multipeer Connectivity Framework が用意している MCBrowserViewController などの各種ヘルパークラスを利用するか、もしくは NSNetService などを利用して自前で実装する。
- 発見した他のピアを MCSession に追加する。2 のプロセスで Multipeer Connectivity Framework が用意している各種ヘルパークラスを利用している場合は、この処理はヘルパークラス内で行われるので実装する必要がない。
- MCSession に追加したピアのセッションの状態が変化すると、MCSessionDelegate プロトコルの session:peer:didChangeState: デリゲートメソッドが呼び出される。ステートが MCSessionStateConnected になったピアは通信可能になるので、それまで待機する。
データ転送の方法
Multipeer Connectivity Framework では、データの送受信を行う際にデータを扱う方法として、下記の3つが用意されています。
NSData を使ってデータを送受信する
NSData 型で表されたバイナリデータを利用してデータの送受信を行うことができます。
データの送信
送信する際は、MCSession クラスの sendData:toPeers:withMode:error: を利用します。
このメソッドは一切デリゲートやハンドラブロックは提供されていませんが、データの送信処理自体はノンブロッキングで行われます。このメソッドを呼び出した際には、フレームワーク内部のデータ転送のキューに登録するのみで、その後非同期にデータ送信処理が実行される仕組みになっています。
また、NSError の参照を渡してエラーを取得できるものの、これはあくまでデータ転送のキューへの登録に失敗した場合に NSError のインスタンスの参照がセットされるというものです。
データの受信
NSData のデータを受信した際には、 MCSessionDelegate プロトコルの session:didReceiveData:fromPeer: が呼び出されますので、このデリゲートメソッド内で受信処理をします。
NSURL を使ってデータを送受信する
ローカルファイルの URL もしくは web の URL を指定してデータの送受信を行うことができます。
データの送信
送信する際は、MCSession クラスの sendResourceAtURL:withName:toPeer:withCompletionHandler: を利用します。このメソッドにはデータ送信の完了をハンドリングするためのブロックが用意されています。
データの受信
NSURL によるデータを受信した場合、受信開始時に MCSessionDelegate プロトコルの session:didStartReceivingResourceWithName:fromPeer:withProgress: が呼び出されます。また、 受信完了もしくはエラー発生時には session:didFinishReceivingResourceWithName:fromPeer:atURL:withError: が呼び出されます。
NSInputStream, NSOutputStream でデータを送受信する
ストリームを利用してデータの送受信を行うことができます。
データの送信
送信する際は、MCSession クラスの startStreamWithName:toPeer:error: を利用してデータを送る NSOutputStream を作成します。
データの受信
ストリームによるデータを受信した場合、受信側では MCSessionDelegate プロトコルの session:didReceiveStream:withName:fromPeer: が呼び出されます。このデリゲートメソッドで渡される NSInputSteam を利用してデータを取得します。
サンプルアプリの作成
では、他のピアとの接続を確立した後、カメラロールの画像を送信するサンプルアプリを作成してみたいと思います。サンプルアプリのソースコードは GitHub に公開してありますので参考にして下さい。
Xcode プロジェクトの作成
Xcode から新規プロジェクトを作成して下さい。Multipeer Connectivity Framework による Peer to Peer 通信機能は iOS 6 以前では利用できません。必ず iOS SDK 7 を利用してビルドして下さい。新規プロジェクトのテンプレートは Single View Application を選択します。
プロジェクトを作成したら、MultipeerConnectivity.framework をプロジェクトに追加する必要があります。ただし、Apple LLVM 5.0 コンパイラで追加された Modules の機能が提供する @import ディレクティブをコードでのインポート時に利用すれば、ビルド時にコンパイラが自動的にリンクしてくれます。
@import MultipeerConnectivity;
UI を作成する
このサンプルアプリの UI と操作の流れは下図の通りです。
ピアの表示名を入力する画面と、接続が確立されたピアの一覧を表示する画面の2つの画面から構成されています。
ピアの表示名の設定
他のピアとの接続プロセスを開始する前に、自身の表示名を設定する必要があります。このアプリでは、先程の UI の図の通り単純に UITextField に入力された文字列を表示名として扱うことにします。
セッションの作成とアドバタイズの開始
さて、ではまずセッションの作成部分から見てきましょう。Create Session ボタンが押下されたら、UITextField に入力された表示文字列を取得してセッションを作成します。また、同時にアドバタイズを開始します。
コードは下記の通りです。
SessionHelper.m
// MCPeerID の生成 MCPeerID *peerID = [[MCPeerID alloc] initWithDisplayName:displayName]; // MCSession の生成 _session = [[MCSession alloc] initWithPeer:peerID]; _session.delegate = self; // MCAdvertiserAssistant の生成とアドバタイズの開始 self.advertiserAssistant = [[MCAdvertiserAssistant alloc] initWithServiceType:self.serviceType discoveryInfo:nil session:self.session]; [self.advertiserAssistant start];
MCPeerID の生成
2行目で自身を表す MCPeerID のインスタンスを生成しています。この際、イニシャライザのパラメータに UITextField に入力された表示名を渡しています。
MCSession の生成
5行目でセッションを扱う MCSession のインスタンスを生成しています。MCSession のイニシャライザには、MCPeerID のインスタンスをパラメータとして渡します。
また、デリゲートも忘れずに設定しておきます。
MCAdvertiserAssistant によるアドバタイズの開始
9行目では MCAdvertiserAssistant のインスタンスを生成しています。MCAdvertiserAssistant のイニシャライザには serviceType, discoveryInfo, MCSession のインスタンスの3つをパラメータとして渡しています。
このサンプルアプリでは serviceType の値を定数として保持しています。この値を他のピアの検索時にも利用することで、同一アプリによってアドバタイズ中のピアを発見できるようにしています。
static NSString * const ServiceType = @"cm-p2ptest";
また、今回は discoveryInfo については特に情報を設定していません。
最後の行で start メソッドを呼び出すことでアドバタイズを開始しています。また、これ以降はアドバタイズと同時に、他のピアからの接続要求に対する応答処理も実行されるようになります。
他のピアの検索と接続の確立
MCBrowserViewController の表示
セッションの作成とアドバタイズの開始処理が終わったら、次は他のピアの検索と接続の確立処理を行います。と言っても MCBrowserViewController を利用すれば、ほぼコードを書かずとも処理を行うことができますし、UI を用意する必要もありません。このサンプルアプリでは、一番手軽な MCBrowserViewController を利用して実装したいと思います。
MCBrowserViewController で提供される UI の表示処理は下記のコードの通りです。
PeerListViewController.m
MCBrowserViewController *viewController = [[MCBrowserViewController alloc] initWithServiceType:self.sessionHelper.serviceType session:self.sessionHelper.session]; viewController.delegate = self; [self presentViewController:viewController animated:YES completion:nil];
MCBrowserViewController の生成時に、イニシャライザに serviceType と MCSession のインスタンスを渡しています。あとは、この ViewController のインスタンスに対して表示処理を実行すれば、下図のような画面が表示されてピアの検索と接続の確立処理を行うことができます。
MCBrowserViewController のイベントハンドリング
MCBrowserViewController は MCBrowserViewControllerDelegate プロトコルによるデリゲートを提供しており、イベントをハンドリングすることができます。
ハンドリングできるイベントは下記の通りです。
- 発見したピアを MCBrowserViewController が提供する UI 上に表示するか判断
- Done ボタン押下時
- Cancel ボタン押下時
下記はサンプルアプリでの実装です。
PeerListViewController.m
// 発見したピアを MCBrowserViewController が提供する UI 上に表示するか判断するデリゲートメソッド - (BOOL)browserViewController:(MCBrowserViewController *)browserViewController shouldPresentNearbyPeer:(MCPeerID *)peerID withDiscoveryInfo:(NSDictionary *)info { return YES; } // Done ボタン押下時のデリゲートメソッド - (void)browserViewControllerDidFinish:(MCBrowserViewController *)browserViewController { [browserViewController dismissViewControllerAnimated:YES completion:nil]; } // Cancel ボタン押下時のデリゲートメソッド - (void)browserViewControllerWasCancelled:(MCBrowserViewController *)browserViewController { [browserViewController dismissViewControllerAnimated:YES completion:nil]; }
このうち、1 のイベントに対応するデリゲートメソッドである、browserViewController:shouldPresentNearbyPeer:withDiscoveryInfo: のみは実装が必須ではありません。このメソッドが実装されていない場合、全てのピアが表示されます。今回の実装では常に YES を返しているだけなので、実装していない場合と同じ動作になります。
他のピアの接続状態の監視
MCBrowserViewController で他のピアへの接続要求を送信すると、MCSession に他のピアが登録されます。この状態になると、MCSession が MCSessionDelegate の session:peer:didChangeState: メソッドを通じて、他のピアの接続状態の変化を通知してくるようになります。
session:peer:didChangeState: の実装は下記の通りです。
SessionHelper.m
- (void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state { BOOL needToNotify = NO; // 他のピアの接続状態を管理 if (state == MCSessionStateConnected) { if (![self.connectedPeerIDs containsObject:peerID]) { [self.connectedPeerIDs addObject:peerID]; needToNotify = YES; } } else { if ([self.connectedPeerIDs containsObject:peerID]) { [self.connectedPeerIDs removeObject:peerID]; needToNotify = YES; } } if (needToNotify) { // メインスレッドで処理を実行 dispatch_async(dispatch_get_main_queue(), ^{ // 他のピアの接続状態が変化したことをViewControllerに通知 [self.delegate sessionHelperDidChangeConnectedPeers:self]; }); } }
上記コードでは、このデリゲートメソッドをハンドリングして他のピアの接続状態を管理しています。
また、MCSessionState はピアの接続状態を表す列挙型で、下記の3つの値が定義されています。
MCSessionStateNotConnected
ピアは接続されていません。
MCSessionStateConnecting
ピアとの接続を実行中です。
MCSessionStateConnected
ピアとの接続が確立されています。
なお、このデリゲートメソッドは MultipeerConnectivityFramework が生成したスレッド上で呼び出されます。このため、UI の更新を行う場合はメインスレッドに処理を移す必要がありますので注意して下さい。
接続を確立したピアへのデータ送信
さて、他のピアとの接続を確立することができたので、データを送信してみたいと思います。このサンプルアプリでは、一番実装が単純な NSData を利用したデータの送受信を利用します。
下記コードは、静止画データ送信処理メソッドです。サンプルアプリでは、UIImagePickerController でカメラロール内にある静止画の UIImage を取得してきた後に呼び出しています。
SessionHelper.m
- (void)sendImage:(UIImage *)image peerID:(MCPeerID *)peerID { NSData *data = UIImageJPEGRepresentation(image, 0.9f); NSError *error; [self.session sendData:data toPeers:@[peerID] withMode:MCSessionSendDataReliable error:&error]; if (error) { NSLog(@"Failed %@", error); } }
UIImageJPEGRepresentation 関数で UIImage から NSData を生成した後、MCSession の sendData:toPeers:withMode:error: メソッドで他のピアにデータを送信しています。
このメソッドの3番目のパラメータは、MCSessionSendDataMode 型の列挙型です。この列挙型では、下記の2つの値が定義されています。
MCSessionSendDataReliable
データ送信のキューに登録した上で、確実に対象のピアにデータが届くようにします。
MCSessionSendDataUnreliable
データ送信のキューには登録せずに即時にデータを送信します。ただし、確実に対象のピアにデータが届くとは限りません。
ここでは、確実にデータを送信したいので、MCSessionSendDataReliable をパラメータとして渡しています。
他のピアからのデータ受信
他のピアからデータを受信した際には、MCSessionDelegate プロトコルのデリゲートメソッドを介してイベントが通知されます。下記コードは、サンプルアプリにおける、データ受信時のデリゲートメソッドの実装です。
SessionHelper.m
- (void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID { UIImage *image = [UIImage imageWithData:data]; dispatch_async(dispatch_get_main_queue(), ^{ // 静止画データの受信を ViewController に通知 [self.delegate sessionHelperDidRecieveImage:image peer:peerID]; }); } - (void)session:(MCSession *)session didStartReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID withProgress:(NSProgress *)progress { // Do nothing } - (void)session:(MCSession *)session didFinishReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID atURL:(NSURL *)localURL withError:(NSError *)error { // Do nothing } - (void)session:(MCSession *)session didReceiveStream:(NSInputStream *)stream withName:(NSString *)streamName fromPeer:(MCPeerID *)peerID { // Do nothing }
NSData 型で送信されたデータを受信した場合、session:didReceiveData:fromPeer: デリゲートメソッドが呼び出されます。ここでは、受信した NSData 型の静止画のデータを UIImage 型に変換した後、デリゲートメソッドを通じて ViewController に渡しています。このデリゲートメソッドも、Multipeer Connectivity Framework が生成したスレッド上で呼び出されるため、ViewController に渡す前に処理をメインスレッドに移しています。なお、データを受け取った ViewController では、カメラロールに保存する処理を実装しています。
また、このサンプルアプリでは NSData によるデータの送受信しか扱わないので、session:didReceiveData:fromPeer: デリゲートメソッド以外は使用しません。しかし、上記4つのデリゲートメソッドは実装が必須であるため、必要のないデリゲートメソッドに関しても実装した上で何も処理しないようにしています。
これでサンプルアプリの実装は完了です。
動作確認
では、実際にサンプルアプリの動作を確認します。デバイスは、iPhone 5 と iPad 4th を使用しました。
アプリを起動すると下図のような画面が表示されます。
テキストフィールドにそれぞれ表示名を入力します。iPhone 5 には "iPhone"、iPad 4th には "iPad" と入力しました。表示名を入力後、Create Session ボタンを押下すると、接続済みピア一覧の画面が表示されます。
まだ接続が確立されたピアがないのでリストは空です。iPhone 側で右上の Browse ボタンを押下すると、ピアの検索が開始され、MCBrowserViewController で提供される UI が表示されます。
画面上部の "NEARBY" セクションに、発見したピアの表示名が表示されます。ここに表示されるのは、MCBrowserViewController で検索対象として指定した serviceType のサービスをアドバタイズしているピアのみです。
表示されたピアを選択すると、そのピアに対して接続要求が送信され、下図のような表示に変わります。
一方、接続要求を受信した iPad 側は、接続要求を受け入れるかユーザーに判断を求めるアラートが表示されます。
なお、接続要求の受信の処理からアラート選択後の接続処理まで、全て MCAdvertiserAssistant が処理を行ってくれています。
iPad 側で表示されたアラートに対して、Accept を選択するとピア同士が接続されます。接続が確立されると、iPhone 側の表示は下図のように変わります。
右上の Done ボタンを押下して接続済みピア一覧画面に戻ると、接続された iPad が表示されています。
iPad 側も同じように、一覧に iPhone が表示されます。
では、iPad から iPhone にカメラロールの静止画を送信してみます。一覧に表示されている iPhone を選択すると、カメラロールが表示されます。ここで適当な静止画を選択すると、iPhone への静止画データの送信が開始されます。
5秒程度待っていると、iPhone 側にデータを受信した旨のアラートが表示されます。
カメラロールを確認すると、iPad から送信した静止画が保存されているのが確認できます。
まとめ
Multipeer Connectivity Framework を利用すると、至って簡単に iOS 端末間での Peer to Peer の通信が行えます。このフレームワークで提供されている API はレイヤ的にかなり高レベルで、Wifi や Bluetooth の通信モジュールの選択から、アドバタイズ、ピアの検索と接続確立まで、煩雑な手続きを意識することなく実装することができます。手軽に Peer to Peer の通信ができるようになったので、色々なアプリに活用することができそうです。