Absprachen: Gerätenetzwerke in der Nähe mit Multipeer-Konnektivität in iOS

Veröffentlicht: 2022-03-11

Traditionell war das Anschließen von Geräten für die Peer-to-Peer-Kommunikation ein ziemlicher Kraftakt. Eine Anwendung muss erkennen, was um sie herum ist, Verbindungen auf beiden Seiten öffnen und sie dann aufrechterhalten, wenn sich die Netzwerkinfrastruktur, Verbindungen, Entfernungen usw. ändern. Angesichts der Schwierigkeiten, die diesen Aktivitäten innewohnen, hat Apple in iOS 7 und macOS 10.10 sein MultipeerConnectivity-Framework (im Folgenden MPC) eingeführt, mit dem Apps diese Aufgaben mit relativ geringem Aufwand ausführen können.

MPC kümmert sich hier um einen Großteil der zugrunde liegenden erforderlichen Infrastruktur:

  • Unterstützung mehrerer Netzwerkschnittstellen (Bluetooth, WiFi und Ethernet)
  • Geräteerkennung
  • Sicherheit durch Verschlüsselung
  • Kleine Nachricht vorbei
  • Datei Übertragung

In diesem Artikel werden wir uns hauptsächlich mit der iOS-Implementierung befassen, aber das meiste, wenn nicht alles, gilt für macOS und tvOS.

Lebenszyklus der Multipeer-Konnektivitätssitzung

Lebenszyklus der Multipeer-Sitzung:

  1. MCNearbyServiceAdvertiser.startAdvertisingForPeers()
  2. MCNearbyServiceBrowser.startBrowsingForPeers()
  3. MCNearbyServiceBrowserDelegate.browser(:foundPeer:withDiscoveryInfo:)
  4. MCNearbyServiceBrowser.invitePeer(...)
  5. MCNearbyServiceAdvertiserDelegate.didReceiveInvitationFromPeer(...)
  6. Rufen Sie den invitationHandler in didReceiveInvitation auf
  7. Create the MCSession
  8. MCSession.send(...)
  9. MCSessionDelegate.session(_:didReceive:data:peerID)
  10. MCSession.disconnect()

Greifen Sie von Zeit zu Zeit auf dieses Bild zurück

Es gibt zahlreiche MultipeerConnectivity-Tutorials und -Beispiele, die vorgeben, iOS-Entwickler durch die Implementierung einer MPC-basierten Anwendung zu führen. Meiner Erfahrung nach sind sie jedoch normalerweise unvollständig und neigen dazu, einige wichtige potenzielle Stolpersteine ​​bei MPC zu beschönigen. In diesem Artikel hoffe ich, den Leser sowohl durch eine rudimentäre Implementierung einer solchen App zu führen als auch Bereiche aufzuzeigen, in denen ich leicht stecken geblieben bin.

Konzepte & Klassen

MPC basiert auf einer Handvoll Klassen. Lassen Sie uns die Liste der häufigsten durchgehen und unser Verständnis des Frameworks vertiefen.

  • MCSession – Eine Sitzung verwaltet die gesamte Kommunikation zwischen den zugehörigen Peers. Sie können Nachrichten, Dateien und Streams über eine Sitzung senden, und ihr Delegierter wird benachrichtigt, wenn eine davon von einem verbundenen Peer empfangen wird.
  • MCPeerID – Mit einer Peer-ID können Sie einzelne Peer-Geräte innerhalb einer Sitzung identifizieren. Ihm ist ein Name zugeordnet, aber seien Sie vorsichtig: Peer-IDs mit demselben Namen werden nicht als identisch angesehen (siehe Grundregeln unten).
  • MCNearbyServiceAdvertiser – Ein Werbetreibender ermöglicht es Ihnen, Ihren Dienstnamen an Geräte in der Nähe zu senden. Dadurch können sie sich mit Ihnen verbinden.
  • MCNearbyServiceBrowser – Mit einem Browser können Sie mithilfe von MCNearbyServiceAdvertiser nach Geräten suchen. Die gemeinsame Verwendung dieser beiden Klassen ermöglicht es Ihnen, Geräte in der Nähe zu erkennen und Ihre Peer-to-Peer-Verbindungen herzustellen.
  • MCBrowserViewController – Dies bietet eine sehr einfache Benutzeroberfläche zum Durchsuchen von Diensten für Geräte in der Nähe (vertrieben über MCNearbyServiceAdvertiser ). Obwohl es für einige Anwendungsfälle geeignet ist, werden wir dies nicht verwenden, da meiner Erfahrung nach einer der besten Aspekte von MCP seine Nahtlosigkeit ist.

Grundregeln

