MultipeerConnectivity.framework梳理

AirDrop

苹果在2010推出的OS X 10.7 Lion系统中加入了全新的AirDrop功能,该功能允许两台Mac机之间无线传输文件。区别于传统的局域网文件共享方式,AirDrop不要求两台机器在同一个网络内。用户无需设置,只需要打开AirDrop文件夹即可查看到其他用户,分享文件变得非常便捷。

AirDrop不需要基于(无线)路由器或者手动建立热点组网,它是利用Mac与Mac之间的点对点网络来进行会话传输。这一切由系统在后台完成,无需断开当前WiFi网络,也不影响当前连接WiFi网络的通信,就可以与其他Mac通过内置特定信道通信。

WWDC13上推出的iOS7也开始支持iOS设备之间使用AirDrop实现共享传输。关于AirDrop的条件要求及内部机制,可参考《为什么iOS 7 和 OS X 之间的AirDrop 不能互传?》。

WWDC14推出的OS X 10.10 Yosemite操作系统,终于打通了与iOS移动设备之间的跨平台AirDrop传输。运行Mac OS X Yosemite 10.10版本的Mac设备(型号≥2012)和运行iOS 7及以上的iOS设备(≥iPhone5,≥iPad 4,iPad mini,≥iPod touch)之间才能实现跨平台文件传输。

根据官方资料显示,AirDrop基于蓝牙和WiFi实现(AirDrop does the rest using Wi-Fi and Bluetooth)。具体来说,通过低功耗蓝牙技术(BLE)进行发现(Advertising/Browsing),使用WiFi Direct(P2P WiFi)技术进行数据传输。可参考《iOS 7的AirDrop是利用什么信号来传输的?》《What Is AirDrop? How Does It Work?》。因此,开启AirDrop不要求双方必须联网或连接到同一局域网,但必须同时打开WiFi和蓝牙,且进行传输的两台设备必须保持在9米的范围之内。

 

Multipeer Connectivity

在iOS7中,引入了一个全新的框架——Multipeer Connectivity(多点连接)。利用Multipeer Connectivity框架,即使在没有连接到WiFi(WLAN)或移动网络(xG)的情况下,距离较近的Apple设备(iMac/iPad/iPhone)之间可基于蓝牙和WiFi(P2P WiFi)技术进行发现和连接实现近场通信。

Multipeer Connectivity扩充的功能与利用AirDrop传输文件非常类似,可以将其看作AirDrop不能直接使用的补偿,代价是需要自己实现。手机不联网也能跟附近的人聊得火热的FireChat和See You Around等近场聊天App、近距离无网遥控交互拍照神器拍咯App就是基于Multipeer Connectivity框架实现。

相比AirDrop,Multipeer Connectivity在进行发现和会话时并不要求同时打开WiFi和蓝牙,也不像AirDrop那样强制打开这两个开关,而是根据条件适时选择使用蓝牙或(和)WiFi。粗略测试情况如下:

    • 双方WiFi和蓝牙都未打开:无法发现。
    • 双方都开启蓝牙:通过蓝牙发现和传输。
    • 双方都开启WiFi:通过WiFi Direct发现和传输,速度接近AirDrop(Reliable速率稍低),不知道同一WLAN下是否优先走局域网?
    • 双方都同时开启了WiFi和蓝牙:应该是模拟AirDrop,通过低功耗蓝牙技术扫描发现握手,然后通过WiFi Direct传输。

 

MultipeerConnectivity.framework

MultipeerConnectivity.framework梳理_第1张图片

以下是MultipeerConnectivity.framework的四个核心对象:

    • Peer ID's allow for unique identification.
    • Advertiser objects tells others they're available.
    • Browser objects browse for advertised devices.
    • Session objects handle the communications.

 

@class MCPeerID

MCPeerID represents a peer in a multipeer session.

Peer IDs (MCPeerID) uniquely identify an app running on a device to nearby peers.

provide information that identifies the device and its user to other nearby devices.

