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相对来说更原生态,此处通过这个两个类来编写代码更容易帮助我们理解其内部实现的过程)。

  1. 通过MCPeer来生成节点信息,
  2. 通过MCNearbyServiceAdvertiser来发送广播,告诉别人这里有个节点可连接,其他节点想要发现此节点必须
  3. 通过MCNearbyServiceBrowser来搜索服务(标示符)来找到发送广播的节点,并请求连接,
  4. 当连接成功后便可以通过MCSession来进行消息的发送和读取。

  为了方便理解笔者此处将程序分为服务端(发送服务)和客户端(搜索服务)。无论是在服务端还是在客户端其节点信息的配置和消息池的原理都相同,下面是具体的实现过程

  代码实战

  通用部分

  a.  配置服务标示符

 //近场通讯标识符(相当于频段号)
static NSString * const ServiceType = @"nearByContent";

  b.创建节点信息和消息池

  在这里我们需要先通过MCPeerID创建节点信息(一般为个人设备信息)。并设置MCSession来控制其数据通信。

 //创建用户消息和广播消息池
self.peerID = [[MCPeerID alloc] initWithDisplayName:[UIDevice currentDevice].name];
self.session = [[MCSession alloc] initWithPeer:self.peerID];
//配置消息池代理
self.session.delegate = self;

  由于MCSession的代理方法较多,笔者会在项目端再做说明,下面来看看每个项目端的实现

  服务端实现

  a.创建一个服务端项目,在storyBoard中配置如下界面,并将两个BarButtonItem(广播,停止广播)和发送消息按钮分别实现点击方法

    

 /**
* 广播方法
*/
- (IBAction)startAdvertiser:(UIBarButtonItem *)sender {
[self.advertiser startAdvertisingPeer];
NSLog(@"开启广播");
}
/**
* 停止广播方法
*/
- (IBAction)stopAdvertiser:(UIBarButtonItem *)sender {
[self.advertiser stopAdvertisingPeer];
NSLog(@"关闭广播");
//关闭时需要关闭通道
[self.writeStream close];
[self.readStream close];
//从消息循环池中移除
[self.writeStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[self.readStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[self.session disconnect];
}
/**
* 消息发送方法
*/
- (IBAction)sendMsg:(UIButton *)sender {
}

  b.在对应的viewController中配置属性

 /**  近场客户 */
@property (nonatomic, strong) MCNearbyServiceBrowser *browser;
/** 近场客户消息池 */
@property (nonatomic, strong) MCSession *session;
/** 个人信息 */
@property (nonatomic, strong) MCPeerID *peerID;
/** 输出流 */
@property (nonatomic, strong) NSOutputStream *writeStream;
/** 输入流 */
@property (nonatomic, strong) NSInputStream *readStream;
/** 存放广播端数组 */
@property (nonatomic, strong) NSMutableArray *dataSource;
/** 链接状态文本 */
@property (weak, nonatomic) IBOutlet UILabel *stateLabel;
/** 消息显示文本 */
@property (weak, nonatomic) IBOutlet UILabel *msgLabel;
/** 消息编辑框 */
@property (weak, nonatomic) IBOutlet UITextField *msgTextField;

  c.创建对应的广播对象MCNearbyServiceAdvertiser

 - (MCNearbyServiceAdvertiser *)advertiser {
if (_advertiser == nil) {
_advertiser = ({
//其中discoveryInfo是展示给Browser端查看的信息可设为nil
MCNearbyServiceAdvertiser *advertiser = [[MCNearbyServiceAdvertiser alloc] initWithPeer:self.peerID discoveryInfo:nil serviceType:ServiceType];
advertiser.delegate = self;
advertiser;
});
}
return _advertiser;
}

  当创建好广播对象中只需要在对应的地方开启广播或停止广播即可,如上a.步骤中代码所示,开启广播后若接收到其他节点的链接请求会触发广播对象的代理方法

 /**
* 接收到客户端要求链接消息时调用
*
* @param advertiser 服务端广播
* @param peerID 客户端信息
* @param context 请求内容
* @param invitationHandler 是否接受链接回调函数
*/
- (void)advertiser:(MCNearbyServiceAdvertiser *)advertiser didReceiveInvitationFromPeer:(MCPeerID *)peerID withContext:(NSData *)context invitationHandler:(void (^)(BOOL, MCSession * _Nonnull))invitationHandler {
//一般服务端不会拒绝链接所以此处直接链接所有客户端
//同意链接并加入广播组消息池
invitationHandler(YES,self.session);
}

  在同意其加入后服务端的消息池就会开始尝试和客户端的消息池建立链接。建立链接时便会回调MCSession的代理方法

 /**
* 消息池连通状态改变时调用
*
* @param session 消息池
* @param peerID 节点信息
* @param state 消息池连通状态
*/
- (void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state {
switch (state) {
case MCSessionStateConnecting:
NSLog(@"正在链接至:%@",peerID.displayName);
break;
case MCSessionStateConnected:{
NSLog(@"与%@建立链接",peerID.displayName);
[self.dataSource addObject:peerID];
//链接成功后创建输出流
NSError *error;
self.writeStream = [self.session startStreamWithName:@"adverting" toPeer:peerID error:&error];
if (error) {
NSLog(@"输出流创建失败");
}
//将输出流通道打开,并加入消息循环池
[self.writeStream open];
[self.writeStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
//展示链接状态
dispatch_async(dispatch_get_main_queue(), ^{
self.stateLabel.text = @"已连接";
});
}
break;
case MCSessionStateNotConnected:{
NSLog(@"与%@无连接",peerID.displayName);
[self.dataSource removeObject:peerID];
dispatch_async(dispatch_get_main_queue(), ^{
self.stateLabel.text = @"未连接";
});
}
break;
default:
break;
}
}

  d.当消息池状态为链接时便可开始发送消息,发送消息的方法分为3种:

  1.直接发送二进制数据

 //二进制文件传输方法
[self.session sendData:[self.msgTextField.text dataUsingEncoding:NSUTF8StringEncoding] toPeers:self.dataSource withMode:MCSessionSendDataReliable error:&error];

  当消息池接收到消息后会回调MCSession的代理方法

 /**
* 接收到二进制数据时调用
*
* @param session 信息池
* @param data 二进制数据
* @param peerID 节点信息
*/
- (void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID {
//获取传输数据
NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
dispatch_async(dispatch_get_main_queue(), ^{
//展示数据
self.msgLabel.text = text;
});
}

  2.通过输入输出流来发送二进制数据

  输出流的创建已在消息池链接状态回调函数中写出,此处便不再多说,当创建好输出流后,对应的链接的消息池会接受到输入流的链接,其MCSession回调函数为

 /**
* 接受到数据流事件请求时调用
*
* @param session 信息池
* @param stream 输入数据流
* @param streamName 数据流名字
* @param peerID 节点信息
*/
- (void) session:(MCSession *)session
didReceiveStream:(NSInputStream *)stream
withName:(NSString *)streamName
fromPeer:(MCPeerID *)peerID {
//打开请求的输入流通道,加入消息循环池
[stream open];
[stream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
//设置代理,以接收数据
stream.delegate = self;
//持有该输入流
self.readStream = stream;
}

  当输入输出流都链接完成并打开通道加入消息循环池后便可以开始利用输入输出流来进行数据通信,输入输出流的数据通信在前面的博客(http://www.cnblogs.com/purple-sweet-pottoes/p/4856955.html)中已有列出,此处不再赘述。

  3.通过文件url地址,直接发送文件

  关于文件的发送其实也和前面差不读,笔者此处直接贴出对应方法

 //MCSession发送文件方法
- (void)sendResourceAtURL:(NSURL *)resourceURL
withName:(NSString *)resourceName
toPeer:(MCPeerID *)peerID
withCompletionHandler:(nullable void (^)(NSError * __nullable error))completionHandler //MCSeesion收到文件时的回调方法
- (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:(nullable NSError *)error;

  到这里服务端的实现已经基本完成。

  客户端的实现

  客户端的实现路数基本同服务端相同,唯一有所不同的是服务端创建的是广播对象,而在客户端是创建搜索服务对象  

 - (MCNearbyServiceBrowser *)browser {
if (_browser == nil) {
_browser = [[MCNearbyServiceBrowser alloc] initWithPeer:self.peerID serviceType:ServiceType];
_browser.delegate = self;
}
return _browser;
}

  对应将广播和停止广播方法改为扫描和断开服务(由于代码相同便不再注释)

 - (IBAction)startSearchAdver:(UIBarButtonItem *)sender {
[self.browser startBrowsingForPeers];
}
- (IBAction)stopConnectAdver:(UIBarButtonItem *)sender {
[self.browser stopBrowsingForPeers];
[self.writeStream close];
[self.readStream close];
[self.writeStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[self.readStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[self.session disconnect];
}

  当扫描到对应的服务节点后,便会回调扫描对象的代理方法

 - (void)browser:(MCNearbyServiceBrowser *)browser foundPeer:(MCPeerID *)peerID withDiscoveryInfo:(NSDictionary<NSString *,NSString *> *)info {
//请求链接到对应的服务节点
[browser invitePeer:peerID toSession:self.session withContext:nil timeout:];
NSLog(@"发现%@广播,正在链接...",peerID.displayName);
}

  其他部分对应服务端代码来编写即可!

  效果图:

  1.未广播服务和扫描服务时状态

    

  2.开启广播和开始扫描服务后状态

     

  3.状态链接便可以开始点对点通信

      

  4.任意端断开服务后,其消息池都会断开链接,若要重新发送消息需要重新进行链接

  关于近场通信的基本使用就讲到这里,如其中有何错误之处请指出,谢谢!最后祝大家新年快乐!

  

MultipeerConnectivity框架,近场通信的基本使用的更多相关文章

  1. Android近场通信---NFC基础转)

    Android近场通信---NFC基础(一)(转) 本文介绍在Android系通过你所能执行的基本任务。它解释了如何用NDEF消息格式来发送和接收NFC数据,并且介绍了支持这些功能的Android框架 ...

  2. NFC:Arduino、Android与PhoneGap近场通信

    NFC:Arduino.Android与PhoneGap近场通信(第一本全面讲解NFC应用开发的技术著作移动智能设备近距离通信编程实战入门) [美]Tom Igoe(汤姆.伊戈),Don Colema ...

  3. nfc近场通信

    NFC简介: Near Field Communication 近场通信,是一种数据传输技术. 与wifi.蓝牙.红外线等数据传输技术的一个主要差异就是有效距离一般不能超过4cm. NFC支持3种工作 ...

  4. Android NFC近场通信1——NFC概述

    最近对NFC挺感兴趣,而且新换的手机也支持NFC功能(最近换了Find5,感觉还不错O(∩_∩)O),所以打算学学NFC编程.NFC就是我们经常说的近场通信.通常距离是4厘米或更短.NFC工作频率是1 ...

  5. Android NFC近场通信03----读写MifareClassic卡

                                           Android NFC近场通信02----读写MifareClassic卡 一.MifareClassic卡 相关 一般来 ...

  6. Android NFC近场通信02----读写卡的准备工作

                        Android NFC近场通信02----读写卡的准备工作      因为公司接了一个听上去感觉比較NB的项目.给某油田做派工系统 .并由小女子负责Androi ...

  7. 基于Antd框架的通信与交互

    基于Antd框架的通信与交互 1.与用户交互 对于input输入框,在于用户交互的过程中,用户在输入任何东西时,都会引起该组件的onChange事件(如果写有这个方法的话). <FormItem ...

  8. RPC框架motan: 通信框架netty( 1)

    服务器端编程都离不开底层的通信框架,在我们刚学习java的时候,主要接触都是Socket和ServerSocket 的阻塞编程,后来开始了解NIO,这种非阻塞的编程模式,它可以一个线程管理很多的Soc ...

  9. Android NFC近场通信2——NFC标签调度

    上面一篇文章简单介绍了NFC的背景和技术应用,今天主要是讲解一下NFC如何发起通信和标签通信(主要是翻译android官网的资料,中间加入个人心得). NFC总是在一个发起者和一个被动目标之间发生.发 ...

随机推荐

  1. 无限分页//////////////zz

    由于网页的执行都是单线程的,在JS执行的过程中,页面会呈现阻塞状态.因此,如果JS处理的数据量过大,过程复杂,可能会造成页面的卡顿.传统的数据展现都以分页的形式,但是分页的效果并不好,需要用户手动点击 ...

  2. Mysql 常用 SQL 语句集锦 转载(https://gold.xitu.io/post/584e7b298d6d81005456eb53)

    Mysql 常用 SQL 语句集锦 基础篇 //查询时间,友好提示 $sql = "select date_format(create_time, '%Y-%m-%d') as day fr ...

  3. [Asp.net]Uploadify上传大文件,Http error 404 解决方案

    引言 之前使用Uploadify做了一个上传图片并预览的功能,今天在项目中,要使用该插件上传大文件.之前弄过上传图片的demo,就使用该demo进行测试.可以查看我的这篇文章:[Asp.net]Upl ...

  4. PHPExcel--基本操作

    下面是PHPExcel的导入与导出的基本操作,也是最重要的两个操作. 生成文件: <?php require_once './Classes/PHPExcel.php'; $content = ...

  5. oracle优化:避免全表扫描(高水位线)

    如果我们查询了一条SQL语句,这条SQL语句进行了全表扫描,那到底是扫描了多少个数据块呢?是表有多少数据,就扫描多少块吗?不是的.而是扫描高水位线一下的所有块.有的时候有人经常说,我的表也不大呀,怎么 ...

  6. DataGrid新增行数据

    本文将介绍一下,如何通过Jquery MiniUI来添加Datagrid一行. 1.效果展示: ↓ 2.具体代码: <script type="text/javascript" ...

  7. 罗技 UE3100 蓝牙耳机使用

    罗技 UE3100 蓝牙耳机使用内置麦克风 蓝牙2.1蓝牙功能 和 手机 .平板 . 电脑 连接.开关 长按 5秒 开机 指示灯变成绿色并闪烁 此时耳机处于待蓝牙设备搜索状态当 智能手机 搜索 蓝牙设 ...

  8. spring3种配置的比较

    引用自:Spring 3.x 企业应用开发实战

  9. 关于Linq中的Lambda表达式中OrderBy的深入理解

    起因:就是一段Linq语句,OrderBy里面的i是什么? IQueryable<Student> slist = (from s in EFDB.Student select s). O ...

  10. CSS零基础学习笔记.

    酸菜记 之 CSS的零基础. 这篇是我自己从零基础学习CSS的笔记加理解总结归纳的,如有不对的地方,请留言指教, 学前了解: CSS中字母是不分大小写的; CSS文件可以使用在各种程序文件中(如:PH ...