Beim Aufbau eines MPC-Netzwerks sind einige Dinge zu beachten:

  • Geräte werden durch MCPeerID-Objekte identifiziert. Dies sind oberflächlich betrachtet umschlossene Zeichenfolgen und können tatsächlich mit einfachen Namen initialisiert werden. Obwohl zwei MCPeerIDs mit derselben Zeichenfolge erstellt werden können, sind sie nicht identisch. Daher sollten MCPeerIDs niemals kopiert oder neu erstellt werden; sie sollten innerhalb der Anwendung herumgereicht werden. Bei Bedarf können sie mit einem NSArchiver gespeichert werden.
  • Während die Dokumentation dazu fehlt, kann MCSession verwendet werden, um zwischen mehr als zwei Geräten zu kommunizieren. Meiner Erfahrung nach besteht die stabilste Methode zur Verwendung dieser Objekte jedoch darin, für jeden Peer, mit dem Ihr Gerät interagiert, eines zu erstellen.
  • MPC funktioniert nicht, während Ihre Anwendung im Hintergrund läuft. Sie sollten alle Ihre MCSessions trennen und herunterfahren, wenn sich Ihre App im Hintergrund befindet. Versuchen Sie nicht, mehr als minimale Operationen in Hintergrundaufgaben auszuführen.

Erste Schritte mit MultipeerConnectivity

Bevor wir unser Netzwerk einrichten können, müssen wir ein wenig Ordnung schaffen und dann die Advertiser- und Browser-Klassen einrichten, um andere Geräte zu erkennen, mit denen wir kommunizieren können. Wir werden ein Singleton erstellen, das wir verwenden, um einige Zustandsvariablen (unsere lokale MCPeerID und alle angeschlossenen Geräte) zu speichern, dann erstellen wir MCNearbyServiceAdvertiser und MCNearbyServiceBrowser . Diese letzten beiden Objekte benötigen einen Diensttyp, der nur eine Zeichenfolge ist, die Ihre Anwendung identifiziert. Es muss weniger als 16 Zeichen lang sein und sollte so eindeutig wie möglich sein (dh „MyApp-MyCo“, nicht „Multipeer“). Wir können unserem Werbetreibenden ein (kleines) Wörterbuch angeben, das Browser lesen können, um etwas mehr Informationen zu liefern, wenn Geräte in der Nähe betrachtet werden (z. B. ein Spieltyp oder eine Geräterolle).

Da sich MPC auf vom System bereitgestellte APIs stützt und mit realen Objekten korreliert (andere Geräte sowie das gemeinsame „Netzwerk“ zwischen ihnen), passt es gut zum Singleton-Muster. Singletons werden zwar häufig überbeansprucht, eignen sich aber gut für gemeinsam genutzte Ressourcen wie diese.

Hier ist die Definition unseres Singletons:

 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 } }

Beachten Sie, dass wir unsere MCPeerID in den Benutzereinstellungen (über NSKeyedArchiver ) speichern und wiederverwenden. Wie oben erwähnt, ist dies wichtig, und wenn es nicht in irgendeiner Weise zwischengespeichert wird, kann dies später zu undurchsichtigen Fehlern führen.

Hier ist unsere Device-Klasse, mit der wir nachverfolgen, welche Geräte erkannt wurden und welchen Status sie haben:

 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) } }

Nachdem wir nun unsere ersten Klassen aufgebaut haben, ist es an der Zeit, einen Schritt zurückzutreten und über das Zusammenspiel zwischen Browsern und Werbetreibenden nachzudenken. In MPC kann ein Gerät für einen von ihm angebotenen Dienst werben und auf anderen Geräten nach einem Dienst suchen, an dem es interessiert ist. Da wir uns auf die Kommunikation von Gerät zu Gerät konzentrieren, indem wir nur unsere App verwenden, werben und suchen wir für denselben Dienst.

In einer herkömmlichen Client/Server-Konfiguration würde ein Gerät (der Server) seine Dienste ankündigen und der Client würde danach suchen. Da wir egalitär sind, möchten wir keine Rollen für unsere Geräte festlegen müssen; Wir werden jedes Gerät sowohl werben als auch durchsuchen lassen.

Wir müssen unserem MPCManager eine Methode hinzufügen, um Geräte zu erstellen, wenn sie entdeckt werden, und sie in unserem Geräte-Array zu verfolgen. Unsere Methode nimmt eine MCPeerID , sucht nach einem vorhandenen Gerät mit dieser ID und gibt es zurück, wenn es gefunden wird. Wenn wir noch kein vorhandenes Gerät haben, erstellen wir ein neues und fügen es unserem Geräte-Array hinzu.

 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 }

Nachdem ein Gerät mit der Werbung begonnen hat, kann ein anderes Browsergerät versuchen, sich mit ihm zu verbinden. Wir müssen unserer MPCSession -Klasse Delegate-Methoden hinzufügen, um in diesem Fall eingehende Delegate-Anrufe von unserem Werbetreibenden zu verarbeiten:

 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) } }

