Multipeer connectivity是一个使附近设备通过Wi-Fi网络、P2P Wi-Fi以及蓝牙个人局域网进行通信的框架。互相链接的节点可以安全地传递信息、流或是其他文件资源,而不用通过网络服务。
概述
从上图中可以看出Multipeer Connectivity的功能与利用AirDrop传输文件非常类似,也可以将其看做是Apple对AirDrop不能直接开发的补偿,关于Multipeer Connectivity与AirDrop之间的对比,可参考《MultipeerConnectivity.framework梳理》
因为iOS系统中用户不能直接对文件进行操作,所以这个框架很少会在app中使用到。这就导致了网上很少有关于介绍这个框架的博文,至于可供参考的demo那就更加少之又少了。但这并不意味着这个技术不实用,像QQ的面对面快传(免流量)功能就是利用这个框架实现的。所以我利用这个框架实现了一个文件传输的demo,这里分享出来,供大家一起学习。
实现功能
demo最终实现的效果图如下:
实现功能如下:
- 可选择相册中的图片、视频进行传送
- 可将想传送的文件移动到工程中LocaFile目录下,然后选择本地文件就可传送
- 可扫描附近节点(只做了一个节点连接的情况 )
- 监控传输进度
连接
要想让两个设备间能进行通信,必先让他们知道对方,这个过程就称之为连接。在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];
这里有三个地方需要注意:
- 在初始化MCNearbyServiceAdvertiser 和MCNearbyServiceBrowser 对象时,传入的serviceType参数,这个参数必须满足:长度在1至15个字符之间,由ASCII字母、数字和“-”组成,不能以“-”为开头或结尾,不能包含除了“-”之外的其他特殊字符,否则会报MCErrorInvalidParameter错误。
- 在监听广播通知时传入的参数serviceType必须与发送广播时传入的参数一致,否则无法监听到广播。
- 发送端和接收端创建的会话对象类型和加密方式等必须一致,否则无法收到对方的连接请求。
初始化完成就要处理两端之间相互交互的逻辑了,具体代码如下:
发送端:
// 发现了附近的广播节点
- (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蓝牙通讯》。
这里有两个地方需要注意:
- 发送数据传入的resourceURL参数是文件在本地的路径,必须使用fileURLWithPath:创建,使用URLWithString:会报Unsupported resource type错误。
- 因为传输的文件可能是临时文件,所以传输完成需要移除临时文件,但这里传输完成不能马上移除本地文件,否则接收端会在文件接收快要完成时会出现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地址奉上。