Zmowa: Sieć urządzeń w pobliżu z łącznością wieloosobową w iOS
Opublikowany: 2022-03-11Tradycyjnie podłączanie urządzeń do komunikacji peer-to-peer było trochę trudne. Aplikacja musi odkryć, co jest wokół niej, otworzyć połączenia po obu stronach, a następnie utrzymywać je w miarę zmiany infrastruktury sieciowej, połączeń, odległości itp. Zdając sobie sprawę z trudności związanych z tymi czynnościami, w iOS 7 i macOS 10.10 firma Apple wprowadziła swoją platformę MultipeerConnectivity (odtąd MPC), zaprojektowaną, aby umożliwić aplikacjom wykonywanie tych zadań przy stosunkowo niewielkim wysiłku.
MPC zajmuje się większością podstawowej wymaganej infrastruktury tutaj:
- Obsługa wielu interfejsów sieciowych (Bluetooth, WiFi i Ethernet)
- Wykrywanie urządzeń
- Bezpieczeństwo poprzez szyfrowanie
- Przekazywanie małych wiadomości
- Transfer plików
W tym artykule zajmiemy się głównie implementacją iOS, ale większość, jeśli nie wszystko, dotyczy macOS i tvOS.

Cykl życia sesji wieloosobowej:
-
MCNearbyServiceAdvertiser.startAdvertisingForPeers()
-
MCNearbyServiceBrowser.startBrowsingForPeers()
-
MCNearbyServiceBrowserDelegate.browser(:foundPeer:withDiscoveryInfo:)
-
MCNearbyServiceBrowser.invitePeer(...)
-
MCNearbyServiceAdvertiserDelegate.didReceiveInvitationFromPeer(...)
- Wywołaj
invitationHandler
wdidReceiveInvitation
-
Create the MCSession
-
MCSession.send(...)
-
MCSessionDelegate.session(_:didReceive:data:peerID)
-
MCSession.disconnect()
Od czasu do czasu wracaj do tego obrazu
Istnieje wiele samouczków i przykładów MultipeerConnectivity, które mają na celu przeprowadzić programistów iOS przez implementację aplikacji opartej na MPC. Jednak z mojego doświadczenia wynika, że są one zwykle niekompletne i mają tendencję do pomijania niektórych ważnych potencjalnych przeszkód w MPC. W tym artykule mam nadzieję, że przeprowadzę czytelnika przez prymitywną implementację takiej aplikacji i wyjaśnię obszary, w których łatwo utknąć.
Koncepcje i klasy
RPP opiera się na kilku klasach. Przejrzyjmy listę najczęstszych i zbudujmy nasze zrozumienie frameworka.
-
MCSession
— sesja zarządza całą komunikacją między powiązanymi z nią partnerami. Możesz wysyłać wiadomości, pliki i strumienie za pośrednictwem sesji, a jej delegat zostanie powiadomiony, gdy jeden z nich zostanie odebrany od połączonego peera. -
MCPeerID
— identyfikator równorzędny umożliwia identyfikację poszczególnych urządzeń równorzędnych w ramach sesji. Ma powiązaną z nim nazwę, ale bądź ostrożny: identyfikatory równorzędne o tej samej nazwie nie są uważane za identyczne (patrz Zasady podstawowe poniżej). -
MCNearbyServiceAdvertiser
— reklamodawca umożliwia rozgłaszanie nazwy usługi na urządzenia znajdujące się w pobliżu. Dzięki temu mogą się z Tobą połączyć. -
MCNearbyServiceBrowser
— przeglądarka umożliwia wyszukiwanie urządzeń przy użyciuMCNearbyServiceAdvertiser
. Korzystanie z tych dwóch klas razem pozwala wykrywać pobliskie urządzenia i tworzyć połączenia peer-to-peer. -
MCBrowserViewController
— zapewnia bardzo podstawowy interfejs użytkownika do przeglądania usług urządzeń w pobliżu (dostarczanych za pośrednictwemMCNearbyServiceAdvertiser
). Chociaż jest to odpowiednie dla niektórych przypadków użycia, nie będziemy tego używać, ponieważ z mojego doświadczenia wynika, że jednym z najlepszych aspektów MCP jest jego płynność.
Podstawowe zasady
Podczas budowy sieci MPC należy pamiętać o kilku kwestiach:
- Urządzenia są identyfikowane przez obiekty MCPeerID. Są to, powierzchownie, owinięte ciągi i faktycznie można je zainicjować prostymi nazwami. Chociaż dwa identyfikatory MCPeerID można utworzyć z tym samym ciągiem, nie są one identyczne. W związku z tym MCPeerIDs nigdy nie powinny być kopiowane ani odtwarzane; powinny być przekazywane w aplikacji. W razie potrzeby można je przechowywać za pomocą NSArchiver.
- Chociaż brakuje dokumentacji na ten temat, MCSession może być używany do komunikacji między więcej niż dwoma urządzeniami. Jednak z mojego doświadczenia wynika, że najstabilniejszym sposobem wykorzystania tych obiektów jest utworzenie jednego dla każdego urządzenia, z którym współpracuje Twoje urządzenie.
- MPC nie będzie działać, gdy aplikacja działa w tle. Powinieneś odłączyć i zniszczyć wszystkie swoje sesje MCS, gdy Twoja aplikacja działa w tle. Nie próbuj wykonywać więcej niż minimalne operacje w zadaniach w tle.
Pierwsze kroki z MultipeerŁączność
Zanim będziemy mogli ustanowić naszą sieć, musimy trochę posprzątać, a następnie skonfigurować klasy reklamodawców i przeglądarki, aby wykryć inne urządzenia, z którymi możemy się komunikować. Stworzymy singletona, którego użyjemy do przechowywania kilku zmiennych stanu (naszego lokalnego MCPeerID i wszelkich podłączonych urządzeń), a następnie utworzymy MCNearbyServiceAdvertiser
i MCNearbyServiceBrowser
. Te dwa ostatnie obiekty wymagają typu usługi, który jest po prostu ciągiem znaków identyfikującym Twoją aplikację. Musi mieć mniej niż 16 znaków i być jak najbardziej unikatowe (tj. „MojaAplikacja-MojaFirma”, a nie „Multipeer”). Możemy określić (mały) słownik dla naszego reklamodawcy, który przeglądarki mogą odczytać, aby podać nieco więcej informacji podczas przeglądania pobliskich urządzeń (na przykład typ gry lub rola urządzenia).
Ponieważ MPC opiera się na interfejsach API dostarczanych przez system i koreluje z obiektami świata rzeczywistego (innymi urządzeniami, a także współdzieloną „siecią” między nimi), jest to dobre dopasowanie do wzorca singleton. Choć często nadużywane, singletony dobrze pasują do współdzielonych zasobów, takich jak ten.
Oto definicja naszego singletona:
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 } }
Zauważ, że przechowujemy nasz MCPeerID
w domyślnych ustawieniach użytkownika (za pomocą NSKeyedArchiver
) i używamy go ponownie. Jak wspomniano powyżej, jest to ważne, a brak buforowania w jakiś sposób może spowodować niejasne błędy w dalszej części linii.
Oto nasza klasa Device, której użyjemy do śledzenia, jakie urządzenia zostały wykryte i jaki jest ich stan:
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) } }
Teraz, gdy mamy już opracowane nasze początkowe klasy, nadszedł czas, aby cofnąć się i pomyśleć o wzajemnym oddziaływaniu przeglądarek i reklamodawców. W MPC urządzenie może reklamować oferowaną usługę i może wyszukiwać usługę, którą jest zainteresowany na innych urządzeniach. Ponieważ koncentrujemy się na komunikacji między urządzeniami za pomocą naszej aplikacji, będziemy zarówno reklamować, jak i przeglądać tę samą usługę.
W tradycyjnej konfiguracji klient/serwer jedno urządzenie (serwer) anonsuje swoje usługi, a klient je przegląda. Ponieważ jesteśmy egalitarni, nie chcemy określać ról dla naszych urządzeń; sprawimy, że każde urządzenie będzie zarówno reklamować, jak i przeglądać.
Musimy dodać metodę do naszego MPCManager
, aby tworzyć urządzenia w miarę ich wykrywania i śledzić je w naszej tablicy urządzeń. Nasza metoda pobierze MCPeerID
, wyszuka istniejące urządzenie o tym identyfikatorze i zwróci je, jeśli zostanie znalezione. Jeśli nie mamy jeszcze istniejącego urządzenia, tworzymy nowe i dodajemy je do naszej tablicy urządzeń.

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 }
Gdy urządzenie zacznie się reklamować, inne urządzenie do przeglądania może spróbować się z nim połączyć. W tym przypadku będziemy musieli dodać metody delegatów do naszej klasy MPCSession
, aby obsługiwać przychodzące wywołania delegatów od naszego reklamodawcy:
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) } }
…metoda na naszym Urządzeniu do tworzenia MCSession:
func connect() { if self.session != nil { return } self.session = MCSession(peer: MPCManager.instance.localPeerID, securityIdentity: nil, encryptionPreference: .required) self.session?.delegate = self }
…i wreszcie sposób na wywołanie zaproszenia, gdy nasza przeglądarka wykryje reklamodawcę:
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) }
W tej chwili ignorujemy argument withDiscoveryInfo
; możemy użyć tego do odfiltrowania poszczególnych urządzeń na podstawie tego, co udostępniły (jest to ten sam słownik, który podaliśmy w argumencie discoveryInfo
w MCNearbyServiceAdvertiser
powyżej).
Podłączanie urządzeń
Teraz, gdy już zajęliśmy się wszystkimi sprawami porządkowymi, możemy rozpocząć rzeczywisty biznes łączenia urządzeń.
W naszej metodzie init MPCSession konfigurujemy zarówno naszego reklamodawcę, jak i naszego delegata. Kiedy będziemy gotowi do rozpoczęcia łączenia, będziemy musieli uruchomić je oba. Można to zrobić w metodzie didFinishLaunching delegata aplikacji lub w razie potrzeby. Oto metoda start()
, którą dodamy do naszej klasy:
func start() { self.advertiser.startAdvertisingPeer() self.browser.startBrowsingForPeers() }
Te połączenia będą oznaczać, że Twoja aplikacja zacznie nadawać swoją obecność przez Wi-Fi. Pamiętaj, że nie musisz być podłączony do sieci Wi-Fi, aby to zadziałało (ale musisz ją włączyć).
Gdy urządzenie odpowie na zaproszenie i rozpocznie swoją sesję MCSession, zacznie odbierać wywołania zwrotne delegata z sesji. Dodamy ich obsługę do naszego obiektu urządzenia; większość z nich na razie zignorujemy:
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?) { } }
Na razie zajmujemy się głównie wywołaniem zwrotnym session(_:peer:didChangeState:)
. Ta funkcja zostanie wywołana za każdym razem, gdy urządzenie przejdzie do nowego stanu ( notConnected
, connecting
, oraz connected
). Będziemy chcieli to śledzić, abyśmy mogli zbudować listę wszystkich podłączonych urządzeń:
extension MPCManager { var connectedDevices: [Device] { return self.devices.filter { $0.state == .connected } } }
Wysyłanie wiadomości
Teraz, gdy mamy już podłączone wszystkie nasze urządzenia, nadszedł czas, aby zacząć wysyłać wiadomości tam iz powrotem. RPP oferuje w tym zakresie trzy opcje:
- Możemy wysłać blok bajtów (obiekt danych)
- Możemy wysłać plik
- Możemy otworzyć strumień na drugie urządzenie
Dla uproszczenia przyjrzymy się tylko pierwszej z tych opcji. Będziemy wysyłać proste wiadomości tam i z powrotem i nie martwić się zbytnio złożonością typów wiadomości, formatowania itp. Do enkapsulacji naszej wiadomości użyjemy struktury Codable, która będzie wyglądać tak:
struct Message: Codable { let body: String }
Dodamy również rozszerzenie do urządzenia, aby wysłać jedno z tych:
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 }
Wnioski
W tym artykule omówiono architekturę wymaganą do zbudowania składników sieciowych aplikacji opartej na technologii MultipeerConnectivity. Pełny kod źródłowy (dostępny na Github) oferuje minimalne opakowanie interfejsu użytkownika, które umożliwia przeglądanie podłączonych urządzeń i wysyłanie wiadomości między nimi.
MPC oferuje niemal bezproblemową łączność między pobliskimi urządzeniami, bez konieczności martwienia się o sieci Wi-Fi, Bluetooth lub złożoną gimnastykę klient/serwer. Możliwość szybkiego sparowania kilku telefonów na krótką sesję grania lub połączenia dwóch urządzeń w celu udostępniania odbywa się w typowy dla Apple sposób.
Kod źródłowy tego projektu jest dostępny na Github pod adresem https://github.com/bengottlieb/MultipeerExample.
Projektujesz system iOS korzystający z AFNetworking? Wzorzec projektowy Model-View-Controller (MVC) doskonale nadaje się do obsługi bazy kodu, ale czasami potrzebujesz jednej klasy do obsługi sieci ze względu na problemy, takie jak kod DRY, scentralizowane rejestrowanie w sieci, a zwłaszcza ograniczanie szybkości. Przeczytaj wszystko o radzeniu sobie z tym z klasą Singleton w iOS Centralized and Decoupled Networking: AFNetworking Tutorial z klasą Singleton