类似sockaddr,用于标识连接的两端endpoint,通常是昵称或设备名称。

该对象只开放了displayName属性,私有MCPeerIDInternal对象持有的设备相关的_idString/_pid64字段并未公开。

在许多情况下,客户端同时广播并发现同一个服务,这将导致一些混乱,尤其是在client/server模式中。所以,每一个服务都应有一个类型标示符——serviceType,它是由ASCII字母、数字和“-”组成的短文本串,最多15个字符。

 

@class MCNearbyServiceAdvertiser

MCNearbyServiceAdvertiser advertises availability of the local peer, and handles invitations from nearby peers.

类似broadcaster

  • 主线程(com.apple.main-thread(serial))创建MCNearbyServiceAdvertiser并启动startAdvertisingPeer。
  • MCNearbyServiceAdvertiserDelegate异步回调(didReceiveInvitationFromPeer)切换回主线程。
  • 在主线程didReceiveInvitationFromPeer中创建MCSession并invitationHandler(YES, session)接受会话连接请求(accept参数为YES)。


@class MCNearbyServiceBrowser

MCNearbyServiceBrowser looks for nearby peers, and connects them to sessions.

类似servo listen+client connect

  • 主线程(com.apple.main-thread(serial))创建MCNearbyServiceBrowser并启动startBrowsingForPeers。
  • MCNearbyServiceBrowserDelegate异步回调(foundPeer/lostPeer)切换回主线程。
  • 主线程创建MCSession并启动invitePeer。

 

@class MCSession

A MCSession facilitates communication among all peers in a multipeer session.

(MCSession) provide support for communication between connected peer devices(identified by MCPeerID). 

Session objects maintain a set of peer ID objects that represent the peers connected to the session. 

注意,peerID并不具备设备识别属性。

类似TCP链接中的socket。创建MCSession时,需指定自身MCPeerID,类似bind

避免频繁的会话数据通知阻塞主线程,MCSessionDelegate异步回调(didChangeState/didReceiveCertificate/didReceiveData/didReceiveStream)有一个专门的回调线程——com.apple.MCSession.callbackQueue(serial)。为避免阻塞MCSeesion回调线程,最好新建数据读(写)线程!

 

@class MCAdvertiserAssistant/MCBrowserViewController

MCAdvertiserAssistant为针对Advertiser封装的管理助手;MCBrowserViewController继承自UIViewController,提供了基本的UI应用框架。

MCBrowser/MCAdvertiser的回调线程一般是delegate所在线程Queue:com.apple.main-thread(serial)。

 

==================================================

Session negotiation

1.Advertiser initiation

// MCPeerID标识自身,discoveryInfo为广播信息。

Advertiser::initWithPeer:withDiscoveryInfo:withServiceType


// 启动广播(定时广播)

// tell nearby peers that your app is willing to join sessions of a specified type.

Advertiser::startAdvertisingPeer

2.Browser initiation

// MCPeerID标识自身

Browser::initWithPeer:withServiceType


//启动扫描/搜索,搜索到Advertiser后,回调browser:foundPeer

// let your app search programmatically for nearby devices with apps that support sessions of a particular type.

Browser::startBrowsingForPeers

类似socket(SO_BROADCAST)listen

3.Browser found advertiser

//搜索到advertiser,可以发出会话邀请建立连接

Browser::browser:foundPeer:withDiscoveryInfo:

4.Browser invite advertiser

//advertiser发出会话邀请协商建立通道,类似TCP三次握手中的<SYN>

//需要基于自身MCPeerID创建specifiedMCSession

// creates a session and invite other peers to join it.

// The timeout parameter is seconds and should be a positive value.

Browser::invitePeer:toSession:withContext:timeout:

类似socket connect

// 与peer的session会话链路首先收到connecting通知

Browser Session::didChangeState(MCSessionStateConnecting)

5.Advertiser receive initiation with certificate

// advertiser接收到邀请(未stopAdvertising

Advertiser:didReceiveInvitationFromPeer:withContext:invitationHandler:

