苹果在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米的范围之内。
在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。粗略测试情况如下:
以下是MultipeerConnectivity.framework的四个核心对象:
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个字符。
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)。
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。
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回调线程,最好新建数据读(写)线程!
MCAdvertiserAssistant为针对Advertiser封装的管理助手;MCBrowserViewController继承自UIViewController,提供了基本的UI应用框架。
MCBrowser/MCAdvertiser的回调线程一般是delegate所在线程Queue:com.apple.main-thread(serial)。
==================================================
// MCPeerID标识自身,discoveryInfo为广播信息。
Advertiser::initWithPeer:withDiscoveryInfo:withServiceType
// 启动广播(定时广播)
// tell nearby peers that your app is willing to join sessions of a specified type.
Advertiser::startAdvertisingPeer
// 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。
//搜索到advertiser,可以发出会话邀请建立连接
Browser::browser:foundPeer:withDiscoveryInfo:
//向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)
// advertiser接收到邀请(未stopAdvertising)
Advertiser:didReceiveInvitationFromPeer:withContext:invitationHandler:
{
// advertiser接受邀请,类似TCP三次握手中的<SYN,ACK>
//需要基于自身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
//与peer的session会话链路收到证书
Browser Session::didReceiveCertificate
//与peer的session会话链路收到connected通知
Browser Session::didChangeState(MCSessionStateConnected)
// browser底层可能再给advertiser发送一个<ACK>包?
//与peer的session会话链路收到connected通知
Advertiser Session::didChangeState(MCSessionStateConnected)
至此,双方通信链路协商成功,可以基于session(connect self and peer)向peer发送data、resource或stream。
该框架内部自行维持Session Keep-Alive,具体不可考。注意可能存在的会话过期和配对问题。
以下为典型Browser-Advertiser发现、握手流程:
可能存在中间人攻击(man-in-the-middle attacks):
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的身份。
==================================================
(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:
3.Resources(1)startStream(NSOutputStream)
// streamName可预埋length
NSOutputStream* outStream = Session::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 currentRunLoop] forMode: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 currentRunLoop] forMode:NSDefaultRunLoopMode];
[inputStream release];
break;
}
}
(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会话传输流程:
参考:
《 使用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》