iOS7新技術:如何使用Multipeer Connectivity
Multipeer connectivity是一個使附近設備通過Wi-Fi網絡、P2P Wi-Fi以及藍牙個人局域網進行通信的框架。互相鏈接的節點可以安全地傳遞信息、流或是其他文件資源,而不用通過網絡服務。
Advertising & Discovering
通信的第一步是讓大家互相知道彼此,我們通過廣播(Advertising)和發現(discovering)服務來實現。
廣播作為服務器搜索附近的節點,而節點同時也去搜索附近的廣播。在許多情況下,客戶端同時廣播并發現同一個服務,這將導致一些混亂,尤其是在client-server模式中。
所以,每一個服務都應有一個類型(標示符),它是由ASCII字母、數字和“-”組成的短文本串,最多15個字符。通常,一個服務的名字應該由應用程序的名字開始,后邊跟“-”和一個獨特的描述符號。(作者認為這和 com.apple.*標示符很像),就像下邊:
- static NSString * const XXServiceType = @"xx-service";
一個節點有一個唯一標示MCPeerID對象,使用展示名稱進行初始化,它可能是用戶指定的昵稱,或是單純的設備名稱。
- MCPeerID *localPeerID = [[MCPeerID alloc] initWithDisplayName:[[UIDevice currentDevice] name]];
節點使用NSNetService或者Bonjour C API進行手動廣播和發現,但這是一個特別深入的問題,關于手動節點管理可具體參見MCSession文檔。
Advertising
服務的廣播通過MCNearbyServiceAdvertiser來操作,初始化時帶著本地節點、服務類型以及任何可與發現該服務的節點進行通信的可選信息。
發現信息使用Bonjour TXT records encoded(according to RFC 6763)發送。
- MCNearbyServiceAdvertiser *advertiser =
- [[MCNearbyServiceAdvertiser alloc] initWithPeer:localPeerID
- discoveryInfo:nil
- serviceType:XXServiceType];
- advertiser.delegate = self;
- [advertiser startAdvertisingPeer];
相關事件由advertiser的代理來處理,需遵從MCNearbyServiceAdvertiserDelegate協議。
在下例中,考慮到用戶可以選擇是否接受或拒絕傳入連接請求,并有權以拒絕或屏蔽任何來自該節點的后續請求選項。
- #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傳遞信 息,用以避免創建和管理delegate造成的過于凌亂的業務邏輯,以避免創建和管理自定義delegate object造成的過于凌亂的業務邏輯。這種方法可以用category來實現,或者改編任何一個CocoaPods里有效的實現。
Creating a Session
在上面的例子中,我們創建了session,并在接受邀請連接時傳遞到節點。一個MCSession對象跟本地節點標識符、securityIdentity以及encryptionPreference參數一起進行初始化。
- MCSession *session = [[MCSession alloc] initWithPeer:localPeerID
- securityIdentity:nil
- encryptionPreference:MCEncryptionNone];
- session.delegate = self;
securityIdentity是一個可選參數。通過X.509證書,它允許節點安全識別并連接其他節點。當設置了該參數時,第一個對象應該 是識別客戶端的SecIdentityRef,接著是一個或更多個用以核實本地節點身份的SecCertificateRef objects。
encryptionPreference參數指定是否加密節點之間的通信。MCEncryptionPreference枚舉提供的三種值是:
MCEncryptionOptional:會話更喜歡使用加密,但會接受未加密的連接。
MCEncryptionRequired:會話需要加密。
MCEncryptionNone:會話不應該加密。
啟用加密會顯著降低傳輸速率,所以除非你的應用程序很特別,需要對用戶敏感信息的處理,否則建議使用MCEncryptionNone。
MCSessionDelegate協議將會在發送和接受信息的部分被覆蓋.
Discovering
客戶端使用MCNearbyServiceBrowser來發現廣播,它需要local peer標識符,以及非常類似MCNearbyServiceAdvertiser的服務類型來初始化:
- MCNearbyServiceBrowser *browser = [[MCNearbyServiceBrowser alloc] initWithPeer:localPeerID serviceType:XXServiceType];
- browser.delegate = self;
可能會有很多節點廣播一個特定的服務,所以為了方便用戶(或開發者),MCBrowserViewController將提供一個內置的、標準的方式來呈現鏈接到廣播節點:
- MCBrowserViewController *browserViewController =
- [[MCBrowserViewController alloc] initWithBrowser:browser
- session:session];
- browserViewController.delegate = self;
- [self presentViewController:browserViewController
- animated:YES
- completion:
- ^{
- [browser startBrowsingForPeers];
- }];
當browser完成節點連接后,它將使用它的delegate調用browserViewControllerDidFinish:,以通知展示視圖控制器--它應該更新UI以適應新連接的客戶端。
Sending & Receiving Information
一旦節點彼此相連,它們將能互傳信息。Multipeer Connectivity框架區分三種不同形式的數據傳輸:
Messages是定義明確的信息,比如端文本或者小序列化對象。
Streams 流是可連續傳輸數據(如音頻,視頻或實時傳感器事件)的信息公開渠道。
Resources是圖片、電影以及文檔的文件。
Messages
Messages使用-sendData:toPeers:withMode:error::方法發送。
- 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:收取信息。以下是如何解碼先前示例代碼中發送的消息:
- #pragma mark - MCSessionDelegate
- - (void)session:(MCSession *)session
- didReceiveData:(NSData *)data
- fromPeer:(MCPeerID *)peerID
- {
- NSString *message =
- [[NSString alloc] initWithData:data
- encoding:NSUTF8StringEncoding];
- NSLog(@"%@", message);
- }
另一種方法是發送NSKeyedArchiver編碼的對象:
- 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是很重要的,這樣如果根對象類沒有遵從<NSSecureCoding>,就會拋出一個異常。欲了解更多信息,請參閱[NSHipster article on NSSecureCoding]。
Streams
Streams 使用 -startStreamWithName:toPeer:創建:
- NSOutputStream *outputStream =
- [session startStreamWithName:name
- toPeer:peer];
- stream.delegate = self;
- [stream scheduleInRunLoop:[NSRunLoop mainRunLoop]
- forMode:NSDefaultRunLoopMode];
- [stream open];
- // ...
Streams通過MCSessionDelegate的方法session:didReceiveStream:withName:fromPeer:來接收:
- #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::
- 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:
- #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);
- }
- }
再次說明,在傳輸期間NSProgress parameter in -session:didStartReceivingResourceWithName:fromPeer:withProgress:允許接收節點來 監控文件傳輸進度。在 -session:didFinishReceivingResourceWithName:fromPeer:atURL:withError: 中,delegate的責任是從臨時localURL移動文件至永久位置。
Multipeer是突破性的API,其價值才剛剛開始被理解。雖然完整的支持功能比如AirDrop目前僅限于最新的設備,你應該會看到它將成為讓所有人盼望的功能。
本文由郭歷成[博客]翻譯自nshipster中的Multipeer Connectivity一節。
【移動開發視頻課程推薦】