Kolusi: Jaringan Perangkat Terdekat dengan Konektivitas Multipeer di iOS

Diterbitkan: 2022-03-11

Secara tradisional, menghubungkan perangkat untuk komunikasi peer-to-peer telah menjadi beban berat. Aplikasi perlu menemukan apa yang ada di sekitarnya, membuka koneksi di kedua sisi, dan kemudian memeliharanya sebagai infrastruktur jaringan, koneksi, jarak, dll, semuanya berubah. Menyadari kesulitan yang melekat dalam aktivitas ini, di iOS 7 dan macOS 10.10 Apple memperkenalkan kerangka kerja MultipeerConnectivity (selanjutnya MPC), yang dirancang untuk memungkinkan aplikasi melakukan tugas ini dengan upaya yang relatif rendah.

MPC menangani sebagian besar infrastruktur dasar yang diperlukan di sini:

  • Dukungan beberapa antarmuka jaringan (Bluetooth, WiFi, dan ethernet)
  • Deteksi perangkat
  • Keamanan melalui enkripsi
  • Pesan kecil lewat
  • Transfer file

Dalam artikel ini, kami terutama akan membahas implementasi iOS, tetapi sebagian besar, jika tidak semuanya, berlaku untuk macOS dan tvOS.

Siklus Hidup Sesi MultipeerConnectivity

Siklus Hidup Sesi Multipeer:

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

Lihat kembali gambar ini dari waktu ke waktu

Ada banyak tutorial dan contoh MultipeerConnectivity di luar sana yang dimaksudkan untuk memandu pengembang iOS melalui penerapan aplikasi berbasis MPC. Namun, menurut pengalaman saya, mereka biasanya tidak lengkap dan cenderung mengabaikan beberapa potensi hambatan penting dengan MPC. Dalam artikel ini, saya berharap untuk memandu pembaca melalui implementasi dasar aplikasi semacam itu dan menyebutkan area yang menurut saya mudah macet.

Konsep & Kelas

MPC didasarkan pada beberapa kelas. Mari kita menelusuri daftar yang umum, dan membangun pemahaman kita tentang kerangka kerja.

  • MCSession – Sesi mengelola semua komunikasi antara rekan-rekan yang terkait. Anda dapat mengirim pesan, file, dan streaming melalui sesi, dan delegasinya akan diberi tahu saat salah satunya diterima dari rekan yang terhubung.
  • MCPeerID – ID rekan memungkinkan Anda mengidentifikasi perangkat rekan individu dalam satu sesi. Ada nama yang terkait dengannya, tetapi hati-hati: ID rekan dengan nama yang sama tidak dianggap identik (lihat Aturan Dasar, di bawah).
  • MCNearbyServiceAdvertiser – Pengiklan mengizinkan Anda untuk menyiarkan nama layanan Anda ke perangkat terdekat. Ini memungkinkan mereka terhubung dengan Anda.
  • MCNearbyServiceBrowser – Peramban memungkinkan Anda mencari perangkat menggunakan MCNearbyServiceAdvertiser . Menggunakan dua kelas ini bersama-sama memungkinkan Anda menemukan perangkat terdekat dan membuat koneksi peer-to-peer Anda.
  • MCBrowserViewController – Ini menyediakan UI yang sangat dasar untuk menjelajahi layanan perangkat terdekat (dijual melalui MCNearbyServiceAdvertiser ). Meskipun cocok untuk beberapa kasus penggunaan, kami tidak akan menggunakan ini, karena, menurut pengalaman saya, salah satu aspek terbaik dari MCP adalah kelancarannya.

Aturan Dasar

