文章目录
  1. 1. Advertising & Discovering
    1. 1.1. Advertising
    2. 1.2. Creating a Session
    3. 1.3. Discovering
  2. 2. Sending & Receiving Information
    1. 2.1. Messages
    2. 2.2. Streams
    3. 2.3. Resources

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

首先必须在工程中导入 MultiPeerConnectivity.framework 框架

Advertising & Discovering

通信的第一步是让大家互相知道彼此,我们通过广播 Advertising 和发现 discovering 服务来实现。

广播作为服务器搜索附近的节点,而节点同时也去搜索附近的广播。在许多情况下,客户端同时广播并发现同一个服务,这将导致一些混乱,尤其是在client-server模式中。

所以,每一个服务都应有一个类型(标示符),它是由ASCII字母、数字和“-”组成的短文本串,最多15个字符。通常,一个服务的名字应该由应用程序的名字开始,后边跟“-”和一个独特的描述符号。

1
static NSString * const XXServiceType = @"xx-service";

一个节点有一个唯一标示MCPeerID对象,使用展示名称进行初始化,它可能是用户指定的昵称,或是单纯的设备名称。

1
MCPeerID *localPeerID = [[MCPeerID alloc] initWithDisplayName:[[UIDevice currentDevice] name]];

节点使用NSNetService或者Bonjour C API进行手动广播和发现,但这是一个特别深入的问题,关于手动节点管理可具体参见MCSession文档。

Advertising

服务的广播通过MCNearbyServiceAdvertiser来操作,初始化时带着本地节点、服务类型以及任何可与发现该服务的节点进行通信的可选信息。

发现信息使用Bonjour TXT records encoded(according to RFC 6763)发送。

1
2
3
4
5
6
MCNearbyServiceAdvertiser *advertiser =
[[MCNearbyServiceAdvertiser alloc] initWithPeer:localPeerID
discoveryInfo:nil
serviceType:XXServiceType];
advertiser.delegate = self;
[advertiser startAdvertisingPeer];

相关事件由advertiser的代理来处理,需遵从 MCNearbyServiceAdvertiserDelegate 协议。

在下例中,考虑到用户可以选择是否接受或拒绝传入连接请求,并有权以拒绝或屏蔽任何来自该节点的后续请求选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#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传递信息。

Creating a Session

在上面的例子中,我们创建了session,并在接受邀请连接时传递到节点。一个MCSession对象跟本地节点标识符、securityIdentity以及encryptionPreference参数一起进行初始化。

1
2
3
4
MCSession *session = [[MCSession alloc] initWithPeer:localPeerID
securityIdentity:nil
encryptionPreference:MCEncryptionNone];
session.delegate = self;

securityIdentity 是一个可选参数。通过X.509证书,它允许节点安全识别并连接其他节点。当设置了该参数时,第一个对象应该是识别客户端的SecIdentityRef,接着是一个或更多个用以核实本地节点身份的SecCertificateRef objects。

encryptionPreference 参数指定是否加密节点之间的通信。

MCEncryptionPreference枚举提供的三种值是:

1
2
3
MCEncryptionOptional:会话更喜欢使用加密,但会接受未加密的连接。
MCEncryptionRequired:会话需要加密。
MCEncryptionNone:会话不应该加密。

启用加密会显著降低传输速率,所以除非你的应用程序很特别,需要对用户敏感信息的处理,否则建议使用 MCEncryptionNone

MCSession代理方法中可以检测连接状态:

1
- (void)session:(MCSession*)session peer:(MCPeerID*)peerID didChangeState:(MCSessionState)state;

Discovering

客户端使用 MCNearbyServiceBrowser 来发现广播,它需要 local peer 标识符,以及非常类似MCNearbyServiceAdvertiser的服务类型来初始化:

1
2
MCNearbyServiceBrowser *browser = [[MCNearbyServiceBrowser alloc] initWithPeer:localPeerID serviceType:XXServiceType];
browser.delegate = self;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (instancetype)initWithPeer:(MCPeerID*)myPeerID serviceType:(NSString*)service // 设备名字, 设备识别码!
- (void)startBrowsingForPeers; // 开始搜索
- (void)stopBrowsingForPeers; // 停止搜索
// 在搜索到周围设备后调用此方法, 邀请相应的设备加入会话!
- (void)invitePeer:(MCPeerID*)peerID toSession:(MCSession*)session withContext:(NSData*)context timeout:(NSTimeInterval)timeout;
// 参数1:邀请的peerID 参数2:建立的session 参数3:置空(没搞太明白) 参数4:延迟时间
MCNearbyServiceBrowser的代理方法:
// 表示发现了周围的设备, 再次代理方法中处理是否发送会话邀请!
- (void)browser:(MCNearbyServiceBrowser*)browser foundPeer:(MCPeerID*)peerID withDiscoveryInfo:(NSDictionary*)info;
// 此方法用于对断开连接的设备做处理!
- (void)browser:(MCNearbyServiceBrowser*)browser lostPeer:(MCPeerID*)peerID

MCBrowserViewController—- (系统封装好的视图, 可直接显示周围已广播的设备)

1
2
3
4
5
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框架区分三种不同形式的数据传输:

  1. Messages 是定义明确的信息,比如端文本或者小序列化对象。
  2. Streams 流是可连续传输数据(如音频,视频或实时传感器事件)的信息公开渠道。
  3. Resources 是图片、电影以及文档的文件。

Messages

Messages使用 sendData:toPeers:withMode:error: 方法发送。

1
2
3
4
5
6
7
8
9
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: 收取信息。以下是如何解码先前示例代码中发送的消息:

1
2
3
4
5
6
7
8
9
10
11
#pragma mark - MCSessionDelegate
- (void)session:(MCSession *)session
didReceiveData:(NSData *)data
fromPeer:(MCPeerID *)peerID
{
NSString *message =
[[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding];
NSLog(@"%@", message);
}

另一种方法是发送 NSKeyedArchiver 编码的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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是很重要的,这样如果根对象类没有遵从,就会抛出一个异常。

Streams

Streams 使用 startStreamWithName:toPeer: 创建:

1
2
3
4
5
6
7
8
9
10
NSOutputStream *outputStream =
[session startStreamWithName:name
toPeer:peer];
stream.delegate = self;
[stream scheduleInRunLoop:[NSRunLoop mainRunLoop]
forMode:NSDefaultRunLoopMode];
[stream open];
// ...

Streams通过 MCSessionDelegate 的方法 session:didReceiveStream:withName:fromPeer: 来接收:

1
2
3
4
5
6
7
8
9
10
11
12
#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:

1
2
3
4
5
6
7
8
9
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#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);
}
}