การสมรู้ร่วมคิด: เครือข่ายอุปกรณ์ใกล้เคียงพร้อมการเชื่อมต่อแบบหลายจุดใน iOS

เผยแพร่แล้ว: 2022-03-11

ตามเนื้อผ้า การเชื่อมต่ออุปกรณ์สำหรับการสื่อสารแบบเพียร์ทูเพียร์นั้นค่อนข้างลำบาก แอปพลิเคชันจำเป็นต้องค้นหาสิ่งที่อยู่รอบๆ ตัว เปิดการเชื่อมต่อทั้งสองด้าน แล้วรักษาไว้เป็นโครงสร้างพื้นฐานของเครือข่าย การเชื่อมต่อ ระยะทาง ฯลฯ ทั้งหมดนี้เปลี่ยนแปลงไป โดยตระหนักถึงปัญหาที่มีอยู่ในกิจกรรมเหล่านี้ ใน iOS 7 และ macOS 10.10 Apple ได้เปิดตัวเฟรมเวิร์กการเชื่อมต่อแบบหลายจุด (ต่อจากนี้ไป MPC) ที่ออกแบบมาเพื่อให้แอปทำงานเหล่านี้ได้โดยใช้ความพยายามเพียงเล็กน้อย

กนง.ดูแลโครงสร้างพื้นฐานที่จำเป็นส่วนใหญ่ที่นี่:

  • รองรับอินเทอร์เฟซเครือข่ายหลายตัว (Bluetooth, WiFi และอีเธอร์เน็ต)
  • การตรวจจับอุปกรณ์
  • ความปลอดภัยผ่านการเข้ารหัส
  • ข้อความเล็กผ่าน
  • การถ่ายโอนไฟล์

ในบทความนี้ เราจะกล่าวถึงการใช้งาน iOS เป็นหลัก แต่ส่วนใหญ่จะใช้ได้กับ macOS และ tvOS หากไม่ใช่ทั้งหมด

MultipeerConnectivity เซสชัน LifeCycle

วงจรชีวิตของเซสชันหลายคน:

  1. MCNearbyServiceAdvertiser.startAdvertisingForPeers()
  2. MCNearbyServiceBrowser.startBrowsingForPeers()
  3. MCNearbyServiceBrowserDelegate.browser(:foundPeer:withDiscoveryInfo:)
  4. MCNearbyServiceBrowser.invitePeer(...)
  5. MCNearbyServiceAdvertiserDelegate.didReceiveInvitationFromPeer(...)
  6. เรียก invitationHandler ตัวจัดการใน didReceiveInvitation
  7. Create the MCSession
  8. MCSession.send(...)
  9. MCSessionDelegate.session(_:didReceive:data:peerID)
  10. MCSession.disconnect()

กลับมาดูภาพนี้เป็นครั้งคราว

มีบทช่วยสอน MultipeerConnectivity มากมายและตัวอย่างที่มีจุดประสงค์เพื่อแนะนำนักพัฒนา iOS ผ่านการปรับใช้แอปพลิเคชันที่ใช้ MPC อย่างไรก็ตาม จากประสบการณ์ของผม มักจะไม่สมบูรณ์และมีแนวโน้มที่จะมองข้ามสิ่งกีดขวางที่สำคัญที่อาจเกิดขึ้นกับ MPC ในบทความนี้ ฉันหวังว่าจะได้แนะนำผู้อ่านเกี่ยวกับการใช้งานแอปดังกล่าวเป็นเบื้องต้น และกล่าวถึงส่วนต่างๆ ที่ฉันพบว่าติดขัดได้ง่าย

แนวคิดและชั้นเรียน