Ada beberapa hal yang perlu diingat ketika membangun jaringan MPC:

  • Perangkat diidentifikasi oleh objek MCPeerID. Ini adalah, secara dangkal, string yang dibungkus, dan pada kenyataannya, dapat diinisialisasi dengan nama sederhana. Meskipun dua MCPeerID dapat dibuat dengan string yang sama, keduanya tidak identik. Jadi, MCPeerID tidak boleh disalin atau dibuat ulang; mereka harus diedarkan dalam aplikasi. Jika perlu, mereka dapat disimpan menggunakan NSArchiver.
  • Meskipun dokumentasinya kurang, MCSession dapat digunakan untuk berkomunikasi antara lebih dari dua perangkat. Namun, menurut pengalaman saya, cara paling stabil untuk memanfaatkan objek ini adalah dengan membuatnya untuk setiap rekan yang berinteraksi dengan perangkat Anda.
  • MPC tidak akan berfungsi saat aplikasi Anda berada di latar belakang. Anda harus memutuskan dan menghancurkan semua MCSessions Anda saat aplikasi Anda berada di latar belakang. Jangan mencoba dan melakukan lebih dari operasi minimal dalam tugas latar belakang apa pun.

Memulai Konektivitas Multipeer

Sebelum kami dapat membangun jaringan kami, kami perlu melakukan sedikit pembersihan, dan kemudian menyiapkan kelas pengiklan dan browser untuk menemukan perangkat lain yang dapat kami gunakan untuk berkomunikasi. Kita akan membuat singleton yang akan kita gunakan untuk menampung beberapa variabel status (MCPeerID lokal kita dan semua perangkat yang terhubung), lalu kita akan membuat MCNearbyServiceAdvertiser dan MCNearbyServiceBrowser . Dua objek terakhir ini memerlukan tipe layanan, yang hanya berupa string yang mengidentifikasi aplikasi Anda. Itu harus kurang dari 16 karakter dan harus seunik mungkin (yaitu, "MyApp-MyCo", bukan "Multipeer"). Kami dapat menentukan kamus (kecil) untuk pengiklan kami daripada yang dapat dibaca browser untuk memberikan sedikit lebih banyak informasi saat melihat perangkat terdekat (mungkin jenis game atau peran perangkat).

Karena MPC bergantung pada API yang disediakan sistem dan berkorelasi dengan objek dunia nyata (perangkat lain, serta "jaringan" bersama di antara mereka), ini cocok untuk pola tunggal. Meskipun sering digunakan secara berlebihan, lajang sangat cocok untuk sumber daya bersama seperti ini.

Berikut adalah definisi dari singleton kami:

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

Perhatikan bahwa kami menyimpan MCPeerID kami di default pengguna (melalui NSKeyedArchiver ), dan menggunakannya kembali. Seperti disebutkan di atas, ini penting, dan kegagalan untuk men-cache-nya dalam beberapa cara dapat menyebabkan bug yang tidak jelas lebih jauh.

Inilah kelas Perangkat kami, yang akan kami gunakan untuk melacak perangkat apa yang telah ditemukan, dan statusnya:

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

Sekarang setelah kelas awal kami dibangun, saatnya untuk mundur dan memikirkan interaksi antara browser dan pengiklan. Di MPC, perangkat dapat mengiklankan layanan yang ditawarkannya, dan dapat menelusuri layanan yang diminatinya di perangkat lain. Karena kami berfokus pada komunikasi perangkat-ke-perangkat hanya dengan menggunakan aplikasi kami, kami akan mengiklankan dan menelusuri layanan yang sama.

Dalam konfigurasi klien/server tradisional, satu perangkat (server) akan mengiklankan layanannya, dan klien akan menelusurinya. Karena kami egaliter, kami tidak ingin harus menentukan peran untuk perangkat kami; kami akan membuat setiap perangkat beriklan dan menjelajah.

Kami perlu menambahkan metode ke MPCManager kami untuk membuat perangkat saat ditemukan dan melacaknya di array perangkat kami. Metode kami akan mengambil MCPeerID , mencari perangkat yang ada dengan ID itu, dan mengembalikannya jika ditemukan. Jika kami belum memiliki perangkat yang ada, kami membuat yang baru dan menambahkannya ke array perangkat kami.

 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 }

Setelah perangkat mulai beriklan, perangkat penjelajahan lain dapat mencoba melampirkannya. Kami perlu menambahkan metode delegasi ke kelas MPCSession kami untuk menangani panggilan delegasi masuk dari pengiklan kami dalam kasus ini:

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

…sebuah metode di Perangkat kami untuk membuat MCSession:

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

