Multipeer Connectivity

随着互联网技术以及各种云服务的发展,设备之间的连通变得日益重要。


在这个移动计算的时代,无论在工作还是娱乐,多人合作的可能性变得空前巨大;也是这个注重隐私却充斥着监控的时代,对安全通信的需求也变得空前巨大;同时也是这个充满连接的时代,即便是随处可见的生活小物件也拥有者大方异彩的前景。


所以,iOS7 引入的 Multipeer Connectivity API 可能会成为最优秀的连接设备方式。它让开发者可以重新考虑移动 APP 的构建,重新定义了连接的可能性。


Multipeer Connectivity 允许附近的设备通过基本的 WIFI 网络,peer-to-peer WIFI 和蓝牙个人局域网来通信。连接了的设备之间可以加密地传输信息或者流数据,或者是文件,而没有必要通过中转互联网设备。

Advertising(广播) & Discovering(发现)

通信的第一步就是让设备彼此发现对方,而这通过Advertising以及Discovering这两个服务来实现。


Advertising 使得某个 service(服务) 能被其他设备知道,而 discovery 则是相对的过程,它使得 client 能发现那些被广播出来的服务。在大多数实际的开发环境下,client 会同时实现对同个服务的 discover 以及 advertise。可能这会引起一些疑惑,特别是对那些习惯于 c-s 架构的开发者。


每一个服务都用一个字符串来辨别,称为 ServiceType。字符串可包含 ASCII 字符,数字以及破折号,最大长度为 15 个字符。按照惯例,service name 应该用 app name 来作为开头,然后是破折号再加上对这个服务的描述:

1
let CONNECTIVITY_SERVICE_TYPE = "kiwi-drone"

每个 peer 利用 MCPeerID 对象作为唯一标识。MCPeerID 对象使用一个 name 来初始化,可使用用户自定义的名称,又或者是使用当前设备的名称:

1
let localPeerID = MCPeerID(displayName: UIDevice.currentDevice().name)

Peers 也可以通过手动使用 NSNetService 或者 Bonjour C APIs 来发现,但是这需要更底层的知识以及经历。关于手动管理 peer 的信息可以在 MCSession 相关的文档里面找到。

Advertising

Service 是通过 MCNearbyServiceAdvertiser 来广播出去的,它需要通过 peer,service type 还有一些其他可选的附加信息(discovery information)来进行初始化。

1
2
3
let advertiser = MCNearbyServiceAdvertiser(peer: localPeerID, discoveryInfo: nil, serviceType: CONNECTIVITY_SERVICE_TYPE)
advertiser.delegate = self
advertiser.startAdvertisingPeer()

Discovery information is sent as Bonjour TXT records encoded according to RFC 6763.


可在 advertiser 的 delegate 中执行一些事件处理。举个例子: client 收到来自某个 peer 的连接请求,并且同意建立连接:

1
2
3
4
5
func advertiser(advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: NSData?, invitationHandler: (Bool, MCSession) -> Void) {
let session = MCSession(peer: peerID, securityIdentity: nil, encryptionPreference: .None)
session.delegate = self
invitationHandler(true, session)
}

Creating a Session

在上面的例子中,session 由 advertiser 创建,然后在同意连接请求的时候传送到请求连接的 peer 那边。在初始化 session 时:

  1. securityIdentity 是一个可选的参数,允许 peer 之间通过 X.509 证书安全地识别对方。如果需要传入这个参数,那么第一个元素应该是 SecIdentityRef, 标识当前 client。后面可以添加一个或者多个 SecCertificateRef 对象,用来验证 peer 的身份。
  2. encryptionPreference 参数指定了是否需要加密 peer 之间的通信。

虽然开启加密选项能够提升安全性,但是也会显著降低传输速率。所以,除非应用之间传输的是用户的敏感数据,否则还是推荐使用 MCEncryptionNone 作为选项。


MCSessionDelegate协议会在后面发送以及接受消息的章节中再详细展开。

Discovering

Client 可是使用 MCNearbyServiceBrowser 来发现广播的服务,它通过当前的本地 peerID 以及 service type 来初始化,与前面提到的 MCNearbyServiceAdvertiser 类似:

1
2
3
let browser = MCNearbyServiceBrowser(peer: localPeerID, serviceType: CONNECTIVITY_SERVICE_TYPE)
browser.delegate = self
browser.startBrowsingForPeers()

周围可能存在多个 peer 在广播特定的服务,为了方便用户以及开发者,苹果提供了 MCBrowserViewController 可以让我们快速地连接正在广播的 peer:

1
2
3
4
5
6
7
8
9
10
let browser = MCNearbyServiceBrowser(peer: localPeerID, serviceType: CONNECTIVITY_SERVICE_TYPE)
browser.delegate = self
browser.startBrowsingForPeers()

let session = MCSession(peer: localPeerID, securityIdentity: nil, encryptionPreference: .None)
let browserViewCon = MCBrowserViewController(browser: browser, session: session)
browserViewCon.delegate = self
self.presentViewController(browserViewCon, animated: true) {
browser.startBrowsingForPeers()
}

当 browser 完成对 peer 的连接之后,它会调用 delegate 的 browserViewControllerDidFinish: 方法,然后可以做一些必要的界面或者其他更新操作。

Sending & Receiving Information

当 peer 之间建立了连接之后,信息就可在彼此之间传递了。传输的数据种类有三种:

  • Messages: 是具有明确边界的信息,例如短文本或者较小的序列化对象
  • Streams: 是一个打开的信息通道,用于频繁传输的数据,例如音视频或者实时的传感器事件
  • Resources: 为典型文件,例如图片,电影,文档等等
Messages

Message 的传输样例如下:

1
2
3
4
5
6
7
let message = "Hello, world!"
let data = message.dataUsingEncoding(NSUTF8StringEncoding)!
do {
try session.sendData(data, toPeers: [connectedPeerID], withMode: .Unreliable)
} catch {
NSLog("Send data error:\(error)")
}

Message 的接收是通过 MCSessionDelegate 的方法来实现的。下面这个例子可以看到作为接收方如何获取上面例子中发送出去的 message:

1
2
3
4
func session(session: MCSession, didReceiveData data: NSData, fromPeer peerID: MCPeerID) {
let message = String(data: data, encoding: NSUTF8StringEncoding)
NSLog("Receive message")
}

上面例子发送的数据使用的是 String,而实际上当然也可可以使用 NSKeyedArchiver 来传输序列化对象数据。

但是,为了防护”对象替换攻击(Object Subsitution Attack)”, 我们需要将 requiresSecureCoding 设置为 true。而因此我们传输的对象需要实现 NSSecureCoding 协议。这里可以获取更多信息NSSecureCoding

Streams

Stream 的创建如下,使用 startStreamWithName:toPeer

1
2
3
4
let outputStream = session.startStreamWithName("stream", toPeer: connectedPeer)
outputStream.delegate = self
outputStream.scheduleInRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)
outputStream.open()

在接收方,同样是通过 MCSessionDelegate 的方法

1
2
3
4
5
func session(session: MCSession, didReceiveStream stream: NSInputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
stream.delegate = self
stream.scheduleInRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)
stream.open()
}

发送数据以及接受数据都应该放在 Stream 的 delegate 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func stream(aStream: NSStream, handleEvent eventCode: NSStreamEvent) {
switch eventCode {
case NSStreamEvent.OpenCompleted:
break
case NSStreamEvent.HasBytesAvailable:
break
case NSStreamEvent.HasSpaceAvailable:
break
case NSStreamEvent.ErrorOccurred:
break
case NSStreamEvent.EndEncountered:
break
case NSStreamEvent.None:
break

}
}

Resources

Resource 的传输方法如下:

1
2
3
4
5
6
let fileURL = NSURL.fileURLWithPath("path/to/resource")
let progress = session.sendResourceAtURL(fileURL, withName: fileURL.lastPathComponent!, toPeer: connectedPeer) { (error) in
if let error = error {
NSLog("error")
}
}

返回的 NSProgress 对象可以使用 KVO 的方式来监控文件的传输进度,同时也提供了取消的方法:cancel


Resource 的接受通过 MCSessionDelegate 中的两个方法来实现:

1
2
3
4
5
6
7
8
9
10
11
12
func session(session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, withProgress progress: NSProgress) {
//TODO:
}

func session(session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, atURL localURL: NSURL, withError error: NSError?) {
let destinationURL = NSURL.fileURLWithPath("/path/to/destination")
do {
try NSFileManager.defaultManager().moveItemAtURL(localURL, toURL: destinationURL)
} catch {
NSLog("Move Resource error: \(error)")
}
}

同样,接收方 peer 可以通过监听 delegate 中获取到的 progress 来知道文件的传输进程。当接受完成之后,接收方需要将文件从临时目录中转移出来。


Multipeer Connectivity 是一套具有开拓性的 API,而它的价值正在被挖掘。当下能够使用 AirDrop 的设备也越来越多,沟通连接的便捷性变得无比之高。未来的具有无限可能性。

参考资料

1.Multipeer Connectivity