基于MultipeerConnectivity Framework的文件傳輸

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ù)缘缚。

概述

多點(diǎn)連接

從上圖中可以看出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)的效果圖如下:


效果圖.jpeg

實(shí)現(xiàn)功能如下:

  1. 可選擇相冊(cè)中的圖片淮阐、視頻進(jìn)行傳送
  2. 可將想傳送的文件移動(dòng)到工程中LocaFile目錄下叮阅,然后選擇本地文件就可傳送
  3. 可掃描附近節(jié)點(diǎn)(只做了一個(gè)節(jié)點(diǎn)連接的情況 )
  4. 監(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è)地方需要注意:

  1. 在初始化MCNearbyServiceAdvertiser 和MCNearbyServiceBrowser 對(duì)象時(shí)魁淳,傳入的serviceType參數(shù)飘诗,這個(gè)參數(shù)必須滿足:長(zhǎng)度在1至15個(gè)字符之間,由ASCII字母界逛、數(shù)字和“-”組成昆稿,不能以“-”為開(kāi)頭或結(jié)尾,不能包含除了“-”之外的其他特殊字符息拜,否則會(huì)報(bào)MCErrorInvalidParameter錯(cuò)誤溉潭。
  2. 在監(jiān)聽(tīng)廣播通知時(shí)傳入的參數(shù)serviceType必須與發(fā)送廣播時(shí)傳入的參數(shù)一致,否則無(wú)法監(jiān)聽(tīng)到廣播该溯。
  3. 發(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è)地方需要注意:

  1. 發(fā)送數(shù)據(jù)傳入的resourceURL參數(shù)是文件在本地的路徑,必須使用fileURLWithPath:創(chuàng)建曙咽,使用URLWithString:會(huì)報(bào)Unsupported resource type錯(cuò)誤蛔趴。
  2. 因?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地址奉上喉钢。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末姆打,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子肠虽,更是在濱河造成了極大的恐慌幔戏,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件税课,死亡現(xiàn)場(chǎng)離奇詭異闲延,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)韩玩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)垒玲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人找颓,你說(shuō)我怎么就攤上這事合愈。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵佛析,是天一觀的道長(zhǎng)妇汗。 經(jīng)常有香客問(wèn)我,道長(zhǎng)说莫,這世上最難降的妖魔是什么杨箭? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮储狭,結(jié)果婚禮上互婿,老公的妹妹穿的比我還像新娘。我一直安慰自己辽狈,他們只是感情好慈参,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著刮萌,像睡著了一般驮配。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上着茸,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天壮锻,我揣著相機(jī)與錄音,去河邊找鬼涮阔。 笑死猜绣,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的敬特。 我是一名探鬼主播掰邢,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼伟阔!你這毒婦竟也來(lái)了辣之?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤皱炉,失蹤者是張志新(化名)和其女友劉穎怀估,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體娃承,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡奏夫,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了历筝。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片酗昼。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖梳猪,靈堂內(nèi)的尸體忽然破棺而出麻削,到底是詐尸還是另有隱情蒸痹,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布呛哟,位于F島的核電站叠荠,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏扫责。R本人自食惡果不足惜榛鼎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望鳖孤。 院中可真熱鬧者娱,春花似錦、人聲如沸苏揣。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)平匈。三九已至框沟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間增炭,已是汗流浹背忍燥。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留弟跑,地道東北人灾前。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓防症,卻偏偏與公主長(zhǎng)得像孟辑,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蔫敲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容