التواطؤ: شبكة الأجهزة القريبة مع MultipeerConnectivity في iOS

نشرت: 2022-03-11

تقليديا ، كان توصيل الأجهزة للاتصالات من نظير إلى نظير عبئًا ثقيلًا بعض الشيء. يحتاج التطبيق إلى اكتشاف ما يدور حوله ، وفتح الاتصالات على كلا الجانبين ، ثم صيانتها كبنية تحتية للشبكة ، واتصالات ، ومسافات ، وما إلى ذلك ، كل ذلك يتغير. إدراكًا للصعوبات الكامنة في هذه الأنشطة ، في iOS 7 و macOS 10.10 ، قدمت Apple إطار عمل MultipeerConnectivity (من الآن فصاعدًا MPC) ، وهو مصمم للسماح للتطبيقات بأداء هذه المهام بجهد منخفض نسبيًا.

تعتني MPC بالكثير من البنية التحتية الأساسية المطلوبة هنا:

  • دعم واجهة شبكة متعددة (Bluetooth و WiFi و Ethernet)
  • كشف الجهاز
  • الأمان عبر التشفير
  • تمرير الرسائل الصغيرة
  • نقل الملف

في هذه المقالة ، سنتناول بشكل أساسي تطبيق iOS ، ولكن معظم ، إن لم يكن كل هذا ، ينطبق على macOS و tvOS.

دورة حياة جلسة الاتصال

دورة حياة جلسة متعددة الوظائف:

  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 - يتيح لك معرّف النظير تحديد الأجهزة النظيرة الفردية في الجلسة. لقد حصل على اسم مرتبط به ، ولكن كن حذرًا: معرفات الأقران التي تحمل الاسم نفسه لا تعتبر متطابقة (انظر القواعد الأساسية أدناه).
  • MCNearbyServiceAdvertiser - يسمح لك المعلن ببث اسم خدمتك إلى الأجهزة القريبة. هذا يتيح لهم الاتصال بك.
  • MCNearbyServiceBrowser - متصفح يتيح لك البحث عن الأجهزة باستخدام MCNearbyServiceAdvertiser . يتيح لك استخدام هاتين الفئتين معًا اكتشاف الأجهزة المجاورة وإنشاء اتصالات نظير إلى نظير.
  • MCBrowserViewController - يوفر هذا واجهة مستخدم أساسية للغاية لتصفح خدمات الأجهزة القريبة (يتم بيعها عبر MCNearbyServiceAdvertiser ). على الرغم من أنه مناسب لبعض حالات الاستخدام ، إلا أننا لن نستخدم هذا ، حسب تجربتي ، فإن أحد أفضل جوانب MCP هو سلسته.

القواعد الأساسية

هناك بعض الأشياء التي يجب وضعها في الاعتبار عند إنشاء شبكة MPC:

  • يتم التعرف على الأجهزة بواسطة كائنات MCPeerID. هذه ، بشكل سطحي ، سلاسل ملفوفة ، وفي الواقع ، يمكن تهيئتها بأسماء بسيطة. على الرغم من أنه يمكن إنشاء اثنين من MCPeerIDs بنفس السلسلة ، إلا أنهما غير متطابقين. وبالتالي ، لا ينبغي نسخ MCPeerIDs أو إعادة إنشائها ؛ يجب أن يتم تمريرها داخل التطبيق. إذا لزم الأمر ، يمكن تخزينها باستخدام NSArchiver.
  • في حين أن التوثيق الخاص به غير موجود ، يمكن استخدام MCSession للتواصل بين أكثر من جهازين. ومع ذلك ، في تجربتي ، فإن الطريقة الأكثر ثباتًا لاستخدام هذه الكائنات هي إنشاء واحد لكل نظير يتفاعل معه جهازك.
  • لن يعمل MPC أثناء عمل التطبيق الخاص بك في الخلفية. يجب قطع الاتصال بكافة جلسات MCSessions وهدمها عندما يكون تطبيقك في الخلفية. لا تحاول القيام بأكثر من الحد الأدنى من العمليات في أي مهام في الخلفية.

الشروع في العمل مع MultipeerConnectivity

قبل أن نتمكن من إنشاء شبكتنا ، نحتاج إلى إجراء القليل من التدبير المنزلي ، ثم إعداد فصول المعلن والمتصفح لاكتشاف الأجهزة الأخرى التي يمكننا التواصل معها. سنقوم بإنشاء مفردة سنستخدمها للاحتفاظ ببعض متغيرات الحالة (MCPeerID المحلي الخاص بنا وأي أجهزة متصلة) ، ثم MCNearbyServiceAdvertiser و MCNearbyServiceBrowser . يحتاج هذان العنصران الأخيران إلى نوع خدمة ، وهو مجرد سلسلة تحدد تطبيقك. يجب أن يكون أقل من 16 حرفًا ويجب أن يكون فريدًا قدر الإمكان (على سبيل المثال ، "MyApp-MyCo" ، وليس "Multipeer"). يمكننا تحديد قاموس (صغير) لمعلننا أكثر مما تستطيع المتصفحات قراءته لإعطاء معلومات أكثر قليلاً عند النظر إلى الأجهزة المجاورة (ربما نوع اللعبة أو دور الجهاز).

