Coluziune: Rețea dispozitiv din apropiere cu MultipeerConnectivity în iOS
Publicat: 2022-03-11În mod tradițional, conectarea dispozitivelor pentru comunicații peer-to-peer a fost un pic cam grea. O aplicație trebuie să descopere ce se află în jurul ei, să deschidă conexiuni pe ambele părți și apoi să le mențină pe măsură ce infrastructura de rețea, conexiunile, distanțele etc., toate se schimbă. Dându-și seama de dificultățile inerente acestor activități, în iOS 7 și macOS 10.10 Apple a introdus cadrul său MultipeerConnectivity (de acum înainte MPC), conceput pentru a permite aplicațiilor să îndeplinească aceste sarcini cu efort relativ redus.
MPC are grijă de mare parte din infrastructura necesară aici:
- Suport pentru mai multe interfețe de rețea (Bluetooth, WiFi și ethernet)
- Detectarea dispozitivului
- Securitate prin criptare
- Mic mesaj în trecere
- Transfer de fișier
În acest articol, vom aborda în principal implementarea iOS, dar cele mai multe, dacă nu toate acestea, sunt aplicabile macOS și tvOS.

Ciclul de viață al sesiunii multipeer:
-
MCNearbyServiceAdvertiser.startAdvertisingForPeers()
-
MCNearbyServiceBrowser.startBrowsingForPeers()
-
MCNearbyServiceBrowserDelegate.browser(:foundPeer:withDiscoveryInfo:)
-
MCNearbyServiceBrowser.invitePeer(...)
-
MCNearbyServiceAdvertiserDelegate.didReceiveInvitationFromPeer(...)
- Apelați
invitationHandler
îndidReceiveInvitation
-
Create the MCSession
-
MCSession.send(...)
-
MCSessionDelegate.session(_:didReceive:data:peerID)
-
MCSession.disconnect()
Referiți-vă din când în când la această imagine
Există numeroase tutoriale și exemple MultipeerConnectivity care pretind să ghideze dezvoltatorii iOS prin implementarea unei aplicații bazate pe MPC. Cu toate acestea, din experiența mea, acestea sunt de obicei incomplete și tind să treacă peste unele potențiale obstacole importante cu MPC. În acest articol, sper să ghidez cititorul printr-o implementare rudimentară a unei astfel de aplicații și să spun zonele în care mi s-a părut ușor să rămân blocat.
Concepte și clase
MPC se bazează pe o mână de clase. Să parcurgem lista celor comune și să ne dezvoltăm înțelegerea cadrului.
-
MCSession
– O sesiune gestionează toate comunicațiile dintre colegii săi asociați. Puteți trimite mesaje, fișiere și fluxuri printr-o sesiune, iar delegatul acesteia va fi notificat când unul dintre acestea este primit de la un peer conectat. -
MCPeerID
– Un ID peer vă permite să identificați dispozitivele peer individuale într-o sesiune. Are un nume asociat, dar aveți grijă: ID-urile de la egal la egal cu același nume nu sunt considerate identice (vezi Regulile de bază, mai jos). -
MCNearbyServiceAdvertiser
– Un agent de publicitate vă permite să difuzați numele serviciului dvs. către dispozitivele din apropiere. Acest lucru le permite să se conecteze la tine. -
MCNearbyServiceBrowser
– Un browser vă permite să căutați dispozitive folosindMCNearbyServiceAdvertiser
. Folosirea împreună a acestor două clase vă permite să descoperiți dispozitivele din apropiere și să vă creați conexiunile peer-to-peer. -
MCBrowserViewController
– Acesta oferă o interfață de utilizare foarte simplă pentru navigarea în serviciile dispozitivelor din apropiere (furnizate prinMCNearbyServiceAdvertiser
). Deși este potrivit pentru unele cazuri de utilizare, nu vom folosi acest lucru, deoarece, din experiența mea, unul dintre cele mai bune aspecte ale MCP este perfectiunea sa.
Reguli de bază
Există câteva lucruri de reținut atunci când construiți o rețea MPC:
- Dispozitivele sunt identificate prin obiecte MCPeerID. Acestea sunt, superficial, șiruri de caractere înfășurate și, de fapt, pot fi inițializate cu nume simple. Deși două MCPeerID-uri pot fi create cu același șir, ele nu sunt identice. Astfel, MCPeerID-urile nu ar trebui să fie niciodată copiate sau recreate; acestea ar trebui să fie transmise în cadrul aplicației. Dacă este necesar, acestea pot fi stocate folosind un NSArchiver.
- În timp ce documentația despre aceasta lipsește, MCSession poate fi folosit pentru a comunica între mai mult de două dispozitive. Cu toate acestea, din experiența mea, cel mai stabil mod de a utiliza aceste obiecte este de a crea unul pentru fiecare peer cu care interacționează dispozitivul.
- MPC nu va funcționa în timp ce aplicația dvs. este în fundal. Ar trebui să vă deconectați și să dezactivați toate MCSessions atunci când aplicația dvs. este în fundal. Nu încercați să faceți mai mult decât operațiuni minime în orice activitate de fundal.
Noțiuni introductive cu MultipeerConnectivity
Înainte de a ne putea stabili rețeaua, trebuie să facem puțină întreținere și apoi să setăm clasele de advertiser și browser pentru a descoperi alte dispozitive cu care putem comunica. Vom crea un singleton pe care îl vom folosi pentru a păstra câteva variabile de stare (MCPeerID-ul nostru local și orice dispozitiv conectat), apoi vom crea MCNearbyServiceAdvertiser
și MCNearbyServiceBrowser
. Aceste ultime două obiecte au nevoie de un tip de serviciu, care este doar un șir care identifică aplicația dvs. Trebuie să aibă mai puțin de 16 caractere și să fie cât mai unic posibil (adică „MyApp-MyCo”, nu „Multipeer”). Putem specifica agentului de publicitate un dicționar (mic) decât îl pot citi browserele pentru a oferi puțin mai multe informații atunci când se uită la dispozitivele din apropiere (poate un tip de joc sau un rol de dispozitiv).
Deoarece MPC se bazează pe API-uri furnizate de sistem și se corelează cu obiecte din lumea reală (alte dispozitive, precum și „rețeaua” partajată între ele), este o potrivire bună pentru modelul singleton. Deși sunt adesea suprautilizate, singleton-urile sunt potrivite pentru resurse partajate precum aceasta.
Iată definiția singleton-ului nostru:
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 } }
Rețineți că MCPeerID
-ul nostru în valorile implicite ale utilizatorului (prin NSKeyedArchiver
) și îl reutilizam. După cum am menționat mai sus, acest lucru este important, iar eșecul în cache poate cauza erori obscure mai departe.
Iată clasa noastră de dispozitive, pe care o vom folosi pentru a urmări ce dispozitive au fost descoperite și care este starea acestora:
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) } }
Acum că ne-am construit cursurile inițiale, este timpul să facem un pas înapoi și să ne gândim la interacțiunea dintre browsere și agenți de publicitate. În MPC, un dispozitiv poate face publicitate unui serviciu pe care îl oferă și poate căuta un serviciu de care este interesat pe alte dispozitive. Deoarece ne concentrăm pe comunicarea de la dispozitiv la dispozitiv folosind doar aplicația noastră, vom face publicitate și vom căuta același serviciu.
Într-o configurație tradițională client/server, un dispozitiv (serverul) și-ar face publicitate serviciilor, iar clientul le-ar căuta. Deoarece suntem egalitari, nu vrem să fim nevoiți să specificăm roluri pentru dispozitivele noastre; vom avea fiecare dispozitiv atât să facă publicitate, cât și să navigheze.
Trebuie să adăugăm o metodă la MPCManager
pentru a crea dispozitive pe măsură ce sunt descoperite și a le urmări în matricea noastră de dispozitive. Metoda noastră va lua un MCPeerID
, va căuta un dispozitiv existent cu acel ID și îl va returna dacă este găsit. Dacă nu avem deja un dispozitiv existent, creăm unul nou și îl adăugăm la matricea noastră de dispozitive.

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 }
După ce un dispozitiv a început să facă publicitate, un alt dispozitiv de navigare poate încerca să se atașeze la acesta. Va trebui să adăugăm metode de delegare la clasa noastră MPCSession
pentru a gestiona apelurile delegate primite de la agentul de publicitate în acest caz:
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) } }
… o metodă pe dispozitivul nostru pentru a crea MCSession:
func connect() { if self.session != nil { return } self.session = MCSession(peer: MPCManager.instance.localPeerID, securityIdentity: nil, encryptionPreference: .required) self.session?.delegate = self }
… și în sfârșit o metodă de a declanșa invitația atunci când browserul nostru descoperă un agent de publicitate:
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) }
În acest moment, ignorăm argumentul withDiscoveryInfo
; am putea folosi acest lucru pentru a filtra anumite dispozitive în funcție de ceea ce au pus la dispoziție (acesta este același dicționar pe care l-am furnizat în argumentul discoveryInfo
pentru MCNearbyServiceAdvertiser
, mai sus).
Conectarea dispozitivelor
Acum că ne-am ocupat de toate lucrările menajului, putem începe afacerea reală a conectării dispozitivelor.
În metoda de inițializare a MPCSession, am configurat atât agentul de publicitate, cât și delegatul nostru. Când suntem gata să începem conexiunea, va trebui să le pornim pe amândouă. Acest lucru se poate face în metoda didFinishLaunching a delegatului aplicației sau oricând este necesar. Iată metoda start()
pe care o vom adăuga la clasa noastră:
func start() { self.advertiser.startAdvertisingPeer() self.browser.startBrowsingForPeers() }
Aceste apeluri vor însemna că aplicația dvs. va începe să-și transmită prezența prin WiFi. Rețineți că nu trebuie să fiți conectat la o rețea WiFi pentru ca aceasta să funcționeze (dar trebuie să o aveți activată).
Când un dispozitiv răspunde la o invitație și își începe MCSession, va începe să primească apeluri inverse ale delegaților din sesiune. Vom adăuga handlere pentru acestea la obiectul nostru dispozitiv; pe majoritatea le vom ignora pentru 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?) { } }
Deocamdată, ne preocupă în principal session(_:peer:didChangeState:)
. Acesta va fi apelat ori de câte ori un dispozitiv trece la o stare nouă ( notConnected
, connecting
, and connected
). Vom dori să urmărim acest lucru, astfel încât să putem construi o listă cu toate dispozitivele conectate:
extension MPCManager { var connectedDevices: [Device] { return self.devices.filter { $0.state == .connected } } }
Trimiterea de Mesaje
Acum că avem toate dispozitivele conectate, este timpul să începem să trimitem mesaje înainte și înapoi. MPC oferă trei opțiuni în acest sens:
- Putem trimite un bloc de octeți (un obiect de date)
- Putem trimite un fișier
- Putem deschide un flux pe celălalt dispozitiv
De dragul simplității, ne vom uita doar la prima dintre aceste opțiuni. Vom trimite mesaje simple înainte și înapoi și nu ne vom îngrijora prea mult cu privire la complexitatea tipurilor de mesaje, a formatării, etc. Vom folosi o structură codabilă pentru a încapsula mesajul nostru, care va arăta astfel:
struct Message: Codable { let body: String }
De asemenea, vom adăuga o extensie la Dispozitiv pentru a trimite una dintre acestea:
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, from Peer peerID: MCPeerID) { dacă mesajul lăsat = încercați? JSONDecoder().decode(Message.self, from: data) { NotificationCenter.default.post(nume: Device.messageReceivedNotification, obiect: mesaj, userInfo: [„de la”: 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 }
Concluzii
Acest articol acoperă arhitectura necesară pentru a construi componentele de rețea ale unei aplicații bazate pe MultipeerConnectivity. Codul sursă complet (disponibil pe Github) oferă un pachet minim de interfață cu utilizatorul care vă permite să vizualizați dispozitivele conectate și să trimiteți mesaje între ele.
MPC oferă o conexiune aproape perfectă între dispozitivele din apropiere, fără a fi nevoie să vă faceți griji pentru rețelele WiFi, Bluetooth sau gimnastica complexă client/server. Posibilitatea de a împerechea rapid câteva telefoane pentru o sesiune scurtă de joc sau de a conecta două dispozitive pentru partajare se face în mod tipic Apple.
Codul sursă pentru acest proiect este disponibil pe Github la https://github.com/bengottlieb/MultipeerExample.
Proiectați un iOS care utilizează AFNetworking? Modelul de design Model-View-Controller (MVC) este excelent pentru o bază de cod de întreținere, dar uneori aveți nevoie de o singură clasă pentru a vă gestiona rețeaua din cauza unor preocupări precum codul DRY, înregistrarea centralizată a rețelei și, mai ales, limitarea ratei. Citiți totul despre gestionarea acestui lucru cu o clasă Singleton în rețelele centralizate și decuplate iOS: Tutorial AFNetworking cu o clasă Singleton