{

    // advertiser接受邀请,类似TCP三次握手中的<SYNACK>

    //需要基于自身MCPeerID创建specified MCSession

    // join a session when invited by another peer.

    invitationHandler(YES, session);

}

invitationHandler(YES)类似socket accept

// 与peer的session会话链路首先收到connecting通知

Advertiser Session::didChangeState(MCSessionStateConnecting)


//与peer的session会话链路收到证书

Advertiser Session::didReceiveCertificate

6.Browser receive acknowledge from advertiser

//与peer的session会话链路收到证书

Browser Session::didReceiveCertificate


//与peer的session会话链路收到connected通知

Browser Session::didChangeState(MCSessionStateConnected)


// browser底层可能再给advertiser发送一个<ACK>包?

7.Advertiser receive acknowledge from browser

//与peer的session会话链路收到connected通知

Advertiser Session::didChangeState(MCSessionStateConnected)

至此,双方通信链路协商成功,可以基于session(connect self and peer)向peer发送data、resource或stream。

该框架内部自行维持Session Keep-Alive,具体不可考。注意可能存在的会话过期和配对问题。


以下为典型Browser-Advertiser发现、握手流程:

MultipeerConnectivity.framework梳理_第2张图片 


MultipeerConnectivity.framework梳理_第3张图片

可能存在中间人攻击(man-in-the-middle attacks):

MultipeerConnectivity.framework梳理_第4张图片


Sender(Browser) startBrowsingForPeers,中间人(MitM)同时开启Browsing/Advertising模式。

①Receiver(Advertiser) startAdvertisingPeer

②MitM Browser嗅探到接收Advertiser的广播报文(discoveryInfo&serviceType,可能加密或完全裸露)。

③MitM Advertiser原封不动广播透传嗅探到的接收Advertiser的广播报文discoveryInfo&serviceType)。

④发送Browser有50%概率扫描到MitM Advertiser,误认为接收Advertiser。

⑤发送Browser邀请伪装的MitM Advertiser加入会话(invitePeer with Context,可能加密或完全裸露)。

⑥伪装的MitM Browser透传context邀请接收Advertiser加入会话(invitePeer with Context)。

⑦⑧⑨⑩攻击者与通信的两端同时建立起独立的会话(Browser-MitM[Advertiser/Browser]-Advertiser),透传发送Browser和接收Advertiser之间的所有数据。

    如未加密,整个过程中的数据将被中间人完全窃取,还可能插入新的内容。


MitM攻击解决方案:

核心思路是在打破或限制MitM与Browser/Advertiser两端建立连接,例如发现Advertiser立即锁定,减少MitM Browser透传Advertising报文给Browser的几率,从而减少或避免Browser发现MitM Advertiser。

另外,应增加Browser发起会话(InvitePeer)的身份标识,从而减少MitM Browser伪造Invite报文的几率。

以上过程1~3步中,由于在启动AirDrop之前没有可预知匹配的设备标识属性(例如uuid、MAC地址),广播发现是完全公开透明的,MitM是不可避免的。

发送方或接收方创建MCSession的接口为initWithPeer:securityIdentity:encryptionPreference:

  • 参数2指定加密(MCEncryptionRequired),可避免报文泄露,但是会降低传输效率,且无法阻止中间人完全透传。
  • 若参数1指定Security Identity提供local peer's identity,可基于PKI对会话握手过程进行身份甄别,但是广播前需要提前交换pubKey,这对于无网场景无疑是一大挑战。或者可基于某一闭环体系内可识别的身份校验体制?

受邀请方接受邀请(invitationHandler:YES)后,彼此都会收到对方的证书(didReceiveCertificate:fromPeer),可通过PKI校验机制,从而鉴别peer的身份。


==================================================

Session transmission

1.Messages

(1)sendData

//发送数据(可发给多个MCPeerID)

Session::sendData:toPeers:withMode:error:

// MCSession send modes for the -sendData:toPeers:withMode:error: method