MPC ขึ้นอยู่กับชั้นเรียนจำนวนหนึ่ง มาดูรายการทั่วไป และสร้างความเข้าใจของเราเกี่ยวกับกรอบงานกัน

  • MCSession – เซสชั่นจัดการการสื่อสารทั้งหมดระหว่างเพื่อนที่เกี่ยวข้อง คุณสามารถส่งข้อความ ไฟล์ และสตรีมผ่านเซสชันได้ และผู้รับมอบสิทธิ์จะได้รับแจ้งเมื่อได้รับหนึ่งในรายการเหล่านี้จากเพียร์ที่เชื่อมต่อ
  • MCPeerID - รหัสเพียร์ช่วยให้คุณระบุอุปกรณ์เพียร์แต่ละตัวภายในเซสชัน มีชื่อที่เชื่อมโยงอยู่ แต่ระวัง: Peer ID ที่มีชื่อเดียวกันไม่ถือว่าเหมือนกัน (ดูกฎพื้นฐาน ด้านล่าง)
  • MCNearbyServiceAdvertiser – ผู้โฆษณาอนุญาตให้คุณเผยแพร่ชื่อบริการของคุณไปยังอุปกรณ์ใกล้เคียง สิ่งนี้ทำให้พวกเขาเชื่อมต่อกับคุณ
  • MCNearbyServiceBrowser – เบราว์เซอร์ช่วยให้คุณค้นหาอุปกรณ์โดยใช้ MCNearbyServiceAdvertiser การใช้สองคลาสนี้ร่วมกันทำให้คุณสามารถค้นพบอุปกรณ์ใกล้เคียงและสร้างการเชื่อมต่อแบบเพียร์ทูเพียร์ของคุณ
  • MCBrowserViewController – นี่เป็น UI พื้นฐานสำหรับการเรียกดูบริการอุปกรณ์ใกล้เคียง (จำหน่ายผ่าน MCNearbyServiceAdvertiser ) แม้ว่าจะเหมาะสมสำหรับกรณีการใช้งานบางกรณี แต่เราจะไม่ใช้สิ่งนี้ เนื่องจากจากประสบการณ์ของฉัน แง่มุมที่ดีที่สุดของ MCP ประการหนึ่งก็คือความราบรื่นของมัน

กฎพื้นฐาน

มีสองสิ่งที่ควรคำนึงถึงเมื่อสร้างเครือข่าย MPC:

  • อุปกรณ์ถูกระบุโดยอ็อบเจ็กต์ MCpeerID เหล่านี้เป็น ผิวเผิน ห่อสตริง และในความเป็นจริง สามารถเริ่มต้นด้วยชื่อง่าย ๆ แม้ว่า MCPeerID สองรายการอาจถูกสร้างขึ้นด้วยสตริงเดียวกัน แต่ก็ไม่เหมือนกัน ดังนั้น MCPeerID ไม่ควรคัดลอกหรือสร้างขึ้นใหม่ ควรส่งต่อภายในแอปพลิเคชัน หากจำเป็น สามารถเก็บไว้ได้โดยใช้ NSArchiver
  • แม้ว่าเอกสารประกอบจะขาดหายไป แต่ MCSession สามารถใช้เพื่อสื่อสารระหว่างอุปกรณ์มากกว่าสองเครื่องได้ อย่างไรก็ตาม จากประสบการณ์ของผม วิธีที่เสถียรที่สุดในการใช้ออบเจ็กต์เหล่านี้คือการสร้างออบเจ็กต์สำหรับเพียร์แต่ละตัวที่อุปกรณ์ของคุณโต้ตอบด้วย
  • MPC จะไม่ทำงานในขณะที่ใบสมัครของคุณอยู่ในเบื้องหลัง คุณควรยกเลิกการเชื่อมต่อและทำลาย MCSessions ทั้งหมดของคุณเมื่อแอปของคุณมีเบื้องหลัง อย่าพยายามทำมากกว่าการดำเนินการขั้นต่ำในงานพื้นหลังใดๆ

เริ่มต้นใช้งาน MultipeerConnectivity

