共謀:iOSのMultipeerConnectivityを使用した近隣のデバイスネットワーキング

公開: 2022-03-11

従来、ピアツーピア通信用のデバイスの接続は、少し手間がかかりました。 アプリケーションは、周囲の状況を検出し、両側で接続を開き、ネットワークインフラストラクチャ、接続、距離などがすべて変化しても、それらを維持する必要があります。 これらのアクティビティに固有の問題を認識し、iOS7およびmacOS10.10では、AppleはMultipeerConnectivityフレームワーク(以降、MPC)を導入しました。これは、アプリが比較的少ない労力でこれらのタスクを実行できるように設計されています。

MPCは、ここで必要なインフラストラクチャの多くを処理します。

  • 複数のネットワークインターフェイスのサポート(Bluetooth、WiFi、およびイーサネット)
  • デバイスの検出
  • 暗号化によるセキュリティ
  • 小さなメッセージパッシング
  • ファイル転送

この記事では、主にiOSの実装について説明しますが、すべてではないにしても、ほとんどの場合、macOSとtvOSに適用できます。

MultipeerConnectivityセッションのライフサイクル

マルチピアセッションのライフサイクル:

  1. MCNearbyServiceAdvertiser.startAdvertisingForPeers()
  2. MCNearbyServiceBrowser.startBrowsingForPeers()
  3. MCNearbyServiceBrowserDelegate.browser(:foundPeer:withDiscoveryInfo:)
  4. MCNearbyServiceBrowser.invitePeer(...)
  5. MCNearbyServiceAdvertiserDelegate.didReceiveInvitationFromPeer(...)
  6. didReceiveInvitationinvitationHandlerを呼び出します
  7. Create the MCSession
  8. MCSession.send(...)
  9. MCSessionDelegate.session(_:didReceive:data:peerID)
  10. MCSession.disconnect()

時々この画像を参照してください

iOS開発者にMPCベースのアプリケーションの実装を説明することを目的としたMultipeerConnectivityチュートリアルと例が数多くあります。 しかし、私の経験では、それらは通常不完全であり、MPCでいくつかの重要な潜在的な障害を覆い隠す傾向があります。 この記事では、このようなアプリの基本的な実装について読者に説明し、行き詰まりやすいと感じた領域を紹介したいと思います。

概念とクラス

MPCは少数のクラスに基づいています。 一般的なもののリストを見ていき、フレームワークについての理解を深めましょう。

  • MCSession –セッションは、関連付けられたピア間のすべての通信を管理します。 セッションを介してメッセージ、ファイル、およびストリームを送信できます。接続されたピアからこれらのいずれかが受信されると、そのデリゲートに通知されます。
  • MCPeerID –ピアIDを使用すると、セッション内の個々のピアデバイスを識別できます。 関連付けられた名前がありますが、注意してください。同じ名前のピアIDは同一とは見なされません(以下の基本ルールを参照)。
  • MCNearbyServiceAdvertiser –広告主は、サービス名を近くのデバイスにブロードキャストすることを許可します。 これにより、彼らはあなたとつながることができます。
  • MCNearbyServiceBrowser –ブラウザーでは、 MCNearbyServiceAdvertiserを使用してデバイスを検索できます。 これらの2つのクラスを一緒に使用すると、近くのデバイスを検出し、ピアツーピア接続を作成できます。
  • MCBrowserViewController –これは近くのデバイスサービスを閲覧するための非常に基本的なUIを提供します( MCNearbyServiceAdvertiserを介して販売されます)。 一部のユースケースには適していますが、これは使用しません。私の経験では、MCPの最も優れた側面の1つはそのシームレス性です。

基本ルール

MPCネットワークを構築する際に留意すべき点がいくつかあります。

  • デバイスはMCPeerIDオブジェクトによって識別されます。 これらは、表面的にはラップされた文字列であり、実際には、単純な名前で初期化できます。 2つのMCPeerIDは同じ文字列で作成できますが、同一ではありません。 したがって、MCPeerIDをコピーまたは再作成しないでください。 それらはアプリケーション内で渡される必要があります。 必要に応じて、NSArchiverを使用して保存できます。
  • ドキュメントが不足していますが、MCSessionを使用して3つ以上のデバイス間で通信できます。 ただし、私の経験では、これらのオブジェクトを利用する最も安定した方法は、デバイスが対話しているピアごとに1つ作成することです。
  • アプリケーションがバックグラウンドにある間、MPCは機能しません。 アプリがバックグラウンドになったら、すべてのMCSessionを切断して破棄する必要があります。 バックグラウンドタスクで最小限の操作以上のことをしようとしないでください。

MultipeerConnectivity入門

ネットワークを確立する前に、少しハウスキーピングを行ってから、通信可能な他のデバイスを検出するために広告主とブラウザのクラスを設定する必要があります。 いくつかの状態変数(ローカルMCPeerIDと接続されているデバイス)を保持するために使用するシングルトンを作成してから、 MCNearbyServiceAdvertiserMCNearbyServiceBrowserを作成します。 これらの最後の2つのオブジェクトには、アプリケーションを識別する文字列であるサービスタイプが必要です。 16文字未満である必要があり、可能な限り一意である必要があります(つまり、「Multipeer」ではなく「MyApp-MyCo」)。 近くのデバイス(おそらくゲームの種類やデバイスの役割)を見るときに、ブラウザーが読み取ることができるよりも(小さな)辞書を広告主に指定して、もう少し多くの情報を提供することができます。