typedefNS_ENUM(NSInteger,MCSessionSendDataMode) {

    MCSessionSendDataReliable,     // guaranteed reliable and in-order delivery

   MCSessionSendDataUnreliable    // sent immediately without queuing, no guaranteed delivery

} NS_ENUM_AVAILABLE(10_10,7_0);

类似socket的两种类型:SOCK_STREAM/SOCK_DGRAM


(2)didReceiveData

//接收数据(可能有多个MCPeerID/MCSession)

Session::session:didReceiveData:fromPeer:

2.Streams

(1)startStream(NSOutputStream

// streamName可预埋length

NSOutputStreamoutStreamSession::startStreamWithName:toPeer:error:

[outStream open];

// 启动发送线程(写数据线程),发送byte stream

if( [outStream hasSpaceAvailable] ) { // 检查流中是否还有可供写入的空间

[outputStream write];// 将buffer中的字节流数据写入流中

}

为保证数据发送的实时性,写数据线程一直while(1)轮询队列中是否有数据要发送。


(2)didReceiveStream(NSInputStream

MCSession底层收到数据流后,其回调线程为com.apple.MCSession.callbackQueue(serial)

如果在该回调线程中直接做数据收割处理,则可能阻塞该回调线程,导致无法及时获取其他的数据流通知。因此,需要新建一个读数据线程(调用[[NSRunLoop currentRunLoop] run]激活循环loop,闲时休眠)。

收到数据流回调didReceiveStream时,将inputStream添加到读数据线程RunLoop的事件源中,在下一个loop,回调NSStreamDelegate,对相应NSStreamEvent进行处理。

// 同一InputStream可能多次接收回调,可提取对比streamName中的length,以判定接收完成

Session::session:didReceiveStream:(NSInputStream*)inputStream withName:fromPeer:

{

//设置代理

inputStream.delegate =self;

//调用performSelector将以下代码块切换到读数据线程执行:

{

    //将该对象分配一个run loop接收stream events

    [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoopforMode:NSDefaultRunLoopMode]

    //启动接收

    [inputStream open];

}

}


//接收回调:NSStreamDelegate

- (void)stream:(NSStream*)inputStream handleEvent:(NSStreamEvent)eventCode

{

switch (eventCode) {

case NSStreamEventHasBytesAvailable:

//从inputstream拷贝字节流数据到buffer中进行组包

[inputStream read];

break;

case NSStreamEventEndEncountered//接收完成

case NSStreamEventErrorOccurred//接收错误

[inputStream close];

[inputStream removeFromRunLoop:[NSRunLoop currentRunLoopforMode:NSDefaultRunLoopMode];

[inputStream release];

break;

}

}

3.Resources

(1)sendResource

//返回NSProgress用于监控进度

Session::sendResourceAtURL:withName:toPeer:withCompletionHandler:

//发送完成时,回调completionHandler

(2)didStartReceivingResource/didFinishReceivingResource

// Start receiving a resource from remote peer

// NSProgress用于监控进度

Session::session:didStartReceivingResource WithName:fromPeer:withProgress:

// Finished receiving a resource from remote peer and saved the content in a temporary location - the app is responsible for moving the file to a permanent location within its sandbox

//接收完成,需从临时localURL移动文件至永久位置

- (void)session:didFinishReceivingResourceWithName:fromPeer:atURL:withError:

以下为典型Browser-Advertiser会话传输流程:

MultipeerConnectivity.framework梳理_第5张图片

参考:

使用AirDrop 以无线方式共享内容
Airdropand Multipeer Connectivity

MultipeerConnectivity Framework

理解iOS7的Multipeer Connectivity框架

MultipeerConnectivity点对点连接

《如何使用MultipeerConnectivity》

《Send the Monkey a Message with Multipeer Connectivity》

《Nayaksb/Airdrop-MultipeerConnectivity》

《Multipeer Connectivity Follow Up》

《Certificate in iOS MCSession》

 

你可能感兴趣的:(MultipeerConnectivity.framework梳理)