Collusion : Mise en réseau d'appareils à proximité avec MultipeerConnectivity dans iOS
Publié: 2022-03-11Traditionnellement, la connexion d'appareils pour les communications peer-to-peer a été un peu lourde. Une application doit découvrir ce qui l'entoure, ouvrir des connexions des deux côtés, puis les maintenir à mesure que l'infrastructure réseau, les connexions, les distances, etc., tout change. Conscient des difficultés inhérentes à ces activités, dans iOS 7 et macOS 10.10, Apple a introduit son framework MultipeerConnectivity (ci-après MPC), conçu pour permettre aux applications d'effectuer ces tâches avec un effort relativement faible.
MPC prend en charge une grande partie de l'infrastructure requise sous-jacente ici :
- Prise en charge de plusieurs interfaces réseau (Bluetooth, Wi-Fi et Ethernet)
- Détection de périphérique
- Sécurité par cryptage
- Petit passage de message
- Transfert de fichier
Dans cet article, nous aborderons principalement l'implémentation d'iOS, mais la plupart, sinon la totalité, s'appliquent à macOS et tvOS.

Cycle de vie de session multi-pair :
-
MCNearbyServiceAdvertiser.startAdvertisingForPeers()
-
MCNearbyServiceBrowser.startBrowsingForPeers()
-
MCNearbyServiceBrowserDelegate.browser(:foundPeer:withDiscoveryInfo:)
-
MCNearbyServiceBrowser.invitePeer(...)
-
MCNearbyServiceAdvertiserDelegate.didReceiveInvitationFromPeer(...)
- Appelez le
invitationHandler
dansdidReceiveInvitation
-
Create the MCSession
-
MCSession.send(...)
-
MCSessionDelegate.session(_:didReceive:data:peerID)
-
MCSession.disconnect()
Revenez de temps en temps sur cette image
Il existe de nombreux didacticiels et exemples de MultipeerConnectivity qui visent à guider les développeurs iOS dans la mise en œuvre d'une application basée sur MPC. Cependant, d'après mon expérience, ils sont généralement incomplets et ont tendance à masquer certains obstacles potentiels importants avec MPC. Dans cet article, j'espère à la fois guider le lecteur à travers une implémentation rudimentaire d'une telle application et signaler les domaines où j'ai trouvé facile de rester bloqué.
Concepts et cours
MPC est basé sur une poignée de classes. Parcourons la liste des plus courants et développons notre compréhension du cadre.
-
MCSession
– Une session gère toutes les communications entre ses homologues associés. Vous pouvez envoyer des messages, des fichiers et des flux via une session, et son délégué sera averti lorsque l'un d'entre eux est reçu d'un pair connecté. -
MCPeerID
– Un ID pair vous permet d'identifier des appareils pairs individuels au sein d'une session. Il y a un nom qui lui est associé, mais soyez prudent : les ID de pairs portant le même nom ne sont pas considérés comme identiques (voir les règles de base ci-dessous). -
MCNearbyServiceAdvertiser
– Un annonceur vous permet de diffuser le nom de votre service sur des appareils à proximité. Cela leur permet de se connecter à vous. -
MCNearbyServiceBrowser
– Un navigateur vous permet de rechercher des appareils à l'aideMCNearbyServiceAdvertiser
. L'utilisation de ces deux classes ensemble vous permet de découvrir les appareils à proximité et de créer vos connexions peer-to-peer. -
MCBrowserViewController
- Cela fournit une interface utilisateur très basique pour parcourir les services d'appareils à proximité (vendus viaMCNearbyServiceAdvertiser
). Bien que cela convienne à certains cas d'utilisation, nous ne l'utiliserons pas car, d'après mon expérience, l'un des meilleurs aspects de MCP est sa transparence.
Règles de base
Il y a quelques points à garder à l'esprit lors de la construction d'un réseau MPC :
- Les appareils sont identifiés par des objets MCPeerID. Ce sont, superficiellement, des chaînes enveloppées et, en fait, elles peuvent être initialisées avec des noms simples. Bien que deux MCPeerID puissent être créés avec la même chaîne, ils ne sont pas identiques. Ainsi, les MCPeerID ne doivent jamais être copiés ou recréés ; ils doivent être transmis dans l'application. Si nécessaire, ils peuvent être stockés à l'aide d'un NSArchiver.
- Bien que la documentation à ce sujet fasse défaut, MCSession peut être utilisé pour communiquer entre plus de deux appareils. Cependant, d'après mon expérience, la manière la plus stable d'utiliser ces objets est d'en créer un pour chaque pair avec lequel votre appareil interagit.
- MPC ne fonctionnera pas tant que votre application est en arrière-plan. Vous devez vous déconnecter et supprimer toutes vos MCSessions lorsque votre application est en arrière-plan. N'essayez pas de faire plus que des opérations minimales dans les tâches d'arrière-plan.
Premiers pas avec MultipeerConnectivity
Avant de pouvoir établir notre réseau, nous devons faire un peu de ménage, puis configurer les classes d'annonceur et de navigateur pour découvrir d'autres appareils avec lesquels nous pouvons communiquer. Nous allons créer un singleton que nous utiliserons pour contenir quelques variables d'état (notre MCPeerID local et tous les appareils connectés), puis nous créerons MCNearbyServiceAdvertiser
et MCNearbyServiceBrowser
. Ces deux derniers objets ont besoin d'un type de service, qui est juste une chaîne identifiant votre application. Il doit comporter moins de 16 caractères et doit être aussi unique que possible (par exemple, "MyApp-MyCo", et non "Multipeer"). Nous pouvons spécifier un (petit) dictionnaire à notre annonceur que les navigateurs peuvent lire pour donner un peu plus d'informations sur les appareils à proximité (peut-être un type de jeu ou un rôle d'appareil).
Étant donné que MPC s'appuie sur des API fournies par le système et est en corrélation avec des objets du monde réel (d'autres appareils, ainsi que le « réseau » partagé entre eux), il convient parfaitement au modèle singleton. Bien que fréquemment surutilisés, les singletons conviennent parfaitement aux ressources partagées telles que celle-ci.
Voici la définition de notre singleton :
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 } }
Notez que nous stockons notre MCPeerID
dans les valeurs par défaut de l'utilisateur (via NSKeyedArchiver
) et que nous le réutilisons. Comme mentionné ci-dessus, c'est important, et le fait de ne pas le mettre en cache d'une manière ou d'une autre peut provoquer des bogues obscurs plus loin sur la ligne.
Voici notre classe Device, que nous utiliserons pour savoir quels appareils ont été découverts et quel est leur état :
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) } }
Maintenant que nous avons construit nos classes initiales, il est temps de prendre du recul et de réfléchir à l'interaction entre les navigateurs et les annonceurs. Dans MPC, un appareil peut annoncer un service qu'il offre et il peut rechercher un service qui l'intéresse sur d'autres appareils. Étant donné que nous nous concentrons sur la communication d'appareil à appareil en utilisant uniquement notre application, nous allons à la fois faire de la publicité et rechercher le même service.
Dans une configuration client/serveur traditionnelle, un périphérique (le serveur) annoncerait ses services et le client les rechercherait. Puisque nous sommes égalitaires, nous ne voulons pas avoir à spécifier des rôles pour nos appareils ; nous aurons chaque appareil à la fois annoncer et parcourir.
Nous devons ajouter une méthode à notre MPCManager
pour créer des appareils au fur et à mesure qu'ils sont découverts et les suivre dans notre tableau d'appareils. Notre méthode prendra un MCPeerID
, recherchera un appareil existant avec cet ID et le renverra s'il est trouvé. Si nous n'avons pas déjà un appareil existant, nous en créons un nouveau et l'ajoutons à notre tableau d'appareils.

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 }
Une fois qu'un appareil a commencé à faire de la publicité, un autre appareil de navigation peut tenter de s'y connecter. Nous devrons ajouter des méthodes déléguées à notre classe MPCSession
pour gérer les appels délégués entrants de notre annonceur dans ce cas :
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) } }
…une méthode sur notre appareil pour créer la MCSession :
func connect() { if self.session != nil { return } self.session = MCSession(peer: MPCManager.instance.localPeerID, securityIdentity: nil, encryptionPreference: .required) self.session?.delegate = self }
…et enfin une méthode pour déclencher l'invitation lorsque notre navigateur découvre un annonceur :
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) }
Pour le moment, nous ignorons l'argument withDiscoveryInfo
; nous pourrions l'utiliser pour filtrer des appareils particuliers en fonction de ce qu'ils ont rendu disponible (il s'agit du même dictionnaire que celui que nous avons fourni dans l'argument discoveryInfo
de MCNearbyServiceAdvertiser
, ci-dessus).
Connecter des appareils
Maintenant que nous nous sommes occupés de tout notre entretien ménager, nous pouvons commencer à connecter des appareils.
Dans la méthode init de notre session MPCSession, nous configurons à la fois notre annonceur et notre délégué. Lorsque nous serons prêts à commencer à nous connecter, nous devrons démarrer les deux. Cela peut être fait dans la méthode didFinishLaunching du délégué App, ou chaque fois que cela est approprié. Voici la méthode start()
que nous ajouterons à notre classe :
func start() { self.advertiser.startAdvertisingPeer() self.browser.startBrowsingForPeers() }
Ces appels signifieront que votre application commencera à diffuser sa présence via WiFi. Notez que vous n'avez pas besoin d'être connecté à un réseau WiFi pour que cela fonctionne (mais vous devez l'avoir activé).
Lorsqu'un appareil répond à une invitation et démarre sa MCSession, il commence à recevoir des rappels de délégués de la session. Nous ajouterons des gestionnaires pour ceux-ci à notre objet périphérique ; nous allons ignorer la plupart d'entre eux pour le moment :
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?) { } }
Pour le moment, nous nous intéressons principalement au rappel session(_:peer:didChangeState:)
. Celui-ci sera appelé chaque fois qu'un appareil passe à un nouvel état ( notConnected
, connecting
et connected
). Nous voudrons garder une trace de cela afin de pouvoir créer une liste de tous les appareils connectés :
extension MPCManager { var connectedDevices: [Device] { return self.devices.filter { $0.state == .connected } } }
Envoi de messages
Maintenant que tous nos appareils sont connectés, il est temps de commencer à envoyer des messages dans les deux sens. MPC propose trois options à cet égard :
- Nous pouvons envoyer un bloc d'octets (un objet de données)
- Nous pouvons envoyer un fichier
- Nous pouvons ouvrir un flux vers l'autre appareil
Par souci de simplicité, nous n'examinerons que la première de ces options. Nous enverrons des messages simples dans les deux sens, sans trop nous soucier de la complexité des types de messages, du formatage, etc. Nous utiliserons une structure Codable pour encapsuler notre message, qui ressemblera à ceci :
struct Message: Codable { let body: String }
Nous ajouterons également une extension à Device pour envoyer l'un de ces éléments :
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:
statique let messageReceivedNotification = Notification.Name("DeviceDidReceiveMessage") public func session(_ session : MCSession, didReceive data : Données, 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 }
conclusion
Cet article couvre l'architecture requise pour créer les composants réseau d'une application basée sur MultipeerConnectivity. Le code source complet (disponible sur Github) offre un wrapper d'interface utilisateur minimal qui vous permet de visualiser les appareils connectés et d'envoyer des messages entre eux.
MPC offre une connectivité quasi transparente entre les appareils à proximité sans avoir à se soucier des réseaux WiFi, du Bluetooth ou de la gymnastique client/serveur complexe. Pouvoir coupler rapidement quelques téléphones pour une courte session de jeu, ou connecter deux appareils pour le partage, se fait à la manière typique d'Apple.
Le code source de ce projet est disponible sur Github à https://github.com/bengottlieb/MultipeerExample.
Concevoir un iOS qui utilise AFNetworking ? Le modèle de conception Model-View-Controller (MVC) est idéal pour une base de code maintenable, mais vous avez parfois besoin d'une seule classe pour gérer votre réseau en raison de problèmes tels que le code DRY, la journalisation réseau centralisée et, surtout, la limitation du débit. Lisez tout sur la gestion de cela avec une classe Singleton dans la mise en réseau centralisée et découplée d'iOS : Tutoriel AFNetworking avec une classe Singleton