…dan akhirnya metode untuk memicu undangan saat browser kami menemukan pengiklan:

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

Saat ini, kami mengabaikan argumen withDiscoveryInfo ; kita dapat menggunakan ini untuk memfilter perangkat tertentu berdasarkan apa yang telah mereka sediakan (ini adalah kamus yang sama yang kami sediakan dalam argumen discoveryInfo ke MCNearbyServiceAdvertiser , di atas).

Menghubungkan Perangkat

Sekarang setelah kami menyelesaikan semua urusan rumah tangga, kami dapat memulai bisnis menghubungkan perangkat yang sebenarnya.

Dalam metode init MPCSession, kami menyiapkan pengiklan dan delegasi kami. Saat kita siap untuk mulai terhubung, kita harus memulai keduanya. Ini dapat dilakukan dalam metode didFinishLaunching delegasi Aplikasi, atau kapan pun sesuai. Inilah metode start() yang akan kita tambahkan ke kelas kita:

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

Panggilan ini berarti aplikasi Anda akan mulai menyiarkan kehadirannya melalui WiFi. Perhatikan bahwa Anda tidak perlu terhubung ke jaringan WiFi agar ini berfungsi (tetapi Anda harus mengaktifkannya).

Saat perangkat merespons undangan dan memulai MCSession-nya, perangkat akan mulai menerima panggilan balik delegasi dari sesi. Kami akan menambahkan penangan untuk itu ke objek perangkat kami; kebanyakan dari mereka akan kita abaikan untuk saat ini:

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

Untuk saat ini, kami terutama berfokus pada panggilan balik session(_:peer:didChangeState:) . Ini akan dipanggil setiap kali perangkat bertransisi ke status baru ( notConnected , connecting , dan connected ). Kami ingin melacak ini sehingga kami dapat membuat daftar semua perangkat yang terhubung:

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

Mengirim Pesan

Sekarang setelah semua perangkat kita terhubung, saatnya untuk benar-benar mulai mengirim pesan bolak-balik. MPC menawarkan tiga opsi dalam hal ini:

  • Kami dapat mengirim blok byte (objek data)
  • Kami dapat mengirim file
  • Kami dapat membuka aliran ke perangkat lain

Demi kesederhanaan, kita hanya akan melihat opsi pertama ini. Kami akan mengirim pesan sederhana bolak-balik, dan tidak terlalu khawatir tentang kerumitan jenis pesan, pemformatan, dll. Kami akan menggunakan struktur Codable untuk merangkum pesan kami, yang akan terlihat seperti ini:

 struct Message: Codable { let body: String }

Kami juga akan menambahkan ekstensi ke Perangkat untuk mengirim salah satu dari ini:

 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”) sesi fungsi publik(_ 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 }

Kesimpulan

Artikel ini membahas arsitektur yang diperlukan untuk membangun komponen jaringan dari aplikasi berbasis MultipeerConnectivity. Kode sumber lengkap (tersedia di Github) menawarkan pembungkus antarmuka pengguna minimal yang memungkinkan Anda melihat perangkat yang terhubung, dan mengirim pesan di antara mereka.

MPC menawarkan konektivitas yang hampir mulus antara perangkat terdekat tanpa perlu khawatir tentang jaringan WiFi, Bluetooth, atau senam klien/server yang kompleks. Mampu dengan cepat memasangkan beberapa ponsel untuk sesi permainan singkat, atau menghubungkan dua perangkat untuk berbagi, dilakukan dengan cara khas Apple.

Kode sumber untuk proyek ini tersedia di Github di https://github.com/bengottlieb/MultipeerExample.

Merancang iOS yang menggunakan AFNetworking? Pola desain Model-View-Controller (MVC) sangat bagus untuk basis kode yang dipelihara, tetapi terkadang Anda memerlukan satu kelas untuk menangani jaringan Anda karena masalah seperti kode KERING, pencatatan jaringan terpusat dan, terutama, pembatasan kecepatan. Baca semua tentang menangani ini dengan Kelas Singleton di iOS Jaringan Terpusat dan Terpisah: AFNetworking Tutorial dengan Kelas Singleton