نظرًا لأن MPC تعتمد على واجهات برمجة التطبيقات التي يوفرها النظام وترتبط بكائنات العالم الحقيقي (الأجهزة الأخرى ، بالإضافة إلى "الشبكة" المشتركة بينها) ، فهي مناسبة بشكل جيد للنمط الفردي. على الرغم من الإفراط في استخدامها بشكل متكرر ، إلا أن المفردات مناسبة تمامًا للموارد المشتركة مثل هذه.

هذا هو تعريف المفرد لدينا:

 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 ، وتبحث عن جهاز موجود بهذا المعرف ، وتعيده إذا تم العثور عليه. إذا لم يكن لدينا جهاز موجود بالفعل ، فسننشئ جهازًا جديدًا ونضيفه إلى مجموعة أجهزتنا.

 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 لمفوض التطبيق ، أو متى كان ذلك مناسبًا. إليك طريقة start() التي سنضيفها إلى فصلنا الدراسي:

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

ستعني هذه المكالمات أن تطبيقك سيبدأ في بث وجوده عبر شبكة WiFi. لاحظ أنك لست بحاجة إلى أن تكون متصلاً بشبكة WiFi حتى يعمل هذا (ولكن يجب عليك تشغيلها).

عندما يستجيب جهاز لدعوة ويبدأ جلسة MCS الخاصة به ، سيبدأ في تلقي عمليات رد نداء المفوضين من الجلسة. سنضيف معالجات لتلك إلى كائن أجهزتنا ؛ سنتجاهل معظمها في الوقت الحالي:

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

إرسال الرسائل

الآن بعد أن تم توصيل جميع أجهزتنا ، حان الوقت لبدء إرسال الرسائل ذهابًا وإيابًا. تقدم MPC ثلاثة خيارات في هذا الصدد:

  • يمكننا إرسال كتلة بايت (كائن بيانات)
  • يمكننا إرسال ملف
  • يمكننا فتح دفق إلى الجهاز الآخر

من أجل البساطة ، سننظر فقط في أول هذه الخيارات. سنرسل رسائل بسيطة ذهابًا وإيابًا ، ولا نقلق كثيرًا بشأن تعقيدات أنواع الرسائل ، والتنسيق ، وما إلى ذلك. سنستخدم بنية قابلة للتشفير لتغليف رسالتنا ، والتي ستبدو كما يلي:

 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:

static let messageReceivedNotification = Notification.Name (“DeviceDidReceiveMessage”) جلسة func العامة (_ session: MCSession ، didReceive data: Data ، fromPeer peerID: MCPeerID) {if let message = try؟ JSONDecoder (). فك الشفرة (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 }

الاستنتاجات

تتناول هذه المقالة البنية المطلوبة لإنشاء مكونات الشبكات لتطبيق قائم على MultipeerConnectivity. يوفر كود المصدر الكامل (المتاح على Github) الحد الأدنى من غلاف واجهة المستخدم الذي يسمح لك بمشاهدة الأجهزة المتصلة وإرسال الرسائل فيما بينها.

توفر MPC اتصالاً شبه سلس بين الأجهزة المجاورة دون الحاجة إلى القلق بشأن شبكات WiFi أو Bluetooth أو الجمباز المعقد للعميل / الخادم. القدرة على إقران عدد قليل من الهواتف بسرعة لجلسة لعب قصيرة ، أو توصيل جهازين للمشاركة ، تتم بطريقة Apple النموذجية.

الكود المصدري لهذا المشروع متاح على جيثب على https://github.com/bengottlieb/MultipeerExample.

تصميم iOS يستخدم AFNetworking؟ يعد نمط تصميم Model-View-Controller (MVC) رائعًا لقاعدة كود الصيانة ، ولكن في بعض الأحيان تحتاج إلى فئة واحدة للتعامل مع الشبكات الخاصة بك بسبب مخاوف مثل رمز DRY ، وتسجيل الشبكات المركزية ، وتحديد المعدل على وجه الخصوص. اقرأ كل شيء عن التعامل مع هذا الأمر مع فصل Singleton Class في شبكات iOS المركزية والمفصولة: برنامج تعليمي AFNetworking مع فصل فردي