Gizli Anlaşma: iOS'ta MultipeerConnectivity ile Yakındaki Cihaz Ağı
Yayınlanan: 2022-03-11Geleneksel olarak, eşler arası iletişim için cihazların bağlanması biraz ağır bir yük olmuştur. Bir uygulamanın etrafındakileri keşfetmesi, her iki taraftaki bağlantıları açması ve ardından ağ altyapısı, bağlantılar, mesafeler vb. değiştikçe bunları sürdürmesi gerekir. iOS 7 ve macOS 10.10'da bu etkinliklerin doğasında var olan zorlukları fark eden Apple, uygulamaların bu görevleri nispeten düşük bir çabayla gerçekleştirmesine olanak sağlamak için tasarlanan MultipeerConnectivity çerçevesini (bundan böyle MPC) tanıttı.
MPC, burada temel alınan gerekli altyapının çoğunu halleder:
- Çoklu ağ arayüzü desteği (Bluetooth, WiFi ve ethernet)
- Cihaz algılama
- Şifreleme yoluyla güvenlik
- Küçük mesaj geçişi
- Dosya transferi
Bu makalede, esas olarak iOS uygulamasını ele alacağız, ancak bunların tümü olmasa da çoğu macOS ve tvOS için geçerlidir.

Çok Eşli Oturum Yaşam Döngüsü:
-
MCNearbyServiceAdvertiser.startAdvertisingForPeers()
-
MCNearbyServiceBrowser.startBrowsingForPeers()
-
MCNearbyServiceBrowserDelegate.browser(:foundPeer:withDiscoveryInfo:)
-
MCNearbyServiceBrowser.invitePeer(...)
-
MCNearbyServiceAdvertiserDelegate.didReceiveInvitationFromPeer(...)
-
didReceiveInvitation
invitationHandler
işleyicisini çağırın -
Create the MCSession
-
MCSession.send(...)
-
MCSessionDelegate.session(_:didReceive:data:peerID)
-
MCSession.disconnect()
Zaman zaman bu resme bakın
Çok sayıda MultipeerConnectivity öğreticisi ve iOS geliştiricilerine MPC tabanlı bir uygulamanın uygulanmasında yol gösterdiğini iddia eden örnekler var. Bununla birlikte, deneyimlerime göre, bunlar genellikle eksiktir ve MPC ile bazı önemli potansiyel engellerin üzerini örtme eğilimindedir. Bu makalede, hem okuyucuyu böyle bir uygulamanın ilkel bir uygulamasında gezdirmeyi hem de takılmanın kolay olduğunu düşündüğüm alanları belirtmeyi umuyorum.
Kavramlar ve Sınıflar
MPC, bir avuç sınıfa dayanmaktadır. Ortak olanların listesini gözden geçirelim ve çerçeve hakkındaki anlayışımızı geliştirelim.
-
MCSession
– Bir oturum, ilişkili eşleri arasındaki tüm iletişimleri yönetir. Bir oturum aracılığıyla mesajlar, dosyalar ve akışlar gönderebilirsiniz ve bunlardan biri bağlı bir eşten alındığında temsilcisi bilgilendirilecektir. -
MCPeerID
– Bir eş kimliği, bir oturum içindeki tek tek eş aygıtları tanımlamanıza olanak tanır. Bununla ilişkili bir adı var, ancak dikkatli olun: aynı ada sahip eş kimlikleri aynı kabul edilmez (aşağıdaki Temel Kurallara bakın). -
MCNearbyServiceAdvertiser
– Bir reklamveren, hizmet adınızı yakındaki cihazlara yayınlamanıza olanak tanır. Bu onların sizinle bağlantı kurmasını sağlar. -
MCNearbyServiceBrowser
– Bir tarayıcı,MCNearbyServiceAdvertiser
kullanarak cihazları aramanıza olanak tanır. Bu iki sınıfı birlikte kullanmak, yakındaki cihazları keşfetmenize ve eşler arası bağlantılarınızı oluşturmanıza olanak tanır. -
MCBrowserViewController
– Bu, yakındaki cihaz servislerine göz atmak için çok temel bir UI sağlar (MCNearbyServiceAdvertiser
aracılığıyla satılır). Bazı kullanım durumları için uygun olsa da, deneyimime göre MCP'nin en iyi yönlerinden biri kusursuzluğu olduğu için bunu kullanmayacağız.
Temel kurallar
Bir MPC ağı oluştururken akılda tutulması gereken birkaç şey vardır:
- Cihazlar, MCPeerID nesneleri tarafından tanımlanır. Bunlar yüzeysel olarak sarılmış dizelerdir ve aslında basit adlarla başlatılabilirler. Aynı dizeyle iki MCPeerID oluşturulabilmesine rağmen, bunlar aynı değildir. Bu nedenle, MCPeerID'ler asla kopyalanmamalı veya yeniden oluşturulmamalıdır; uygulama içinde dolaştırılmalıdırlar. Gerekirse, bir NSArchiver kullanılarak saklanabilirler.
- Üzerindeki belgeler eksik olsa da, MCSession ikiden fazla cihaz arasında iletişim kurmak için kullanılabilir. Ancak, deneyimlerime göre, bu nesneleri kullanmanın en kararlı yolu, cihazınızın etkileşimde bulunduğu her eş için bir tane oluşturmaktır.
- Uygulamanız arka plandayken MPC çalışmayacaktır. Uygulamanız arka plandayken tüm MCSession'larınızın bağlantısını kesmeli ve kaldırmalısınız. Herhangi bir arka plan görevinde minimum işlemden fazlasını denemeyin ve yapmayın.
MultipeerConnectivity ile Başlarken
Ağımızı kurmadan önce biraz temizlik yapmamız ve ardından iletişim kurabileceğimiz diğer cihazları keşfetmek için reklamcı ve tarayıcı sınıflarını kurmamız gerekiyor. Birkaç durum değişkenini (yerel MCPeerID ve herhangi bir bağlı cihaz) tutmak için kullanacağımız bir singleton oluşturacağız, ardından MCNearbyServiceAdvertiser
ve MCNearbyServiceBrowser
oluşturacağız. Bu son iki nesne, yalnızca uygulamanızı tanımlayan bir dize olan bir hizmet türüne ihtiyaç duyar. 16 karakterden az olmalı ve mümkün olduğunca benzersiz olmalıdır (yani, "Multipeer" değil, "MyApp-MyCo"). Reklamverenimize, yakındaki cihazlara bakarken biraz daha fazla bilgi vermek için tarayıcıların okuyabileceğinden (küçük) bir sözlük belirleyebiliriz (belki bir oyun türü veya cihaz rolü).
MPC, sistem tarafından sağlanan API'lere dayandığından ve gerçek dünyadaki nesnelerle (diğer aygıtlar ve aralarında paylaşılan "ağ") ilişki kurduğundan, tekli model için iyi bir seçimdir. Sık sık aşırı kullanılsa da, singleton'lar bunun gibi paylaşılan kaynaklar için iyi bir seçimdir.
İşte singleton'ımızın tanımı:
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
kullanıcı varsayılanlarında ( NSKeyedArchiver
) depoladığımızı ve yeniden kullandığımızı unutmayın. Yukarıda bahsedildiği gibi, bu önemlidir ve bir şekilde önbelleğe alınmaması, hattın ilerisinde belirsiz hatalara neden olabilir.
İşte hangi cihazların keşfedildiğini ve durumlarının ne olduğunu takip etmek için kullanacağımız Device sınıfımız:
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) } }
Başlangıç sınıflarımızı oluşturduğumuza göre, artık geri adım atıp tarayıcılar ve reklamverenler arasındaki etkileşimi düşünmenin zamanı geldi. MPC'de bir cihaz sunduğu bir hizmetin reklamını yapabilir ve diğer cihazlarda ilgilendiği bir hizmete göz atabilir. Yalnızca uygulamamızı kullanarak cihazdan cihaza iletişime odaklandığımız için, aynı hizmetin hem reklamını yapacağız hem de göz atacağız.
Geleneksel bir istemci/sunucu yapılandırmasında, bir cihaz (sunucu) hizmetlerinin reklamını yapar ve istemci bunlara göz atar. Eşitlikçi olduğumuz için cihazlarımız için roller belirlemek zorunda kalmak istemiyoruz; her cihazın hem reklamını yapmasını hem de göz atmasını sağlayacağız.
Cihazları keşfedildikleri gibi oluşturmak ve cihazlar dizimizde izlemek için MPCManager
bir yöntem eklememiz gerekiyor. Metodumuz bir MCPeerID
alacak, bu kimliğe sahip mevcut bir cihazı arayacak ve bulunursa geri verecektir. Halihazırda mevcut bir cihazımız yoksa yeni bir tane oluşturup onu cihaz dizimize ekliyoruz.

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 }
Bir cihaz reklam vermeye başladıktan sonra, başka bir tarama cihazı ona bağlanmayı deneyebilir. Bu durumda reklamverenimizden gelen temsilci çağrılarını işlemek için MPCSession
sınıfımıza temsilci yöntemleri eklememiz gerekecek:
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) } }
…Cihazımızda MCSession oluşturmak için bir yöntem:
func connect() { if self.session != nil { return } self.session = MCSession(peer: MPCManager.instance.localPeerID, securityIdentity: nil, encryptionPreference: .required) self.session?.delegate = self }
…ve son olarak, tarayıcımız bir reklamveren keşfettiğinde daveti tetikleyecek bir yöntem:
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) }
Şu anda withDiscoveryInfo
argümanını yok sayıyoruz; bunu, belirli aygıtları kullanılabilir hale getirdiklerine göre filtrelemek için kullanabiliriz (bu, yukarıda discoveryInfo
için MCNearbyServiceAdvertiser
bağımsız değişkeninde sağladığımız sözlüğün aynısıdır).
Cihazları Bağlama
Artık tüm temizlik işlerimizi hallettiğimize göre, gerçek cihaz bağlama işine başlayabiliriz.
MPCSession'ın init yönteminde hem reklamcımızı hem de temsilcimizi kurduk. Bağlanmaya başlamaya hazır olduğumuzda, ikisini de başlatmamız gerekecek. Bu, Uygulama temsilcisinin didFinishLaunching yönteminde veya uygun olduğunda yapılabilir. İşte sınıfımıza ekleyeceğimiz start()
yöntemi:
func start() { self.advertiser.startAdvertisingPeer() self.browser.startBrowsingForPeers() }
Bu aramalar, uygulamanızın varlığını WiFi üzerinden yayınlamaya başlayacağı anlamına gelir. Bunun çalışması için bir WiFi ağına bağlı olmanız gerekmediğini unutmayın (ancak açık olması gerekir).
Bir cihaz bir davete yanıt verdiğinde ve MCSession'ını başlattığında, oturumdan temsilci geri aramaları almaya başlayacaktır. Cihaz nesnemize bunlar için işleyiciler ekleyeceğiz; çoğunu şimdilik görmezden geleceğiz:
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?) { } }
Şimdilik, esas olarak session(_:peer:didChangeState:)
geri çağrısıyla ilgileniyoruz. Bu, bir aygıt yeni bir duruma geçtiğinde ( notConnected
, connecting
ve connected
) çağrılır. Tüm bağlı cihazların bir listesini oluşturabilmemiz için bunu takip etmek isteyeceğiz:
extension MPCManager { var connectedDevices: [Device] { return self.devices.filter { $0.state == .connected } } }
Mesaj gönderme
Artık tüm cihazlarımızı bağladığımıza göre, gerçekten ileri geri mesaj göndermeye başlamanın zamanı geldi. MPC bu konuda üç seçenek sunar:
- Bir bayt bloğu gönderebiliriz (bir veri nesnesi)
- dosya gönderebiliriz
- Diğer cihaza bir akış açabiliriz
Basitlik adına, bu seçeneklerden yalnızca ilkine bakacağız. Basit mesajları ileri geri göndereceğiz ve mesaj türleri, biçimlendirme vb. karmaşıklıkları hakkında çok fazla endişelenmeyeceğiz. Mesajımızı kapsüllemek için şöyle görünecek bir Kodlanabilir yapı kullanacağız:
struct Message: Codable { let body: String }
Ayrıca şunlardan birini göndermek için Cihaza bir uzantı ekleyeceğiz:
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”) genel işlev oturumu(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { if let message = try? JSONDecoder().decode(Message.self, from: data) { NotificationCenter.default.post(name: Device.messageReceivedNotification, nesne: mesaj, 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 }
Sonuçlar
Bu makale, MultipeerConnectivity tabanlı bir uygulamanın ağ bileşenlerini oluşturmak için gereken mimariyi kapsar. Tam kaynak kodu (Github'da mevcuttur), bağlı cihazları görüntülemenize ve bunlar arasında mesaj göndermenize izin veren minimal bir kullanıcı arayüzü sarmalayıcı sunar.
MPC, WiFi ağları, Bluetooth veya karmaşık istemci/sunucu jimnastiği hakkında endişelenmenize gerek kalmadan yakındaki cihazlar arasında neredeyse kesintisiz bağlantı sunar. Kısa bir oyun oturumu için birkaç telefonu hızlı bir şekilde eşleştirebilmek veya paylaşım için iki cihazı bağlayabilmek, tipik Apple tarzında yapılır.
Bu projenin kaynak kodu Github'da https://github.com/bengottlieb/MultipeerExample adresinde mevcuttur.
AFNetworking kullanan bir iOS tasarlamak mı? Model-View-Controller (MVC) tasarım modeli, sürdürülebilir bir kod tabanı için harikadır, ancak bazen DRY kodu, merkezi ağ günlüğü kaydı ve özellikle hız sınırlama gibi endişeler nedeniyle ağınızı yönetmek için tek bir sınıfa ihtiyacınız vardır. Bunu iOS Merkezileştirilmiş ve Ayrılmış Ağ İletişiminde Singleton Sınıfı ile ele alma hakkında her şeyi okuyun: Singleton Sınıfı ile AFNetworking Eğitimi