ก่อนที่เราจะสามารถสร้างเครือข่ายได้ เราจำเป็นต้องดูแลทำความสะอาดเล็กน้อย จากนั้นจึงตั้งค่าคลาสผู้ลงโฆษณาและเบราว์เซอร์เพื่อค้นหาอุปกรณ์อื่นๆ ที่เราสามารถสื่อสารด้วยได้ เราจะสร้างซิงเกิลตันที่เราจะใช้เพื่อเก็บตัวแปรสถานะสองสามตัว (MCPeerID ในพื้นที่และอุปกรณ์ที่เชื่อมต่อ) จากนั้นเราจะสร้าง MCNearbyServiceAdvertiser และ MCNearbyServiceBrowser ออบเจ็กต์สองรายการสุดท้ายนี้ต้องการประเภทบริการ ซึ่งเป็นเพียงสตริงที่ระบุแอปพลิเคชันของคุณ ต้องมีอักขระน้อยกว่า 16 ตัวและต้องไม่ซ้ำกันมากที่สุด (เช่น "MyApp-MyCo" ไม่ใช่ "Multipeer") เราสามารถระบุพจนานุกรม (ขนาดเล็ก) ให้กับผู้โฆษณาของเราได้เกินกว่าที่เบราว์เซอร์จะอ่านได้เพื่อให้ข้อมูลเพิ่มเติมเมื่อดูอุปกรณ์ใกล้เคียง (อาจเป็นประเภทเกมหรือบทบาทของอุปกรณ์)

เนื่องจาก MPC อาศัย API ที่ระบบจัดเตรียมไว้และสัมพันธ์กับอ็อบเจ็กต์ในโลกแห่งความเป็นจริง (อุปกรณ์อื่นๆ รวมทั้ง "เครือข่าย" ที่แชร์ระหว่างกัน) จึงเหมาะอย่างยิ่งสำหรับรูปแบบซิงเกิลตัน แม้ว่าจะใช้มากเกินไป แต่ซิงเกิลตันก็เหมาะสมสำหรับทรัพยากรที่ใช้ร่วมกันเช่นนี้

นี่คือคำจำกัดความของซิงเกิลตันของเรา:

 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 ของเราไว้ในค่าเริ่มต้นของผู้ใช้ (โดยใช้ NSKeyedArchiver ) และนำกลับมาใช้ใหม่ ดังที่ได้กล่าวไว้ข้างต้น สิ่งนี้มีความสำคัญ และความล้มเหลวในการแคชในทางใดทางหนึ่งอาจทำให้เกิดข้อบกพร่องที่ไม่ชัดเจนในระยะต่อไป

นี่คือคลาสอุปกรณ์ของเรา ซึ่งเราจะใช้เพื่อติดตามว่าอุปกรณ์ใดถูกค้นพบ และสถานะของอุปกรณ์คืออะไร:

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

ตอนนี้เราได้สร้างคลาสเริ่มต้นแล้ว ถึงเวลาถอยกลับไปคิดเกี่ยวกับการทำงานร่วมกันระหว่างเบราว์เซอร์และผู้โฆษณา ใน MPC อุปกรณ์สามารถโฆษณาบริการที่นำเสนอ และสามารถเรียกดูบริการที่สนใจในอุปกรณ์อื่นๆ เนื่องจากเรามุ่งเน้นที่การสื่อสารระหว่างอุปกรณ์กับอุปกรณ์โดยใช้เพียงแอปของเรา เราจึงโฆษณาและเรียกดูบริการเดียวกัน

ในการกำหนดค่าไคลเอนต์/เซิร์ฟเวอร์แบบดั้งเดิม อุปกรณ์หนึ่งตัว (เซิร์ฟเวอร์) จะโฆษณาบริการของตน และไคลเอนต์จะค้นหาอุปกรณ์เหล่านั้น เนื่องจากเรามีความเท่าเทียม เราจึงไม่ต้องการระบุบทบาทสำหรับอุปกรณ์ของเรา เราจะมีทุกอุปกรณ์ทั้งโฆษณาและเรียกดู

