Multipeer connectivity是一个使附近设备通过Wi-Fi网络、P2P Wi-Fi以及蓝牙个人局域网进行通信的框架。互相链接的节点可以安全地传递信息、流或是其他文件资源,而不用通过网络服务。此框架是在iOS7以后推出,旨在替代GameKit下的GKPeerPickerController通信。通过此框架我们可以直接连接同一网络下的设备,让其直接进行类似微信,qq那样的即时通讯效果。
原理
其中通讯的原理,是利用节点来进行广播服务(标示符),其他节点可以通过服务(标示符)发现广播。并对此节点进行连接。在项目中可以将广播和发现放在一起实现,这样既可以发现并连接到其他节点,同时也可以被其他节点所搜索链接。服务的命名规则为由ASCII字母、数字和“-”组成的短文本串,最多15个字符。通常,一个服务的名字应该由应用程序的名字开始,后边跟“-”和一个独特的描述符号。
相关类
针对于近场通信,在Multipeer connectivity框架中我们所需要学习的类如下:
1. MCPeerID //代表用户信息
2.MCSession //启用和管理Multipeer连接会话中的所有人之间的沟通。 通过Sesion,给别人发送和读取数据。
3.MCNearbyServiceBrowser //用于搜索附近的服务端,并可以对搜索到的服务端发出邀请加入某个会话中。
4.MCNearbyServiceAdvertiser //广播服务可以接收,并处理用户请求连接的响应。但是,这个类会有回调,告知有用户要与服务端设备连接,需要自定义提示框,以及自定义连接处理。
5.MCAdvertiserAssistant //广播服务可以接收,并处理用户请求连接的响应。没有回调,会弹出默认的提示框,并处理连接。
6. MCBrowserViewController //用于搜索附近的用户,是基于MCNearbyServiceBrowse的封装
使用步骤
MCPeerID,MCSession,MCNearbyServiceAdvertiser,MCNearbyServiceBrowser本文中用这四个类来实现(MCNearbyServiceAdvertiser,MCNearbyServiceBrowserx相对来说更原生态,此处通过这个两个类来编写代码更容易帮助我们理解其内部实现的过程)。
为了方便理解笔者此处将程序分为服务端(发送服务)和客户端(搜索服务)。无论是在服务端还是在客户端其节点信息的配置和消息池的原理都相同,下面是具体的实现过程
代码实战
通用部分
a. 配置服务标示符
1 //近场通讯标识符(相当于频段号) 2 static NSString * const ServiceType = @"nearByContent";
b.创建节点信息和消息池
在这里我们需要先通过MCPeerID创建节点信息(一般为个人设备信息)。并设置MCSession来控制其数据通信。
1 //创建用户消息和广播消息池 2 self.peerID = [[MCPeerID alloc] initWithDisplayName:[UIDevice currentDevice].name]; 3 self.session = [[MCSession alloc] initWithPeer:self.peerID]; 4 //配置消息池代理 5 self.session.delegate = self;
由于MCSession的代理方法较多,笔者会在项目端再做说明,下面来看看每个项目端的实现
服务端实现
a.创建一个服务端项目,在storyBoard中配置如下界面,并将两个BarButtonItem(广播,停止广播)和发送消息按钮分别实现点击方法
1 /** 2 * 广播方法 3 */ 4 - (IBAction)startAdvertiser:(UIBarButtonItem *)sender { 5 [self.advertiser startAdvertisingPeer]; 6 NSLog(@"开启广播"); 7 } 8 /** 9 * 停止广播方法 10 */ 11 - (IBAction)stopAdvertiser:(UIBarButtonItem *)sender { 12 [self.advertiser stopAdvertisingPeer]; 13 NSLog(@"关闭广播"); 14 //关闭时需要关闭通道 15 [self.writeStream close]; 16 [self.readStream close]; 17 //从消息循环池中移除 18 [self.writeStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; 19 [self.readStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; 20 [self.session disconnect]; 21 } 22 /** 23 * 消息发送方法 24 */ 25 - (IBAction)sendMsg:(UIButton *)sender { 26 }
b.在对应的viewController中配置属性
1 /** 近场客户 */ 2 @property (nonatomic, strong) MCNearbyServiceBrowser *browser; 3 /** 近场客户消息池 */ 4 @property (nonatomic, strong) MCSession *session; 5 /** 个人信息 */ 6 @property (nonatomic, strong) MCPeerID *peerID; 7 /** 输出流 */ 8 @property (nonatomic, strong) NSOutputStream *writeStream; 9 /** 输入流 */ 10 @property (nonatomic, strong) NSInputStream *readStream; 11 /** 存放广播端数组 */ 12 @property (nonatomic, strong) NSMutableArray *dataSource; 13 /** 链接状态文本 */ 14 @property (weak, nonatomic) IBOutlet UILabel *stateLabel; 15 /** 消息显示文本 */ 16 @property (weak, nonatomic) IBOutlet UILabel *msgLabel; 17 /** 消息编辑框 */ 18 @property (weak, nonatomic) IBOutlet UITextField *msgTextField;
c.创建对应的广播对象MCNearbyServiceAdvertiser
1 - (MCNearbyServiceAdvertiser *)advertiser { 2 if (_advertiser == nil) { 3 _advertiser = ({ 4 //其中discoveryInfo是展示给Browser端查看的信息可设为nil 5 MCNearbyServiceAdvertiser *advertiser = [[MCNearbyServiceAdvertiser alloc] initWithPeer:self.peerID discoveryInfo:nil serviceType:ServiceType]; 6 advertiser.delegate = self; 7 advertiser; 8 }); 9 } 10 return _advertiser; 11 }
当创建好广播对象中只需要在对应的地方开启广播或停止广播即可,如上a.步骤中代码所示,开启广播后若接收到其他节点的链接请求会触发广播对象的代理方法
1 /** 2 * 接收到客户端要求链接消息时调用 3 * 4 * @param advertiser 服务端广播 5 * @param peerID 客户端信息 6 * @param context 请求内容 7 * @param invitationHandler 是否接受链接回调函数 8 */ 9 - (void)advertiser:(MCNearbyServiceAdvertiser *)advertiser didReceiveInvitationFromPeer:(MCPeerID *)peerID withContext:(NSData *)context invitationHandler:(void (^)(BOOL, MCSession * _Nonnull))invitationHandler { 10 //一般服务端不会拒绝链接所以此处直接链接所有客户端 11 //同意链接并加入广播组消息池 12 invitationHandler(YES,self.session); 13 }
在同意其加入后服务端的消息池就会开始尝试和客户端的消息池建立链接。建立链接时便会回调MCSession的代理方法
1 /** 2 * 消息池连通状态改变时调用 3 * 4 * @param session 消息池 5 * @param peerID 节点信息 6 * @param state 消息池连通状态 7 */ 8 - (void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state { 9 switch (state) { 10 case MCSessionStateConnecting: 11 NSLog(@"正在链接至:%@",peerID.displayName); 12 break; 13 case MCSessionStateConnected:{ 14 NSLog(@"与%@建立链接",peerID.displayName); 15 [self.dataSource addObject:peerID]; 16 //链接成功后创建输出流 17 NSError *error; 18 self.writeStream = [self.session startStreamWithName:@"adverting" toPeer:peerID error:&error]; 19 if (error) { 20 NSLog(@"输出流创建失败"); 21 } 22 //将输出流通道打开,并加入消息循环池 23 [self.writeStream open]; 24 [self.writeStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; 25 //展示链接状态 26 dispatch_async(dispatch_get_main_queue(), ^{ 27 self.stateLabel.text = @"已连接"; 28 }); 29 } 30 break; 31 case MCSessionStateNotConnected:{ 32 NSLog(@"与%@无连接",peerID.displayName); 33 [self.dataSource removeObject:peerID]; 34 dispatch_async(dispatch_get_main_queue(), ^{ 35 self.stateLabel.text = @"未连接"; 36 }); 37 } 38 break; 39 default: 40 break; 41 } 42 }
d.当消息池状态为链接时便可开始发送消息,发送消息的方法分为3种:
1.直接发送二进制数据
1 //二进制文件传输方法 2 [self.session sendData:[self.msgTextField.text dataUsingEncoding:NSUTF8StringEncoding] toPeers:self.dataSource withMode:MCSessionSendDataReliable error:&error];
当消息池接收到消息后会回调MCSession的代理方法
1 /** 2 * 接收到二进制数据时调用 3 * 4 * @param session 信息池 5 * @param data 二进制数据 6 * @param peerID 节点信息 7 */ 8 - (void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID { 9 //获取传输数据 10 NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 11 dispatch_async(dispatch_get_main_queue(), ^{ 12 //展示数据 13 self.msgLabel.text = text; 14 }); 15 }
2.通过输入输出流来发送二进制数据
输出流的创建已在消息池链接状态回调函数中写出,此处便不再多说,当创建好输出流后,对应的链接的消息池会接受到输入流的链接,其MCSession回调函数为
1 /** 2 * 接受到数据流事件请求时调用 3 * 4 * @param session 信息池 5 * @param stream 输入数据流 6 * @param streamName 数据流名字 7 * @param peerID 节点信息 8 */ 9 - (void) session:(MCSession *)session 10 didReceiveStream:(NSInputStream *)stream 11 withName:(NSString *)streamName 12 fromPeer:(MCPeerID *)peerID { 13 //打开请求的输入流通道,加入消息循环池 14 [stream open]; 15 [stream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; 16 //设置代理,以接收数据 17 stream.delegate = self; 18 //持有该输入流 19 self.readStream = stream; 20 }
当输入输出流都链接完成并打开通道加入消息循环池后便可以开始利用输入输出流来进行数据通信,输入输出流的数据通信在前面的博客(http://www.cnblogs.com/purple-sweet-pottoes/p/4856955.html)中已有列出,此处不再赘述。
3.通过文件url地址,直接发送文件
关于文件的发送其实也和前面差不读,笔者此处直接贴出对应方法
1 //MCSession发送文件方法 2 - (void)sendResourceAtURL:(NSURL *)resourceURL 3 withName:(NSString *)resourceName 4 toPeer:(MCPeerID *)peerID 5 withCompletionHandler:(nullable void (^)(NSError * __nullable error))completionHandler 6 7 //MCSeesion收到文件时的回调方法 8 - (void) session:(MCSession *)session 9 didStartReceivingResourceWithName:(NSString *)resourceName 10 fromPeer:(MCPeerID *)peerID 11 withProgress:(NSProgress *)progress; 12 - (void) session:(MCSession *)session 13 didFinishReceivingResourceWithName:(NSString *)resourceName 14 fromPeer:(MCPeerID *)peerID 15 atURL:(NSURL *)localURL 16 withError:(nullable NSError *)error;
到这里服务端的实现已经基本完成。
客户端的实现
客户端的实现路数基本同服务端相同,唯一有所不同的是服务端创建的是广播对象,而在客户端是创建搜索服务对象
1 - (MCNearbyServiceBrowser *)browser { 2 if (_browser == nil) { 3 _browser = [[MCNearbyServiceBrowser alloc] initWithPeer:self.peerID serviceType:ServiceType]; 4 _browser.delegate = self; 5 } 6 return _browser; 7 }
对应将广播和停止广播方法改为扫描和断开服务(由于代码相同便不再注释)
1 - (IBAction)startSearchAdver:(UIBarButtonItem *)sender { 2 [self.browser startBrowsingForPeers]; 3 } 4 - (IBAction)stopConnectAdver:(UIBarButtonItem *)sender { 5 [self.browser stopBrowsingForPeers]; 6 [self.writeStream close]; 7 [self.readStream close]; 8 [self.writeStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; 9 [self.readStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; 10 [self.session disconnect]; 11 }
当扫描到对应的服务节点后,便会回调扫描对象的代理方法
1 - (void)browser:(MCNearbyServiceBrowser *)browser foundPeer:(MCPeerID *)peerID withDiscoveryInfo:(NSDictionary<NSString *,NSString *> *)info { 2 //请求链接到对应的服务节点 3 [browser invitePeer:peerID toSession:self.session withContext:nil timeout:30]; 4 NSLog(@"发现%@广播,正在链接...",peerID.displayName); 5 }
其他部分对应服务端代码来编写即可!
效果图:
1.未广播服务和扫描服务时状态
2.开启广播和开始扫描服务后状态
3.状态链接便可以开始点对点通信
4.任意端断开服务后,其消息池都会断开链接,若要重新发送消息需要重新进行链接
关于近场通信的基本使用就讲到这里,如其中有何错误之处请指出,谢谢!最后祝大家新年快乐!