…eine Methode auf unserem Gerät, um die MCSession zu erstellen:

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

…und schließlich eine Methode, um die Einladung auszulösen, wenn unser Browser einen Werbetreibenden entdeckt:

 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) }

Im Moment ignorieren wir das Argument withDiscoveryInfo ; Wir könnten dies verwenden, um bestimmte Geräte basierend auf dem, was sie zur Verfügung gestellt haben, herauszufiltern (dies ist das gleiche Wörterbuch, das wir im discoveryInfo -Argument für MCNearbyServiceAdvertiser oben angegeben haben).

Anschließen von Geräten

Jetzt, da wir uns um unseren gesamten Haushalt gekümmert haben, können wir mit dem eigentlichen Geschäft beginnen, Geräte anzuschließen.

In der Init-Methode unserer MPCSession richten wir sowohl unseren Advertiser als auch unseren Delegaten ein. Wenn wir bereit sind, mit der Verbindung zu beginnen, müssen wir beide starten. Dies kann in der didFinishLaunching-Methode des App-Delegaten oder wann immer es angebracht ist erfolgen. Hier ist die Methode start() , die wir unserer Klasse hinzufügen werden:

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

Diese Anrufe bedeuten, dass Ihre App beginnt, ihre Präsenz über WLAN zu übertragen. Beachten Sie, dass Sie nicht mit einem WLAN-Netzwerk verbunden sein müssen, damit dies funktioniert (es muss jedoch aktiviert sein).

Wenn ein Gerät auf eine Einladung antwortet und seine MCSession startet, beginnt es mit dem Empfang von Delegiertenrückrufen von der Sitzung. Wir fügen Handler für diese zu unserem Geräteobjekt hinzu; die meisten von ihnen werden wir vorerst ignorieren:

 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?) { } }

Im Moment beschäftigen wir uns hauptsächlich mit dem callback session(_:peer:didChangeState:) . Dies wird immer dann aufgerufen, wenn ein Gerät in einen neuen Zustand übergeht ( notConnected , connecting und connected ). Wir möchten dies verfolgen, damit wir eine Liste aller verbundenen Geräte erstellen können:

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

Nachrichten senden

Jetzt, da wir alle unsere Geräte verbunden haben, ist es an der Zeit, Nachrichten hin und her zu senden. MPC bietet hierfür drei Möglichkeiten:

  • Wir können einen Block von Bytes (ein Datenobjekt) senden
  • Wir können eine Datei senden
  • Wir können einen Stream zum anderen Gerät öffnen

Der Einfachheit halber betrachten wir nur die erste dieser Optionen. Wir senden einfache Nachrichten hin und her und kümmern uns nicht zu sehr um die Komplexität von Nachrichtentypen, Formatierung usw. Wir verwenden eine codierbare Struktur, um unsere Nachricht zu kapseln, die so aussehen wird:

 struct Message: Codable { let body: String }

Wir fügen dem Gerät auch eine Erweiterung hinzu, um Folgendes zu senden:

 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 }

Schlussfolgerungen

Dieser Artikel behandelt die Architektur, die zum Aufbau der Netzwerkkomponenten einer MultipeerConnectivity-basierten Anwendung erforderlich ist. Der vollständige Quellcode (verfügbar auf Github) bietet einen minimalen Wrapper für die Benutzeroberfläche, mit dem Sie verbundene Geräte anzeigen und Nachrichten zwischen ihnen senden können.

MPC bietet nahezu nahtlose Konnektivität zwischen Geräten in der Nähe, ohne sich Gedanken über WLAN-Netzwerke, Bluetooth oder komplexe Client/Server-Gymnastik machen zu müssen. Die Möglichkeit, schnell ein paar Telefone für eine kurze Gaming-Session zu koppeln oder zwei Geräte zur gemeinsamen Nutzung zu verbinden, erfolgt in typischer Apple-Manier.

Der Quellcode für dieses Projekt ist auf Github unter https://github.com/bengottlieb/MultipeerExample verfügbar.

Entwerfen Sie ein iOS, das AFNetworking verwendet? Das Model-View-Controller (MVC)-Entwurfsmuster eignet sich hervorragend für eine wartbare Codebasis, aber manchmal benötigen Sie aufgrund von Bedenken wie DRY-Code, zentralisierter Netzwerkprotokollierung und insbesondere Ratenbegrenzung eine einzelne Klasse, um Ihr Netzwerk zu verwalten. Lesen Sie alles darüber, wie Sie dies mit einer Singleton-Klasse handhaben, in iOS Centralized and Decoupled Networking: AFNetworking Tutorial with a Singleton Class