เราจำเป็นต้องเพิ่มวิธีการใน MPCManager เพื่อสร้างอุปกรณ์ตามที่ค้นพบและติดตามในอาร์เรย์อุปกรณ์ของเรา วิธีการของเราจะใช้ MCPeerID ค้นหาอุปกรณ์ที่มีอยู่ซึ่งมี ID นั้น และส่งคืนหากพบ หากเรายังไม่มีอุปกรณ์อยู่แล้ว เราจะสร้างอุปกรณ์ใหม่และเพิ่มอุปกรณ์นั้นลงในอาร์เรย์อุปกรณ์ของเรา

 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 }

หลังจากที่อุปกรณ์ได้เริ่มโฆษณาแล้ว อุปกรณ์การท่องเว็บอื่นสามารถพยายามเชื่อมต่อกับอุปกรณ์นั้นได้ เราจำเป็นต้องเพิ่มวิธีการมอบสิทธิ์ให้กับคลาส MPCSession ของเราเพื่อจัดการการโทรเข้าของผู้ได้รับมอบหมายจากผู้ลงโฆษณาของเราในกรณีนี้:

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

…วิธีการบนอุปกรณ์ของเราเพื่อสร้าง MCSession:

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

…และสุดท้ายคือวิธีการเรียกคำเชิญเมื่อเบราว์เซอร์ของเราค้นพบผู้โฆษณา:

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

ขณะนี้ เรากำลังละเว้นอาร์กิวเมนต์ withDiscoveryInfo เราสามารถใช้สิ่งนี้เพื่อกรองอุปกรณ์เฉพาะโดยพิจารณาจากสิ่งที่พวกเขามีให้ (นี่คือพจนานุกรมเดียวกันกับที่เราให้ไว้ในอาร์กิวเมนต์ discoveryInfo ของ MCNearbyServiceAdvertiser ด้านบน)

อุปกรณ์เชื่อมต่อ

ตอนนี้เราได้รับการดูแลทำความสะอาดทั้งหมดแล้ว เราก็สามารถเริ่มต้นธุรกิจการเชื่อมต่ออุปกรณ์ได้อย่างแท้จริง

ในวิธีการเริ่มต้นของ MPCSession เราตั้งค่าทั้งผู้โฆษณาและผู้รับมอบสิทธิ์ของเรา เมื่อเราพร้อมที่จะเริ่มเชื่อมต่อ เราจะต้องเริ่มต้นทั้งคู่ ซึ่งสามารถทำได้ในวิธี didFinishLaunching ของผู้รับมอบสิทธิ์ App หรือเมื่อไรก็ตามที่เหมาะสม นี่คือเมธอด start() ที่เราจะเพิ่มในคลาสของเรา:

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

การโทรเหล่านี้จะหมายความว่าแอปของคุณจะเริ่มแพร่ภาพการแสดงตนผ่าน WiFi โปรดทราบว่าคุณไม่จำเป็นต้องเชื่อมต่อกับเครือข่าย WiFi เพื่อให้ทำงานได้ (แต่คุณต้องเปิดเครื่องไว้)

เมื่ออุปกรณ์ตอบรับคำเชิญและเริ่ม MCSession อุปกรณ์จะเริ่มรับการเรียกกลับของผู้รับมอบสิทธิ์จากเซสชัน เราจะเพิ่มตัวจัดการสำหรับสิ่งเหล่านั้นในวัตถุอุปกรณ์ของเรา ส่วนใหญ่เราจะเพิกเฉยในขณะนี้:

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

ในขณะนี้ เรากังวลกับการเรียกกลับ session(_:peer:didChangeState:) เป็นหลัก สิ่งนี้จะถูกเรียกเมื่อใดก็ตามที่อุปกรณ์เปลี่ยนเป็นสถานะใหม่ ( notConnected , กำลัง connecting และ connected ) เราต้องการติดตามสิ่งนี้เพื่อให้เราสามารถสร้างรายการอุปกรณ์ที่เชื่อมต่อทั้งหมด:

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

