Multipeer connectivity是一個(gè)使附近設(shè)備通過(guò)Wi-Fi網(wǎng)絡(luò)查描、P2P Wi-Fi以及藍(lán)牙個(gè)人局域網(wǎng)進(jìn)行通信的框架“芈保互相鏈接的節(jié)點(diǎn)可以安全地傳遞信息冬三、流或是其他文件資源,而不用通過(guò)網(wǎng)絡(luò)服務(wù)缘缚。
概述
從上圖中可以看出Multipeer Connectivity的功能與利用AirDrop傳輸文件非常類(lèi)似勾笆,也可以將其看做是Apple對(duì)AirDrop不能直接開(kāi)發(fā)的補(bǔ)償,關(guān)于Multipeer Connectivity與AirDrop之間的對(duì)比桥滨,可參考《MultipeerConnectivity.framework梳理》
因?yàn)閕OS系統(tǒng)中用戶不能直接對(duì)文件進(jìn)行操作窝爪,所以這個(gè)框架很少會(huì)在app中使用到。這就導(dǎo)致了網(wǎng)上很少有關(guān)于介紹這個(gè)框架的博文齐媒,至于可供參考的demo那就更加少之又少了酸舍。但這并不意味著這個(gè)技術(shù)不實(shí)用,像QQ的面對(duì)面快傳(免流量)功能就是利用這個(gè)框架實(shí)現(xiàn)的里初。所以我利用這個(gè)框架實(shí)現(xiàn)了一個(gè)文件傳輸?shù)膁emo啃勉,這里分享出來(lái),供大家一起學(xué)習(xí)双妨。
實(shí)現(xiàn)功能
demo最終實(shí)現(xiàn)的效果圖如下:
實(shí)現(xiàn)功能如下:
- 可選擇相冊(cè)中的圖片淮阐、視頻進(jìn)行傳送
- 可將想傳送的文件移動(dòng)到工程中LocaFile目錄下叮阅,然后選擇本地文件就可傳送
- 可掃描附近節(jié)點(diǎn)(只做了一個(gè)節(jié)點(diǎn)連接的情況 )
- 監(jiān)控傳輸進(jìn)度
連接
要想讓兩個(gè)設(shè)備間能進(jìn)行通信,必先讓他們知道對(duì)方泣特,這個(gè)過(guò)程就稱(chēng)之為連接浩姥。在Multipeer Connectivity框架中則是使用廣播(Advertisting)和發(fā)現(xiàn)(Disconvering)模式來(lái)進(jìn)行連接:假設(shè)有兩臺(tái)設(shè)備A、B状您,B作為廣播去發(fā)送自身服務(wù)勒叠,A作為發(fā)現(xiàn)的客戶端。一旦A發(fā)現(xiàn)了B就試圖建立連接膏孟,經(jīng)過(guò)B同意二者建立連接就可以相互發(fā)送數(shù)據(jù)眯分。關(guān)于連接過(guò)程的更詳盡介紹,可參考《 iOS--MultipeerConnectivity藍(lán)牙通訊》柒桑。連接之前必須先初始化廣播(Advertisting)和發(fā)現(xiàn)(Disconvering)兩個(gè)對(duì)象弊决,才能利用他們來(lái)進(jìn)行連接。具體初始化代碼如下
發(fā)送端:
//創(chuàng)建會(huì)話
MCPeerID *peerID = [[MCPeerID alloc] initWithDisplayName:[[UIDevice currentDevice] name]];
self.session = [[MCSession alloc] initWithPeer:peerID securityIdentity:nil encryptionPreference:MCEncryptionRequired];
self.session.delegate = self;
//監(jiān)聽(tīng)廣播
self.nearbyServiceBrowser = [[MCNearbyServiceBrowser alloc] initWithPeer:peerID serviceType:@"rsp-receiver"];
self.nearbyServiceBrowser.delegate = self;
[self.nearbyServiceBrowser startBrowsingForPeers];
接收端:
//創(chuàng)建會(huì)話
MCPeerID *peerID = [[MCPeerID alloc] initWithDisplayName:[UIDevice currentDevice].name];
self.session = [[MCSession alloc] initWithPeer:peerID securityIdentity:nil encryptionPreference:MCEncryptionRequired];
self.session.delegate = self;
//廣播通知
self.nearbyServiceAdveriser = [[MCNearbyServiceAdvertiser alloc] initWithPeer:peerID discoveryInfo:nil serviceType:@"rsp-receiver"];
self.nearbyServiceAdveriser.delegate = self;
[self.nearbyServiceAdveriser startAdvertisingPeer];
這里有三個(gè)地方需要注意:
- 在初始化MCNearbyServiceAdvertiser 和MCNearbyServiceBrowser 對(duì)象時(shí)魁淳,傳入的serviceType參數(shù)飘诗,這個(gè)參數(shù)必須滿足:長(zhǎng)度在1至15個(gè)字符之間,由ASCII字母界逛、數(shù)字和“-”組成昆稿,不能以“-”為開(kāi)頭或結(jié)尾,不能包含除了“-”之外的其他特殊字符息拜,否則會(huì)報(bào)MCErrorInvalidParameter錯(cuò)誤溉潭。
- 在監(jiān)聽(tīng)廣播通知時(shí)傳入的參數(shù)serviceType必須與發(fā)送廣播時(shí)傳入的參數(shù)一致,否則無(wú)法監(jiān)聽(tīng)到廣播该溯。
- 發(fā)送端和接收端創(chuàng)建的會(huì)話對(duì)象類(lèi)型和加密方式等必須一致岛抄,否則無(wú)法收到對(duì)方的連接請(qǐng)求。
初始化完成就要處理兩端之間相互交互的邏輯了狈茉,具體代碼如下:
發(fā)送端:
// 發(fā)現(xiàn)了附近的廣播節(jié)點(diǎn)
- (void)browser:(MCNearbyServiceBrowser *)browser foundPeer:(MCPeerID *)peerID
withDiscoveryInfo:(nullable NSDictionary<NSString *, NSString *> *)info
{
//這里只考慮一個(gè)節(jié)點(diǎn)的情況:發(fā)現(xiàn)節(jié)點(diǎn)就停止搜索
[browser stopBrowsingForPeers];
self.peerID = peerID;
//發(fā)出邀請(qǐng)
[self.nearbyServiceBrowser invitePeer:self.peerID toSession:self.session withContext:nil timeout:30];
//更新UI顯示夫椭,
[self showPeer];
}
// 廣播節(jié)點(diǎn)丟失
- (void)browser:(MCNearbyServiceBrowser *)browser lostPeer:(MCPeerID *)peerID
{
//這里只考慮一個(gè)節(jié)點(diǎn)的情況
[browser startBrowsingForPeers];
self.peerID = nil;
//更新UI顯示
[self hidePeer];
}
// 搜索失敗回調(diào)
- (void)browser:(MCNearbyServiceBrowser *)browser didNotStartBrowsingForPeers:(NSError *)error
{
[browser stopBrowsingForPeers];
}
這里需要注意:發(fā)出邀請(qǐng)有時(shí)間限制,當(dāng)超出時(shí)限氯庆,接收端同意連接會(huì)報(bào)MCErrorTimedOut錯(cuò)誤蹭秋。這時(shí)如果想建立連接必須重新發(fā)出邀請(qǐng)
接收端:
// 收到節(jié)點(diǎn)邀請(qǐng)回調(diào)
- (void)advertiser:(MCNearbyServiceAdvertiser *)advertiser
didReceiveInvitationFromPeer:(MCPeerID *)peerID withContext:(nullable NSData *)context invitationHandler:(void (^)(BOOL accept, MCSession * __nullable session))invitationHandler
{
[advertiser stopAdvertisingPeer];
//交互選擇框
UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil message:[NSString stringWithFormat:@"%@請(qǐng)求與你建立連接", peerID.displayName] preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *accept = [UIAlertAction actionWithTitle:@"接受" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
invitationHandler(YES, self.session);
}];
[alert addAction:accept];
UIAlertAction *reject = [UIAlertAction actionWithTitle:@"拒絕" style:UIAlertActionStyleDestructive handler:^(UIAlertAction * _Nonnull action) {
invitationHandler(NO, self.session);
}];
[alert addAction:reject];
[self presentViewController:alert animated:YES completion:nil];
}
// 廣播失敗回調(diào)
- (void)advertiser:(MCNearbyServiceAdvertiser *)advertiser didNotStartAdvertisingPeer:(NSError *)error
{
[advertiser stopAdvertisingPeer];
}
當(dāng)收到發(fā)送端的連接請(qǐng)求時(shí),就應(yīng)該關(guān)閉廣播通知
至此堤撵,雙方通信鏈路協(xié)商成功仁讨,可以開(kāi)始基于session向?qū)Ψ桨l(fā)送數(shù)據(jù)。
數(shù)據(jù)發(fā)送
發(fā)送代碼如下
發(fā)送端:
- (void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state
{
switch (state) {
case MCSessionStateNotConnected://未連接
NSLog(@"未連接");
break;
case MCSessionStateConnecting://連接中
NSLog(@"連接中");
break;
case MCSessionStateConnected://連接完成
{
NSProgress *progress = [self.session sendResourceAtURL:[NSURL fileURLWithPath:_filePath] withName:[_filePath lastPathComponent] toPeer:[self.session.connectedPeers firstObject] withCompletionHandler:^(NSError * _Nullable error) {
if (error) {
NSLog(@"發(fā)送源數(shù)據(jù)發(fā)生錯(cuò)誤:%@", [error localizedDescription]);
}else {
__weak typeof(self) ws = self;
dispatch_async(dispatch_get_main_queue(), ^{
[ws.receiverBtn setProgressValue:0];
});
}
}];
[progress addObserver:self forKeyPath:@"completedUnitCount" options:NSKeyValueObservingOptionNew context:nil];
}
break;
}
}
session提供了三種數(shù)據(jù)傳輸方式:普通數(shù)據(jù)傳輸(data)实昨、數(shù)據(jù)流傳輸(streams)洞豁、數(shù)據(jù)源傳輸(resources),這里使用第三種,關(guān)于三種數(shù)據(jù)傳輸方式的使用及場(chǎng)景丈挟,可參考《 iOS--MultipeerConnectivity藍(lán)牙通訊》刁卜。
這里有兩個(gè)地方需要注意:
- 發(fā)送數(shù)據(jù)傳入的resourceURL參數(shù)是文件在本地的路徑,必須使用fileURLWithPath:創(chuàng)建曙咽,使用URLWithString:會(huì)報(bào)Unsupported resource type錯(cuò)誤蛔趴。
- 因?yàn)閭鬏數(shù)奈募赡苁桥R時(shí)文件,所以傳輸完成需要移除臨時(shí)文件例朱,但這里傳輸完成不能馬上移除本地文件孝情,否則接收端會(huì)在文件接收快要完成時(shí)會(huì)出現(xiàn)localURL參數(shù)為空 報(bào)錯(cuò)為:Peer no longer connected,具體原因不明洒嗤。
接收端:
// 數(shù)據(jù)源傳輸開(kāi)始
- (void)session:(MCSession *)session didStartReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID withProgress:(NSProgress *)progress
{
NSLog(@"數(shù)據(jù)傳輸開(kāi)始");
//KVO觀察
self.progress = progress;
[progress addObserver:self forKeyPath:@"completedUnitCount" options:NSKeyValueObservingOptionNew context:nil];
}
// 數(shù)據(jù)傳輸完成回調(diào)
- (void)session:(MCSession *)session didFinishReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID atURL:(NSURL *)localURL withError:(nullable NSError *)error
{
if (error) {
NSLog(@"數(shù)據(jù)傳輸結(jié)束%@----%@", localURL.absoluteString, error);
}else {
NSString *destinationPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:resourceName];
NSURL *destinationURL = [NSURL fileURLWithPath:destinationPath];
//轉(zhuǎn)移文件
NSError *error1 = nil;
if (![[NSFileManager defaultManager] moveItemAtURL:localURL toURL:destinationURL error:&error1]) {
NSLog(@"移動(dòng)文件出錯(cuò):error = %@", error1.localizedDescription);
}else {
__weak typeof(self) ws = self;
dispatch_async(dispatch_get_main_queue(), ^{
NSString *message = [NSString stringWithFormat:@"%@", resourceName];
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"文件接收成功" message:message preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *action = [UIAlertAction actionWithTitle:@"確定" style:UIAlertActionStyleDefault handler:nil];
[alert addAction:action];
[ws presentViewController:alert animated:YES completion:nil];
});
}
}
//移除監(jiān)聽(tīng)
[self.progress removeObserver:self forKeyPath:@"completedUnitCount" context:nil];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSProgress *progress = (NSProgress *)object;
NSLog(@"%lf", progress.fractionCompleted);
dispatch_async(dispatch_get_main_queue(), ^{
[self.receiverBtn setProgressValue:progress.fractionCompleted];
});
}
至此箫荡,一次文件傳輸就已完成。
結(jié)尾
這里使用的是MCNearbyServiceAdvertiser和MCNearbyServiceBrowser來(lái)進(jìn)行節(jié)點(diǎn)連接烁竭,當(dāng)然還可以使用MCAdvertiserAssistant和MCBrowserViewController來(lái)進(jìn)行節(jié)點(diǎn)連接菲茬,因?yàn)楹笳呦到y(tǒng)封裝了一套標(biāo)準(zhǔn)的UI界面吉挣,所以集成起來(lái)更加簡(jiǎn)單派撕,這里就不再贅述。
PS:因?yàn)镸UltiPeerConnectivity Framework是基于Wi-Fi和藍(lán)牙的睬魂,所以在傳輸之前必須保證Wi-Fi或藍(lán)牙至少有一個(gè)開(kāi)啟终吼。demo中沒(méi)有做這一步的檢測(cè),因?yàn)檫@個(gè)檢測(cè)很容易實(shí)現(xiàn)氯哮,所以有具體需求的可以自行添加际跪。
最后希望大家能通過(guò)這篇文章能了解到MUltiPeerConnectivity Framework的使用,Demo地址奉上喉钢。