基于MultipeerConnectivity Framework的文件传输

Multipeer connectivity是一个使附近设备通过Wi-Fi网络、P2P Wi-Fi以及蓝牙个人局域网进行通信的框架。互相链接的节点可以安全地传递信息、流或是其他文件资源,而不用通过网络服务。

概述

多点连接

从上图中可以看出Multipeer Connectivity的功能与利用AirDrop传输文件非常类似,也可以将其看做是Apple对AirDrop不能直接开发的补偿,关于Multipeer Connectivity与AirDrop之间的对比,可参考《MultipeerConnectivity.framework梳理》
因为iOS系统中用户不能直接对文件进行操作,所以这个框架很少会在app中使用到。这就导致了网上很少有关于介绍这个框架的博文,至于可供参考的demo那就更加少之又少了。但这并不意味着这个技术不实用,像QQ的面对面快传(免流量)功能就是利用这个框架实现的。所以我利用这个框架实现了一个文件传输的demo,这里分享出来,供大家一起学习。

实现功能

demo最终实现的效果图如下:


效果图.jpeg

实现功能如下:

  1. 可选择相册中的图片、视频进行传送
  2. 可将想传送的文件移动到工程中LocaFile目录下,然后选择本地文件就可传送
  3. 可扫描附近节点(只做了一个节点连接的情况 )
  4. 监控传输进度

连接

要想让两个设备间能进行通信,必先让他们知道对方,这个过程就称之为连接。在Multipeer Connectivity框架中则是使用广播(Advertisting)和发现(Disconvering)模式来进行连接:假设有两台设备A、B,B作为广播去发送自身服务,A作为发现的客户端。一旦A发现了B就试图建立连接,经过B同意二者建立连接就可以相互发送数据。关于连接过程的更详尽介绍,可参考《 iOS--MultipeerConnectivity蓝牙通讯》。连接之前必须先初始化广播(Advertisting)和发现(Disconvering)两个对象,才能利用他们来进行连接。具体初始化代码如下
发送端:

   //创建会话
    MCPeerID *peerID = [[MCPeerID alloc] initWithDisplayName:[[UIDevice currentDevice] name]];
    self.session = [[MCSession alloc] initWithPeer:peerID securityIdentity:nil encryptionPreference:MCEncryptionRequired];
    self.session.delegate = self;
    
    //监听广播
    self.nearbyServiceBrowser = [[MCNearbyServiceBrowser alloc] initWithPeer:peerID serviceType:@"rsp-receiver"];
    self.nearbyServiceBrowser.delegate = self;
    [self.nearbyServiceBrowser startBrowsingForPeers];

接收端:

    //创建会话
    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];

这里有三个地方需要注意:

  1. 在初始化MCNearbyServiceAdvertiser 和MCNearbyServiceBrowser 对象时,传入的serviceType参数,这个参数必须满足:长度在1至15个字符之间,由ASCII字母、数字和“-”组成,不能以“-”为开头或结尾,不能包含除了“-”之外的其他特殊字符,否则会报MCErrorInvalidParameter错误。
  2. 在监听广播通知时传入的参数serviceType必须与发送广播时传入的参数一致,否则无法监听到广播。
  3. 发送端和接收端创建的会话对象类型和加密方式等必须一致,否则无法收到对方的连接请求。

初始化完成就要处理两端之间相互交互的逻辑了,具体代码如下:
发送端:

// 发现了附近的广播节点
- (void)browser:(MCNearbyServiceBrowser *)browser foundPeer:(MCPeerID *)peerID
withDiscoveryInfo:(nullable NSDictionary<NSString *, NSString *> *)info
{
    //这里只考虑一个节点的情况:发现节点就停止搜索
    [browser stopBrowsingForPeers];
    self.peerID = peerID;
    //发出邀请
    [self.nearbyServiceBrowser invitePeer:self.peerID toSession:self.session withContext:nil timeout:30];
    //更新UI显示,
    [self showPeer];
}

// 广播节点丢失
- (void)browser:(MCNearbyServiceBrowser *)browser lostPeer:(MCPeerID *)peerID
{
    //这里只考虑一个节点的情况
    [browser startBrowsingForPeers];
    self.peerID = nil;
    //更新UI显示
    [self hidePeer];
}

// 搜索失败回调
- (void)browser:(MCNearbyServiceBrowser *)browser didNotStartBrowsingForPeers:(NSError *)error
{
    [browser stopBrowsingForPeers];
}

这里需要注意:发出邀请有时间限制,当超出时限,接收端同意连接会报MCErrorTimedOut错误。这时如果想建立连接必须重新发出邀请

接收端:

// 收到节点邀请回调
- (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:@"%@请求与你建立连接", 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];
}

// 广播失败回调
- (void)advertiser:(MCNearbyServiceAdvertiser *)advertiser didNotStartAdvertisingPeer:(NSError *)error
{
   [advertiser stopAdvertisingPeer];
}

当收到发送端的连接请求时,就应该关闭广播通知
至此,双方通信链路协商成功,可以开始基于session向对方发送数据。

数据发送

发送代码如下
发送端:

- (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(@"发送源数据发生错误:%@", [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提供了三种数据传输方式:普通数据传输(data)、数据流传输(streams)、数据源传输(resources),这里使用第三种,关于三种数据传输方式的使用及场景,可参考《 iOS--MultipeerConnectivity蓝牙通讯》
这里有两个地方需要注意:

  1. 发送数据传入的resourceURL参数是文件在本地的路径,必须使用fileURLWithPath:创建,使用URLWithString:会报Unsupported resource type错误。
  2. 因为传输的文件可能是临时文件,所以传输完成需要移除临时文件,但这里传输完成不能马上移除本地文件,否则接收端会在文件接收快要完成时会出现localURL参数为空 报错为:Peer no longer connected,具体原因不明。

接收端:

// 数据源传输开始
- (void)session:(MCSession *)session didStartReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID withProgress:(NSProgress *)progress
{
    NSLog(@"数据传输开始");
    //KVO观察
    self.progress = progress;
    [progress addObserver:self forKeyPath:@"completedUnitCount" options:NSKeyValueObservingOptionNew context:nil];
}

// 数据传输完成回调
- (void)session:(MCSession *)session didFinishReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID atURL:(NSURL *)localURL withError:(nullable NSError *)error
{
    if (error) {
        NSLog(@"数据传输结束%@----%@", localURL.absoluteString, error);
    }else {
        NSString *destinationPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:resourceName];
        NSURL *destinationURL = [NSURL fileURLWithPath:destinationPath];
        //转移文件
        NSError *error1 = nil;
        if (![[NSFileManager defaultManager] moveItemAtURL:localURL toURL:destinationURL  error:&error1]) {
            NSLog(@"移动文件出错: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];
            });
        }
    }
    
    //移除监听
    [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];
    });
}

至此,一次文件传输就已完成。

结尾

这里使用的是MCNearbyServiceAdvertiser和MCNearbyServiceBrowser来进行节点连接,当然还可以使用MCAdvertiserAssistant和MCBrowserViewController来进行节点连接,因为后者系统封装了一套标准的UI界面,所以集成起来更加简单,这里就不再赘述。
PS:因为MUltiPeerConnectivity Framework是基于Wi-Fi和蓝牙的,所以在传输之前必须保证Wi-Fi或蓝牙至少有一个开启。demo中没有做这一步的检测,因为这个检测很容易实现,所以有具体需求的可以自行添加。
最后希望大家能通过这篇文章能了解到MUltiPeerConnectivity Framework的使用,Demo地址奉上。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容