การส่งข้อความ

เมื่อเราเชื่อมต่ออุปกรณ์ทั้งหมดแล้ว ก็ถึงเวลาเริ่มส่งข้อความไปมาจริงๆ กนง.เสนอทางเลือกสามทางในเรื่องนี้:

  • เราสามารถส่งบล็อกของไบต์ (วัตถุข้อมูล)
  • เราส่งไฟล์ได้
  • เราสามารถเปิดสตรีมไปยังอุปกรณ์อื่นได้

เพื่อความง่าย เราจะดูเฉพาะตัวเลือกแรกเท่านั้น เราจะส่งข้อความธรรมดาไปมา และไม่ต้องกังวลกับความซับซ้อนของประเภทข้อความ การจัดรูปแบบ ฯลฯ มากเกินไป เราจะใช้โครงสร้าง Codable เพื่อสรุปข้อความ ซึ่งจะมีลักษณะดังนี้:

 struct Message: Codable { let body: String }

นอกจากนี้ เราจะเพิ่มส่วนขยายลงในอุปกรณ์เพื่อส่งรายการใดรายการหนึ่งต่อไปนี้

 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:

คงที่ ให้ messageReceivedNotification = การแจ้งเตือน ชื่อ ("DeviceDidReceiveMessage") เซสชันสาธารณะ func (_ เซสชัน: MCSession, ข้อมูล didReceive: ข้อมูล, fromPeer peerID: MCPeerID) { ถ้าให้ข้อความ = ลอง? JSONDecoder ().decode (Message.self จาก: data) { NotificationCenter.default.post (ชื่อ: Device.messageReceivedNotification วัตถุ: ข้อความ userInfo: ["จาก": 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 }

บทสรุป

บทความนี้ครอบคลุมถึงสถาปัตยกรรมที่จำเป็นในการสร้างส่วนประกอบเครือข่ายของแอปพลิเคชันที่ใช้ MultipeerConnectivity ซอร์สโค้ดแบบเต็ม (มีอยู่ใน Github) มีแรปเปอร์อินเทอร์เฟซผู้ใช้ขั้นต่ำที่ให้คุณดูอุปกรณ์ที่เชื่อมต่อและส่งข้อความระหว่างกัน

MPC นำเสนอการเชื่อมต่อที่ราบรื่นระหว่างอุปกรณ์ใกล้เคียงโดยไม่จำเป็นต้องกังวลเกี่ยวกับเครือข่าย WiFi, Bluetooth หรือยิมนาสติกไคลเอนต์/เซิร์ฟเวอร์ที่ซับซ้อน ความสามารถในการจับคู่โทรศัพท์สองสามเครื่องอย่างรวดเร็วสำหรับเซสชั่นการเล่นเกมสั้น ๆ หรือเชื่อมต่ออุปกรณ์สองเครื่องเพื่อแชร์นั้นทำได้ตามแบบฉบับของ Apple

ซอร์สโค้ดสำหรับโครงการนี้มีอยู่ใน Github ที่ https://github.com/bengottlieb/MultipeerExample

การออกแบบ iOS ที่ใช้ AFNetworking? รูปแบบการออกแบบ Model-View-Controller (MVC) นั้นยอดเยี่ยมสำหรับ codebase บำรุงรักษา แต่บางครั้งคุณต้องการคลาสเดียวเพื่อจัดการเครือข่ายของคุณเนื่องจากข้อกังวล เช่น รหัส DRY การบันทึกเครือข่ายแบบรวมศูนย์ และโดยเฉพาะอย่างยิ่ง การจำกัดอัตรา อ่านทั้งหมดเกี่ยวกับการจัดการสิ่งนี้ด้วย Singleton Class ใน iOS Centralized and Decoupled Networking: AFNetworking Tutorial พร้อม Singleton Class