Multipeer connectivity 是一个使附近设备通过WiFi网络、P2P WiFi以及蓝牙个人局域网进行通信的框架。互相链接的节点可以安全地传递信息、流或是其他文件资源,而不用通过网络服务。
首先必须在工程中导入 MultiPeerConnectivity.framework
框架
Advertising & Discovering
通信的第一步是让大家互相知道彼此,我们通过广播 Advertising
和发现 discovering
服务来实现。
广播作为服务器搜索附近的节点,而节点同时也去搜索附近的广播。在许多情况下,客户端同时广播并发现同一个服务,这将导致一些混乱,尤其是在client-server模式中。
所以,每一个服务都应有一个类型(标示符),它是由ASCII字母、数字和“-”组成的短文本串,最多15个字符。通常,一个服务的名字应该由应用程序的名字开始,后边跟“-”和一个独特的描述符号。
1
| static NSString * const XXServiceType = @"xx-service";
|
一个节点有一个唯一标示MCPeerID对象,使用展示名称进行初始化,它可能是用户指定的昵称,或是单纯的设备名称。
1
| MCPeerID *localPeerID = [[MCPeerID alloc] initWithDisplayName:[[UIDevice currentDevice] name]];
|
节点使用NSNetService或者Bonjour C API进行手动广播和发现,但这是一个特别深入的问题,关于手动节点管理可具体参见MCSession文档。
Advertising
服务的广播通过MCNearbyServiceAdvertiser来操作,初始化时带着本地节点、服务类型以及任何可与发现该服务的节点进行通信的可选信息。
发现信息使用Bonjour TXT records encoded(according to RFC 6763)发送。
1 2 3 4 5 6
| MCNearbyServiceAdvertiser *advertiser = [[MCNearbyServiceAdvertiser alloc] initWithPeer:localPeerID discoveryInfo:nil serviceType:XXServiceType]; advertiser.delegate = self; [advertiser startAdvertisingPeer];
|
相关事件由advertiser的代理来处理,需遵从 MCNearbyServiceAdvertiserDelegate 协议。
在下例中,考虑到用户可以选择是否接受或拒绝传入连接请求,并有权以拒绝或屏蔽任何来自该节点的后续请求选项。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #pragma mark - MCNearbyServiceAdvertiserDelegate - (void)advertiser:(MCNearbyServiceAdvertiser *)advertiser didReceiveInvitationFromPeer:(MCPeerID *)peerID withContext:(NSData *)context invitationHandler:(void(^)(BOOL accept, MCSession *session))invitationHandler { if ([self.mutableBlockedPeers containsObject:peerID]) { invitationHandler(NO, nil); return; } [[UIActionSheet actionSheetWithTitle:[NSString stringWithFormat:NSLocalizedString(@"Received Invitation from %@", @"Received Invitation from {Peer}"), peerID.displayName] cancelButtonTitle:NSLocalizedString(@"Reject", nil) destructiveButtonTitle:NSLocalizedString(@"Block", nil) otherButtonTitles:@[NSLocalizedString(@"Accept", nil)] block:^(UIActionSheet *actionSheet, NSInteger buttonIndex) { BOOL acceptedInvitation = (buttonIndex == [actionSheet firstOtherButtonIndex]); if (buttonIndex == [actionSheet destructiveButtonIndex]) { [self.mutableBlockedPeers addObject:peerID]; } MCSession *session = [[MCSession alloc] initWithPeer:localPeerID securityIdentity:nil encryptionPreference:MCEncryptionNone]; session.delegate = self; invitationHandler(acceptedInvitation, (acceptedInvitation ? session : nil)); }] showInView:self.view]; }
|
为了简单起见,本例中使用了一个带有block的actionsheet来作为操作框,它可以直接给invitationHandler传递信息。
Creating a Session
在上面的例子中,我们创建了session,并在接受邀请连接时传递到节点。一个MCSession对象跟本地节点标识符、securityIdentity以及encryptionPreference参数一起进行初始化。
1 2 3 4
| MCSession *session = [[MCSession alloc] initWithPeer:localPeerID securityIdentity:nil encryptionPreference:MCEncryptionNone]; session.delegate = self;
|
securityIdentity
是一个可选参数。通过X.509证书,它允许节点安全识别并连接其他节点。当设置了该参数时,第一个对象应该是识别客户端的SecIdentityRef,接着是一个或更多个用以核实本地节点身份的SecCertificateRef objects。
encryptionPreference
参数指定是否加密节点之间的通信。
MCEncryptionPreference枚举提供的三种值是:
1 2 3
| MCEncryptionOptional:会话更喜欢使用加密,但会接受未加密的连接。 MCEncryptionRequired:会话需要加密。 MCEncryptionNone:会话不应该加密。
|
启用加密会显著降低传输速率,所以除非你的应用程序很特别,需要对用户敏感信息的处理,否则建议使用 MCEncryptionNone
。
MCSession代理方法中可以检测连接状态:
1
| - (void)session:(MCSession*)session peer:(MCPeerID*)peerID didChangeState:(MCSessionState)state;
|
Discovering
客户端使用 MCNearbyServiceBrowser
来发现广播,它需要 local peer
标识符,以及非常类似MCNearbyServiceAdvertiser的服务类型来初始化:
1 2
| MCNearbyServiceBrowser *browser = [[MCNearbyServiceBrowser alloc] initWithPeer:localPeerID serviceType:XXServiceType]; browser.delegate = self;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| - (instancetype)initWithPeer:(MCPeerID*)myPeerID serviceType:(NSString*)service - (void)startBrowsingForPeers; - (void)stopBrowsingForPeers; - (void)invitePeer:(MCPeerID*)peerID toSession:(MCSession*)session withContext:(NSData*)context timeout:(NSTimeInterval)timeout; MCNearbyServiceBrowser的代理方法: - (void)browser:(MCNearbyServiceBrowser*)browser foundPeer:(MCPeerID*)peerID withDiscoveryInfo:(NSDictionary*)info; - (void)browser:(MCNearbyServiceBrowser*)browser lostPeer:(MCPeerID*)peerID
|
MCBrowserViewController
—- (系统封装好的视图, 可直接显示周围已广播的设备)
1 2 3 4 5
| MCBrowserViewController *browserViewController = [[MCBrowserViewController alloc] initWithBrowser:browser session:session]; browserViewController.delegate = self; [self presentViewController:browserViewController animated:YES completion: ^{ [browser startBrowsingForPeers]; }];
|
当browser完成节点连接后,它将使用它的delegate调用 browserViewControllerDidFinish:
,以通知展示视图控制器–它应该更新UI以适应新连接的客户端。
一旦节点彼此相连,它们将能互传信息。Multipeer Connectivity框架区分三种不同形式的数据传输:
Messages
是定义明确的信息,比如端文本或者小序列化对象。
Streams
流是可连续传输数据(如音频,视频或实时传感器事件)的信息公开渠道。
Resources
是图片、电影以及文档的文件。
Messages
Messages使用 sendData:toPeers:withMode:error:
方法发送。
1 2 3 4 5 6 7 8 9
| NSString *message = @"Hello, World!"; NSData *data = [message dataUsingEncoding:NSUTF8StringEncoding]; NSError *error = nil; if (![self.session sendData:data toPeers:peers withMode:MCSessionSendDataReliable error:&error]) { NSLog(@"[Error] %@", error); }
|
通过 MCSessionDelegate 方法 sessionDidReceiveData:fromPeer:
收取信息。以下是如何解码先前示例代码中发送的消息:
1 2 3 4 5 6 7 8 9 10 11
| #pragma mark - MCSessionDelegate - (void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID { NSString *message = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; NSLog(@"%@", message); }
|
另一种方法是发送 NSKeyedArchiver
编码的对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| id <NSSecureCoding> object = NSData *data = [NSKeyedArchiver archivedDataWithRootObject:object]; NSError *error = nil; if (![self.session sendData:data toPeers:peers withMode:MCSessionSendDataReliable error:&error]) { NSLog(@"[Error] %@", error); } #pragma mark - MCSessionDelegate - (void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID { NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data]; unarchiver.requiresSecureCoding = YES; id object = [unarchiver decodeObject]; [unarchiver finishDecoding]; NSLog(@"%@", object); }
|
为了防范对象替换攻击,设置 requiresSecureCoding
为YES是很重要的,这样如果根对象类没有遵从,就会抛出一个异常。
Streams
Streams 使用 startStreamWithName:toPeer:
创建:
1 2 3 4 5 6 7 8 9 10
| NSOutputStream *outputStream = [session startStreamWithName:name toPeer:peer]; stream.delegate = self; [stream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; [stream open];
|
Streams通过 MCSessionDelegate 的方法 session:didReceiveStream:withName:fromPeer:
来接收:
1 2 3 4 5 6 7 8 9 10 11 12
| #pragma mark - MCSessionDelegate - (void)session:(MCSession *)session didReceiveStream:(NSInputStream *)stream withName:(NSString *)streamName fromPeer:(MCPeerID *)peerID { stream.delegate = self; [stream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; [stream open]; }
|
输入和输出的streams必须安排好并打开,然后才能使用它们。一旦这样做,streams就可以被读出和写入。
Resources
Resources 发送使用 sendResourceAtURL:withName:toPeer:withCompletionHandler:
1 2 3 4 5 6 7 8 9
| NSURL *fileURL = [NSURL fileURLWithPath:@"path/to/resource"]; NSProgress *progress = [self.session sendResourceAtURL:fileURL withName:[fileURL lastPathComponent] toPeer:peer withCompletionHandler:^(NSError *error) { NSLog(@"[Error] %@", error); }];
|
返回的 NSProgress
对象可以是通过KVO(Key-Value Observed)来监视文件传输的进度,并且它提供取消传输的方法:cancel
。
接收资源实现 MCSessionDelegate 两种方法:session:didStartReceivingResourceWithName:fromPeer:withProgress:
和 session:didFinishReceivingResourceWithName:fromPeer:atURL:withError:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #pragma mark - MCSessionDelegate - (void)session:(MCSession *)session didStartReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID withProgress:(NSProgress *)progress { } - (void)session:(MCSession *)session didFinishReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID atURL:(NSURL *)localURL withError:(NSError *)error { NSURL *destinationURL = [NSURL fileURLWithPath:@"/path/to/destination"]; NSError *error = nil; if (![[NSFileManager defaultManager] moveItemAtURL:localURL toURL:destinationURL error:&error]) { NSLog(@"[Error] %@", error); } }
|