MPCはシステムが提供するAPIに依存し、実際のオブジェクト(他のデバイス、およびそれらの間の共有「ネットワーク」)と相関するため、シングルトンパターンに適しています。 頻繁に使用されますが、シングルトンはこのような共有リソースに適しています。

シングルトンの定義は次のとおりです。

 class MPCManager: NSObject { var advertiser: MCNearbyServiceAdvertiser! var browser: MCNearbyServiceBrowser! static let instance = MPCManager() let localPeerID: MCPeerID let serviceType = "MPC-Testing" var devices: [Device] = [] override init() { if let data = UserDefaults.standard.data(forKey: "peerID"), let id = NSKeyedUnarchiver.unarchiveObject(with: data) as? MCPeerID { self.localPeerID = id } else { let peerID = MCPeerID(displayName: UIDevice.current.name) let data = try? NSKeyedArchiver.archivedData(withRootObject: peerID) UserDefaults.standard.set(data, forKey: "peerID") self.localPeerID = peerID } super.init() self.advertiser = MCNearbyServiceAdvertiser(peer: localPeerID, discoveryInfo: nil, serviceType: self.serviceType) self.advertiser.delegate = self self.browser = MCNearbyServiceBrowser(peer: localPeerID, serviceType: self.serviceType) self.browser.delegate = self } }

MCPeerID NSKeyedArchiverて)ユーザーのデフォルトに保存し、それを再利用していることに注意してください。 上記のように、これは重要であり、何らかの方法でキャッシュに失敗すると、さらに先のあいまいなバグが発生する可能性があります。

これがDeviceクラスです。これを使用して、検出されたデバイスとその状態を追跡します。

 class Device: NSObject { let peerID: MCPeerID var session: MCSession? var name: String var state = MCSessionState.notConnected init(peerID: MCPeerID) { self.name = peerID.displayName self.peerID = peerID super.init() } func invite() { browser.invitePeer(self.peerID, to: self.session!, withContext: nil, timeout: 10) } }

最初のクラスが構築されたので、今度は一歩下がって、ブラウザと広告主の間の相互作用について考えます。 MPCでは、デバイスは提供するサービスをアドバタイズでき、他のデバイスで関心のあるサービスを参照できます。 私たちはアプリだけを使用したデバイス間の通信に重点を置いているため、同じサービスの宣伝と閲覧の両方を行います。

従来のクライアント/サーバー構成では、1つのデバイス(サーバー)がそのサービスをアドバタイズし、クライアントがそれらを参照していました。 私たちは平等主義者なので、デバイスの役割を指定する必要はありません。 すべてのデバイスにアドバタイズとブラウジングの両方があります。

MPCManagerにメソッドを追加して、デバイスが検出されたときにデバイスを作成し、デバイス配列で追跡する必要があります。 このメソッドはMCPeerIDを取得し、そのIDを持つ既存のデバイスを探し、見つかった場合はそれを返します。 既存のデバイスがまだない場合は、新しいデバイスを作成し、それをデバイスアレイに追加します。

 func device(for id: MCPeerID) -> Device { for device in self.devices { if device.peerID == id { return device } } let device = Device(peerID: id) self.devices.append(device) return device }

デバイスがアドバタイズを開始した後、別のブラウジングデバイスがデバイスへの接続を試みることができます。 この場合、広告主からの着信デリゲート呼び出しを処理するために、デリゲートメソッドをMPCSessionクラスに追加する必要があります。

 extension MPCManager: MCNearbyServiceAdvertiserDelegate { func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) { let device = MPCManager.instance.device(for: peerID) device.connect() invitationHandler(true, device.session) } }

…MCSessionを作成するためのデバイス上のメソッド:

 func connect() { if self.session != nil { return } self.session = MCSession(peer: MPCManager.instance.localPeerID, securityIdentity: nil, encryptionPreference: .required) self.session?.delegate = self }

…そして最後に、ブラウザが広告主を検出したときに招待をトリガーする方法:

 extension MPCManager: MCNearbyServiceBrowserDelegate { func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) { let device = MPCManager.instance.device(for: peerID) device.invite(with: self.browser) }

現在、 withDiscoveryInfo引数を無視しています。 これを使用して、利用可能にしたものに基づいて特定のデバイスを除外できます(これは、上記のMCNearbyServiceAdvertiserdiscoveryInfo引数で提供した辞書と同じです)。

デバイスの接続

すべてのハウスキーピングが完了したので、デバイスを接続する実際のビジネスを開始できます。

MPCSessionのinitメソッドでは、広告主と代理人の両方を設定しました。 接続を開始する準備ができたら、両方を起動する必要があります。 これは、アプリデリゲートのdidFinishLaunchingメソッドで、または適切なときにいつでも実行できます。 クラスに追加するstart()メソッドは次のとおりです。

 func start() { self.advertiser.startAdvertisingPeer() self.browser.startBrowsingForPeers() }

