个性化阅读
专注于IT技术分析

串通:iOS中具有MultipeerConnectivity的附近设备联网

本文概述

传统上, 用于点对点通信的连接设备有些繁重。应用程序需要发现周围的事物, 打开双方的连接, 然后随着网络基础结构, 连接, 距离等的变化而进行维护。意识到这些活动中固有的困难, Apple在iOS 7和macOS 10.10中引入了其MultipeerConnectivity框架(此后称为MPC), 旨在使应用程序以相对较低的工作量即可执行这些任务。

MPC在这里处理了许多基础需求基础架构:

  • 多种网络接口支持(蓝牙, WiFi和以太网)
  • 设备检测
  • 加密安全
  • 小消息传递
  • 文件传输

在本文中, 我们将主要讨论iOS的实现, 但大多数(如果不是全部)都适用于macOS和tvOS。

多对等连接会话生命周期

多对等会话生命周期:

  1. MCNearbyServiceAdvertiser.startAdvertisingForPeers()
  2. MCNearbyServiceBrowser.startBrowsingForPeers()
  3. MCNearbyServiceBrowserDelegate.browser(:foundPeer:withDiscoveryInfo 🙂
  4. MCNearbyServiceBrowser.invitePeer(…)
  5. MCNearbyServiceAdvertiserDelegate.didReceiveInvitationFromPeer(…)
  6. 在didReceiveInvitation中调用InvitationHandler
  7. 创建MCSession
  8. MCSession.send(…)
  9. MCSessionDelegate.session(_:didReceive:data:peerID)
  10. MCSession.disconnect()

时不时返回这张图片

那里有许多MultipeerConnectivity教程和示例, 旨在引导iOS开发人员完成基于MPC的应用程序的实现。但是, 以我的经验来看, 它们通常是不完整的, 往往会掩盖MPC的一些重要潜在绊脚石。在本文中, 我希望引导读者逐步了解该应用程序的基本实现, 并指出我发现容易卡住的地方。

概念与类

MPC基于少数几个类。让我们浏览一下常见的列表, 并加深对框架的理解。

  • MCSession –会话管理其关联对等方之间的所有通信。你可以通过会话发送消息, 文件和流, 当从连接的对等方收到消息, 文件和流之一时, 将通知其委托。
  • MCPeerID –对等ID可让你识别会话中的各个对等设备。它有一个相关的名称, 但要小心:具有相同名称的对等ID不会被认为是相同的(请参见下面的基本规则)。
  • MCNearbyServiceAdvertiser –广告商允许你将服务名称广播到附近的设备。这使他们可以连接到你。
  • MCNearbyServiceBrowser –使用浏览器, 你可以使用MCNearbyServiceAdvertiser搜索设备。结合使用这两个类, 可以发现附近的设备并创建对等连接。
  • MCBrowserViewController –这提供了一个非常基本的UI, 用于浏览附近的设备服务(通过MCNearbyServiceAdvertiser出售)。虽然适用于某些用例, 但我们不会使用它, 因为以我的经验, MCP的最佳方面之一就是它的无缝性。

基本规则

构建MPC网络时, 需要牢记以下几点:

  • 设备由MCPeerID对象标识。这些是表面上包裹的字符串, 实际上可以用简单的名称初始化。尽管可以使用相同的字符串创建两个MCPeerID, 但是它们并不相同。因此, 绝不应复制或重新创建MCPeerID。它们应该在应用程序中传递。如有必要, 可以使用NSArchiver存储它们。
  • 尽管缺少文档, 但MCSession可用于在两个以上的设备之间进行通信。但是, 以我的经验, 利用这些对象的最稳定方法是为设备与之交互的每个对等对象创建一个。
  • 当你的应用程序在后台运行时, MPC将无法运行。当应用程序进入后台时, 你应该断开连接并拆除所有MCSession。在任何后台任务中, 请勿尝试做最少的操作。

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参数;我们可以使用它根据可用的设备过滤掉特定设备(这与我们在上面的MCNearbyServiceAdvertiser的DiscoveryInfo参数中提供的词典相同)。

连接设备

现在, 我们已经完成了所有客房整理工作, 接下来就可以开始连接设备的实际业务了。

在MPCSession的初始化方法中, 我们设置了广告客户和代表。当我们准备开始连接时, 我们需要同时启动它们。这可以通过应用程序委托的didFinishLaunching方法完成, 也可以在适当的时候完成。这是我们将添加到类中的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 :)回调。每当设备转换到新状态(未连接, 正在连接和已连接)时, 都会调用此方法。我们将对此进行跟踪, 以便我们建立所有已连接设备的列表:

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

传送讯息

现在我们已经连接了所有设备, 是时候开始真正地来回发送消息了。 MPC在这方面提供了三种选择:

  • 我们可以发送一个字节块(一个数据对象)
  • 我们可以发送文件
  • 我们可以打开到其他设备的流

为了简单起见, 我们将仅讨论其中的第一个选项。我们将来回发送简单的消息, 而不必过于担心消息类型, 格式等方面的复杂性。我们将使用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:

静态let messageReceivedNotification = Notification.Name(” DeviceDidReceiveMessage”)公共功能会话(_ session:MCSession, didReceive数据:Data, 来自对等方peerID:MCPeerID){如果let message =试试? JSONDecoder()。decode(Message.self, 来自:数据){NotificationCenter.default.post(名称:Device.messageReceivedNotification, 对象: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网络, 蓝牙或复杂的客户端/服务器体操。能够以短暂的游戏时间快速配对几部手机, 或连接两台设备进行共享, 这是典型的Apple方式。

Github上的https://github.com/bengottlieb/MultipeerExample提供了该项目的源代码。

设计使用AFNetworking的iOS? Model-View-Controller(MVC)设计模式非常适合维护代码库, 但有时由于DRY代码, 集中式网络日志记录(尤其是速率限制)等问题, 你需要一个类来处理网络。阅读有关在iOS集中式和解耦网络中使用Singleton类进行处理的所有信息:具有Singleton类的AFNetworking教程

赞(0)
未经允许不得转载:srcmini » 串通:iOS中具有MultipeerConnectivity的附近设备联网

评论 抢沙发

评论前必须登录!