これらの呼び出しは、アプリがWiFi経由でそのプレゼンスのブロードキャストを開始することを意味します。 これを機能させるためにWiFiネットワークに接続する必要はないことに注意してください(ただし、WiFiネットワークをオンにする必要があります)。

デバイスが招待に応答してMCSessionを開始すると、セッションからデリゲートコールバックの受信を開始します。 それらのハンドラーをデバイスオブジェクトに追加します。 それらのほとんどは、当面は無視します。

 extension Device: MCSessionDelegate { public func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) { self.state = state NotificationCenter.default.post(name: Multipeer.Notifications.deviceDidChangeState, object: self) } public func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { } public func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) { } public func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) { } public func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) { } }

当面は、主にsession(_:peer:didChangeState:)コールバックに関心があります。 これは、デバイスが新しい状態( notConnectedconnecting 、およびconnected )に遷移するたびに呼び出されます。 接続されているすべてのデバイスのリストを作成できるように、これを追跡する必要があります。

 extension MPCManager { var connectedDevices: [Device] { return self.devices.filter { $0.state == .connected } } }

メッセージの送信

すべてのデバイスが接続されたので、実際にメッセージの送受信を開始します。 MPCは、この点に関して3つのオプションを提供します。

  • バイトのブロック(データオブジェクト)を送信できます
  • ファイルを送ることができます
  • 他のデバイスへのストリームを開くことができます

簡単にするために、これらのオプションの最初のものだけを見ていきます。 単純なメッセージを前後に送信し、メッセージの種類やフォーマットなどの複雑さについてあまり心配する必要はありません。Codable構造を使用して、メッセージをカプセル化します。これは次のようになります。

 struct Message: Codable { let body: String }

また、デバイスに拡張機能を追加して、次のいずれかを送信します。

 extension Device { func send(text: String) throws { let message = Message(body: text) let payload = try JSONEncoder().encode(message) try self.session?.send(payload, toPeers: [self.peerID], with: .reliable) } } ~~~swift Finally, we'll need to modify our `Device.session(_:didReceive:fromPeer)` code to receive the message, parse it, and notify any interested objects about it:

static let messageReceivedNotification = Notification.Name(“ DeviceDidReceiveMessage”)public func session(_ session:MCSession、didReceive data:Data、fromPeer peerID:MCPeerID){if let message = try? JSONDecoder()。decode(Message.self、from:data){NotificationCenter.default.post(name:Device.messageReceivedNotification、object:message、userInfo:[“ from”:self])}}

 ## Disconnections Now that we've got a connection created between multiple devices, we have to be able to both disconnect on demand and also handle system interruptions. One of the undocumented weaknesses of MPC is that it doesn't function in the background. We need to observe the `UIApplication.didEnterBackgroundNotification` notification, and make sure that we shut down all our sessions. Failure to do this will lead to undefined states in the sessions and devices and can cause lots of confusing, hard-to-track-down errors. There is a temptation to use a background task to keep your sessions around, in case the user jumps back into your app. However, this is a bad idea, as MPC will usually fail within the first second of being backgrounded. When your app returns to the foreground, you can rely on MPC's delegate methods to rebuild your connections. In our MPCSession's `start()` method, we'll want to observe this notification and add code to handle it and shut down all our sessions. ~~~swift func start() { self.advertiser.startAdvertisingPeer() self.browser.startBrowsingForPeers() NotificationCenter.default.addObserver(self, selector: #selector(enteredBackground), name: Notification.Name.UIApplicationDidEnterBackground, object: nil) } @objc func enteredBackground() { for device in self.devices { device.disconnect() } } func disconnect() { self.session?.disconnect() self.session = nil }

結論

この記事では、MultipeerConnectivityベースのアプリケーションのネットワークコンポーネントを構築するために必要なアーキテクチャについて説明します。 完全なソースコード(Githubで入手可能)は、接続されたデバイスを表示し、それらの間でメッセージを送信できるようにする最小限のユーザーインターフェイスラッパーを提供します。

MPCは、WiFiネットワーク、Bluetooth、または複雑なクライアント/サーバー体操について心配することなく、近くのデバイス間でほぼシームレスな接続を提供します。 短いゲームセッションのためにいくつかの電話をすばやくペアリングしたり、共有のために2つのデバイスを接続したりできることは、典型的なAppleのやり方で行われます。

このプロジェクトのソースコードは、Githubのhttps://github.com/bengottlieb/MultipeerExampleで入手できます。

AFNetworkingを使用するiOSを設計していますか? Model-View-Controller(MVC)デザインパターンは、maintainabeコードベースに最適ですが、DRYコード、集中型ネットワークロギング、特にレート制限などの懸念から、ネットワークを処理するために単一のクラスが必要になる場合があります。 iOSの集中型および分離型ネットワークでのシングルトンクラスでのこれの処理に関するすべてをお読みください:シングルトンクラスを使用したAFNetworkingチュートリアル