iOS开发–通过MultipeerConnectivity完成蓝牙通讯

iOS蓝牙通讯的三种方式:

  • GameKit.framework:iOS7之前的蓝牙通讯框架,从iOS7开始过期,但是目前已经被淘汰。(不做介绍)

  • MultipeerConnectivity.framework:iOS7开始引入的新的蓝牙通讯开发框架,用于取代GameKit。(详细介绍)

  • CoreBluetooth.framework:功能强大的蓝牙开发框架,要求设备必须支持蓝牙4.0。


MultipeerConnectivity实现蓝牙通讯

前面已经说了GameKit相关的蓝牙操作类从iOS7已经全部过期,苹果官方推荐使用MultipeerConnectivity代替。但是应该了解,MultipeerConnectivity.framework并不仅仅支持蓝牙连接,准确的说它是一种支持Wi-Fi网络、P2P Wi-Fi已经蓝牙个人局域网的通信框架,它屏蔽了具体的连接技术,让开发人员有统一的接口编程方法。通过MultipeerConnectivity连接的节点之间可以安全的传递信息、流或者其他文件资源而不必通过网络服务。此外使用MultipeerConnectivity进行近场通信也不再局限于同一个应用之间传输,而是可以在不同的应用之间进行数据传输(当然如果有必要的话你仍然可以选择在一个应用程序之间传输)。

要了解MultipeerConnectivity的使用必须要清楚一个概念:广播(Advertisting)和发现(Disconvering),这很类似于一种Client-Server模式。假设有两台设备A、B,B作为广播去发送自身服务,A作为发现的客户端。一旦A发现了B就试图建立连接,经过B同意二者建立连接就可以相互发送数据。在使用GameKit框架时,A和B既作为广播又作为发现,当然这种情况在MultipeerConnectivity中也很常见。

  • A.广播

无论是作为服务器端去广播还是作为客户端去发现广播服务,那么两个(或更多)不同的设备之间必须要有区分,通常情况下使用MCPeerID对象来区分一台设备,在这个设备中可以指定显示给对方查看的名称(display

name)。另外不管是哪一方,还必须建立一个会话MCSession用于发送和接受数据。通常情况下会在会话的-(void)session:(MCSession

)session peer:(MCPeerID )peerID didChangeState:(MCSessionState)state代理方法中跟踪会话状态(已连接、正在连接、未连接);在会话的-(void)session:(MCSession

)session didReceiveData:(NSData )data fromPeer:(MCPeerID *)peerID代理方法中接收数据;同时还会调用会话的-(void)sendData: toPeers:withMode: error:方法去发送数据。

广播作为一个服务器去发布自身服务,供周边设备发现连接。在MultipeerConnectivity中使用MCAdvertiserAssistant来表示一个广播,通常创建广播时指定一个会话MCSession对象将广播服务和会话关联起来。一旦调用广播的start方法周边的设备就可以发现该广播并可以连接到此服务。在MCSession的代理方法中可以随时更新连接状态,一旦建立了连接之后就可以通过MCSession的connectedPeers获得已经连接的设备。

  • B.发现

前面已经说过作为发现的客户端同样需要一个MCPeerID来标志一个客户端,同时会拥有一个MCSession来监听连接状态并发送、接受数据。除此之外,要发现广播服务,客户端就必须要随时查找服务来连接,在MultipeerConnectivity中提供了一个控制器MCBrowserViewController来展示可连接和已连接的设备(这类似于GameKit中的GKPeerPickerController),当然如果想要自己定制一个界面来展示设备连接的情况你可以选择自己开发一套UI界面。一旦通过MCBroserViewController选择一个节点去连接,那么作为广播的节点就会收到通知,询问用户是否允许连接。由于初始化MCBrowserViewController的过程已经指定了会话MCSession,所以连接过程中会随时更新会话状态,一旦建立了连接,就可以通过会话的connected属性获得已连接设备并且可以使用会话发送、接受数据。

下面用两个不同的应用程序来演示使用MultipeerConnectivity的使用过程,其中一个应用运行在模拟器中作为广播节点,另一个运行在iPhone真机上作为发现节点,并且实现两个节点的文字互传。

首先看一下作为广播节点的程序:

#import "ViewController.h"
#import <MultipeerConnectivity/MultipeerConnectivity.h> @interface ViewController ()<MCAdvertiserAssistantDelegate, MCSessionDelegate,
UINavigationControllerDelegate, UIImagePickerControllerDelegate>
@property (strong,nonatomic) MCSession *session;
@property (strong,nonatomic) MCAdvertiserAssistant *advertiserAssistant;
@end @implementation ViewController
#pragma mark - 控制器视图方法
- (void)viewDidLoad {
[super viewDidLoad];
//创建节点,displayName是用于提供给周边设备查看和区分此服务的
MCPeerID *peerID=[[MCPeerID alloc]initWithDisplayName:@"KenshinCui_Advertiser"];
_session=[[MCSession alloc]initWithPeer:peerID];
_session.delegate = self;
//创建广播
_advertiserAssistant=[[MCAdvertiserAssistant alloc]initWithServiceType:@"cmj-stream" discoveryInfo:nil session:_session];
_advertiserAssistant.delegate = self;
}
#pragma mark - UI事件
- (IBAction)advertiserClick:(UIBarButtonItem *)sender {
//开始广播
[self.advertiserAssistant start];
}
- (IBAction)selectClick:(UIBarButtonItem *)sender {
//发送文字
//发送数据给所有已连接设备
NSError *error=nil;
[self.session sendData:[@"蓝牙数据传输" dataUsingEncoding:NSUTF8StringEncoding] toPeers:[self.session connectedPeers] withMode:MCSessionSendDataUnreliable error:&error];
NSLog(@"开始发送数据...");
if (error) {
NSLog(@"发送数据过程中发生错误,错误信息:%@",error.localizedDescription);
} } #pragma mark - MCSession代理方法
-(void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state{
NSLog(@"didChangeState");
switch(state){
case MCSessionStateConnected:
NSLog(@"连接成功.");
break;
case MCSessionStateConnecting:
NSLog(@"正在连接...");
break;
default:
NSLog(@"连接失败.");
break;
}
}
@end

再看一下作为发现节点的程序:

#import "ViewController.h"
#import <MultipeerConnectivity/MultipeerConnectivity.h> @interface ViewController ()<MCSessionDelegate, MCBrowserViewControllerDelegate,
UIPopoverControllerDelegate>
@property (strong,nonatomic) MCSession *session;
@property (strong,nonatomic) MCBrowserViewController *browserController;
@property (strong, nonatomic) IBOutlet UILabel *label;
@end @implementation ViewController - (void)viewDidLoad {
[super viewDidLoad];
//创建节点
MCPeerID *peerID=[[MCPeerID alloc]initWithDisplayName:@"KenshinCui"];
//创建会话
_session=[[MCSession alloc]initWithPeer:peerID];
_session.delegate=self;
} - (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
} #pragma mark- UI事件
- (IBAction)browserClick:(UIButton *)sender {
_browserController=[[MCBrowserViewController alloc]initWithServiceType:@"cmj-stream" session:self.session];
_browserController.delegate=self; [self presentViewController:_browserController animated:YES completion:nil];
} #pragma mark - MCBrowserViewController代理方法
-(void)browserViewControllerDidFinish:(MCBrowserViewController *)browserViewController{
NSLog(@"已选择");
[self.browserController dismissViewControllerAnimated:YES completion:nil];
}
-(void)browserViewControllerWasCancelled:(MCBrowserViewController *)browserViewController{
NSLog(@"取消浏览.");
[self.browserController dismissViewControllerAnimated:YES completion:nil];
}
#pragma mark - MCSession代理方法
-(void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state{
NSLog(@"didChangeState");
switch (state) {
case MCSessionStateConnected:
NSLog(@"连接成功.");
[self.browserController dismissViewControllerAnimated:YES completion:nil];
break;
case MCSessionStateConnecting:
NSLog(@"正在连接...");
break;
default:
NSLog(@"连接失败.");
break;
}
}
//接收数据
-(void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID{
NSLog(@"开始接收数据...");
//接收文字信息
NSLog(@"%@", [NSThread currentThread]);//(<NSThread: 0x170270540>{number = 3, name = (null)}) dispatch_async(dispatch_get_main_queue(), ^{
NSString *string = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
self.label.font = [UIFont systemFontOfSize:14.0];
self.label.text = string;
});
}
@end

MultipeerConnectivity传输数据三种方式:

通过MultipeerConnectivity传输数据有三种方式分别为:

  • Messages

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

  • Streams

    Streams 使用 -startStreamWithName:toPeer:创建:

  • Resources

    Resources 发送使用 -sendResourceAtURL: withName:toPeer: withCompletionHandler:


Messages(普通数据传输)

  • 普通数据的发送方式:
    NSError *error=nil;
NSData *data = [[NSString stringWithFormat:@"--我们要显示的信息--"] dataUsingEncoding:NSUTF8StringEncoding];
[self.session sendData:data toPeers:[self.session connectedPeers] withMode:MCSessionSendDataUnreliable error:&error];
if (error) {
NSLog(@"发送数据过程中发生错误,错误信息:%@",error.localizedDescription);
}
  • 普通数据的接收方式

    所有数据的传输都在session(会话)中进行。因此,接收函数为session的回调函数。这里需要注意一下,此回调函数不是在主线程中进行的,所以我们如果要改变UI的话,需要转化到主线程。

-(void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID{
//NSLog(@"%@", [NSThread currentThread]);//(<NSThread: 0x170270540>{number = 3, name = (null)}) dispatch_async(dispatch_get_main_queue(), ^{
NSString *string = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
self.textView.text = string;
});
}

Streams(数据流传输)

数据流传输可以用在音频播放传输。

  • 数据流基本概念介绍:

什么是数据流?

流是位数据通过通信路径的连续传送序列。它是单向的,从一个应用程序的角度,流可以是输入流(读操作流)或者输出流(写操作流),除了基于文件的流之外,其余的都是non-seekable的。一旦流数据被提供或者被使用,数据就不能够从流中获取到。

-

数据流的简单分类? Cocoa包括三种与流有关的类:NSStream,NSInputStream,NSOutputStream.

NSStream是抽象类,它定义了流对象的基本接口和属性。NSInputStream和NSOutputStream是NSStream的子类,它们实现了输入流和输出流的基本操作。你可以为存储在内存中,向文件或者C

buffer写的流数据创建NSOutputStream对象;可以为从NSData对象和文件中读取的流数据创建NSInputStream对象;也可以在网络套接字的两端创建NSInputStream和NSOutputStream对象,通过流对象,你可以不用一次性将所有的流数据加载到内存中。

-

输入数据流(NSInputStream)的使用步骤?

1. 从数据源创建和初始化一个NSInputStream实例

2. 将输入流对象配置到一个run loop,open the stream

3. 通过流对象的delegate函数处理事件

4. 当所有数据读完,进行流对象的内存处理

-

输出数据流(NSOutputStream)的使用步骤?

1. 使用存储写入数据的存储库创建和初始化一个NSOutputSteam实例,并且设置它的delegate。

2. 将这个流对象布置在一个runloop上并且open the stream。

3. 处理流对象向其delegate发送的事件消息。

4. 如果流对象向内存中写入了数据,那么可以通过使用NSStreamDataWrittenToMemoryStreamKey属性获取数据。

5. 当没有数据可供写入时,清理流对象。

了解学习数据流可以参考:

http://blog.csdn.net/CrazyChickOne/article/details/37565317

  • 数据流的发送方式
    NSError *error;
self.outputStream = [self.session startStreamWithName:@"super stream" toPeer:[self.session.connectedPeers firstObject] error:&error];
self.outputStream.delegate = self;
[self.outputStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
if(error || !self.outputStream) {
NSLog(@"%@", error);
}
else{
[self.outputStream open];
}
  • 数据流的接收方式
- (void)session:(MCSession *)session didReceiveStream:(NSInputStream *)stream withName:(NSString *)streamName fromPeer:(MCPeerID *)peerID{
NSLog(@"获取流数据");
self.inputStream = stream;
self.inputStream.delegate = self;
[self.inputStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[self.inputStream open];
}
  • 数据流代理操作
/**
* 流数据操作
*
* @param aStream 流数据
* @param eventCode 流数据获取事件
*/
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode{
switch (eventCode) {
case NSStreamEventOpenCompleted:{
NSLog(@"数据流开始");
self.byteIndex = 0;
self.streamData = [[NSMutableData alloc]init];
}
break;
case NSStreamEventHasBytesAvailable:{
//有数据可用
NSInputStream *input = (NSInputStream *)aStream;
uint8_t buffer[1024];
NSInteger length = [input read:buffer maxLength:1024];
NSLog(@"%ld", length);
[self.streamData appendBytes:(const void *)buffer length:(NSUInteger)length];
// 记住这边的数据陆陆续续的
}
break;
case NSStreamEventHasSpaceAvailable:{
//有空间可以存放
NSData *data = [NSData dataWithContentsOfURL:[NSURL fileURLWithPath:[self recordPath]]];
NSOutputStream *output = (NSOutputStream *)aStream;
NSUInteger len = ((data.length - self.byteIndex >= 1024) ? 1024 : (data.length-self.byteIndex));
NSData *data1 = [data subdataWithRange:NSMakeRange(self.byteIndex, len)];
[output write:data1.bytes maxLength:len];
self.byteIndex += len;
}
break;
case NSStreamEventEndEncountered:{
//结束
[aStream close];
[aStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
if([aStream isKindOfClass:[NSInputStream class]]){
NSFileManager *fileManager = [[NSFileManager alloc]init];
[fileManager createFileAtPath:[self recordPath] contents:self.streamData attributes:nil];
}
self.byteIndex = 0;
}
break;
case NSStreamEventErrorOccurred:{
//发生错误
NSLog(@"error");
}
break;
default:
break;
}
}

Resources(数据源传输)

在传输比较大型的文件时,我们通常使用数据源传输。

    NSURL *fileURL = [NSURL fileURLWithPath:[self imagePath]];
self.imageProcess = [self.session sendResourceAtURL:fileURL withName:@"image" toPeer:[self.session.connectedPeers firstObject] withCompletionHandler:^(NSError *error) {\
if (error) {
NSLog(@"发送源数据发生错误:%@", error);
}
dispatch_async(dispatch_get_main_queue(), ^{
//数据传送完毕
[self.progressViewImage setProgress:0 animated:NO];
});
}];
//KVO观察
[self.imageProcess addObserver:self
forKeyPath:@"completedUnitCount"
options:NSKeyValueObservingOptionNew
context:nil];
  • 数据源传输的接受方式

    数据传输都是在session的代理方法中进行的。数据源传送方式有两个代理方法,分别为数据源传输刚刚开始调用session: didStartReceivingResourceWithName: fromPeer: withProgress:和数据源传输结束时调用session: didFinishReceivingResourceWithName: fromPeer: atURL: withError:

    数据源传输刚刚开始调用:一般用于设置一些初始值,比如文件接受者的进度Progress进度KVO观察。

    数据源传输结束时调用:主要用于将传输的文件从暂时存放的位置放到真正需要存放的位置。

  • 数据源传输刚刚开始调用

- (void)session:(MCSession *)session didStartReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID withProgress:(NSProgress *)progress{
NSLog(@"开始获取文件数据");
self.imageProcess = progress;
//KVO观察
[self.imageProcess addObserver:self
forKeyPath:@"completedUnitCount"
options:NSKeyValueObservingOptionNew
context:nil];
}
  • 数据源传输结束时调用
- (void)session:(MCSession *)session didFinishReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID atURL:(NSURL *)localURL withError:(NSError *)error{
NSLog(@"获取文件数据结束");
NSURL *destinationURL = [NSURL fileURLWithPath:[self imagePath]];
//判断文件是否存在,存在则删除
if ([[NSFileManager defaultManager] isDeletableFileAtPath:[self imagePath]]) {
[[NSFileManager defaultManager] removeItemAtPath:[self imagePath] error:nil];
}
//转移文件
NSError *error1 = nil;
if (![[NSFileManager defaultManager] moveItemAtURL:localURL toURL:destinationURL error:&error1]) {
NSLog(@"[Error] %@", error1);
}
}
  • KVO键值观察
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
// NSLog(@"keyPath:%@", keyPath);
// NSLog(@"object:%@", object);
// NSLog(@"change:%@", change);
// NSLog(@"context:%@", context);
dispatch_async(dispatch_get_main_queue(), ^{
int64_t numberCom = self.imageProcess.completedUnitCount;
int64_t numberTotal = self.imageProcess.totalUnitCount;
NSLog(@"%lld/%lld", numberCom, numberTotal);
});
}

参考网站:

iOS7新技术:如何使用Multipeer Connectivity

NSStream

iOS开发--通过MultipeerConnectivity完成蓝牙通讯的更多相关文章

  1. iOS开发系列--通讯录、蓝牙、内购、GameCenter、iCloud、Passbook系统服务开发汇总

    --系统应用与系统服务 iOS开发过程中有时候难免会使用iOS内置的一些应用软件和服务,例如QQ通讯录.微信电话本会使用iOS的通讯录,一些第三方软件会在应用内发送短信等.今天将和大家一起学习如何使用 ...

  2. iOS开发系列通讯录、蓝牙、内购、GameCenter、iCloud、Passbook系统服务开

    --系统应用与系统服务 iOS开发过程中有时候难免会使用iOS内置的一些应用软件和服务,例如QQ通讯录.微信电话本会使用iOS的通讯录,一些第三方软件会在应用内发送短信等.今天将和大家一起学习如何使用 ...

  3. iOS开发系列--通讯录、蓝牙、

    iOS开发过程中有时候难免会使用iOS内置的一些应用软件和服务,例如QQ通讯录.微信电话本会使用iOS的通讯录,一些第三方软件会在应用内发送短信等.今天将和大家一起学习如何使用系统应用.使用系统服务: ...

  4. iOS开发系列文章(持续更新……)

    iOS开发系列的文章,内容循序渐进,包含C语言.ObjC.iOS开发以及日后要写的游戏开发和Swift编程几部分内容.文章会持续更新,希望大家多多关注,如果文章对你有帮助请点赞支持,多谢! 为了方便大 ...

  5. iOS开发检测是否开启定位、是否允许消息推送等权限

    1.iOS开发检测是否开启定位: 需要导入: #import <CoreLocation/CoreLocation.h> 代码如下: + (void)openLocationService ...

  6. iOS开发之蓝牙通讯

    iOS开发之蓝牙通讯 一.引言 蓝牙是设备近距离通信的一种方便手段,在iPhone引入蓝牙4.0后,设备之间的通讯变得更加简单.相关的蓝牙操作由专门的CoreBluetooth.framework进行 ...

  7. iOS开发——高级技术&蓝牙服务

    蓝牙服务 蓝牙 随着蓝牙低功耗技术BLE(Bluetooth Low Energy)的发展,蓝牙技术正在一步步成熟,如今的大部分移动设备都配备有蓝牙4.0,相比之前的蓝牙技术耗电量大大降低.从iOS的 ...

  8. iOS蓝牙开发(二)蓝牙相关基础知识

    原文链接: http://liuyanwei.jumppo.com/2015/07/17/ios-BLE-1.html iOS蓝牙开发(一)蓝牙相关基础知识: 蓝牙常见名称和缩写 MFI ====== ...

  9. iOS蓝牙开发(一)蓝牙相关基础知识(转)

    转载自:http://www.cocoachina.com/ios/20150915/13454.html 原文作者:刘彦玮 蓝牙常见名称和缩写 MFI ======= make for ipad , ...

随机推荐

  1. LINUX防火墙firewall、iptables

    (1) 重启后永久性生效: 开启: systemctl enable iptables.service'.ln -s '/usr/lib/systemd/system/iptables.service ...

  2. C# 数据结构 线性表(顺序表 链表 IList 数组)

    线性表 线性表是最简单.最基本.最常用的数据结构.数据元素 1 对 1的关系,这种关系是位置关系. 特点 (1)第一个元素和最后一个元素前后是没有数据元素,线性表中剩下的元素是近邻的,前后都有元素. ...

  3. SQL脚本小笔记

    --表添加字段.说明--- --脚本 alter table 表名 ADD 字段名 FLOAT(类型) NOT NULL Default 0(默认值) EXECUTE sp_addextendedpr ...

  4. scrollView and tableView

    As we all know, tableView is the subclass of scrollView,  tableView has every properties that scroll ...

  5. Application+Handle+Task

    Application Application和Activity,Service一样,是android框架的一个系统组件,android系统会为每个程序运行时创建一个Application类的对象且仅 ...

  6. BZOJ4033 T1

    Description 有一棵点数为\(N\)的树,树边有边权.给你一个在\(0-N\)之内的正整数\(K\),你要在这棵树中选择\(K\)个点,将其染成黑色,并将其他的\(N-K\)个点染成白色.将 ...

  7. 【 UVALive - 2197】Paint the Roads(上下界费用流)

    Description In a country there are n cities connected by m one way roads. You can paint any of these ...

  8. 修改Delphi工具控件的默认字体

    修改Delphi工具控件的默认字体: 注册表: Delphi 6:    HKEY_CURRENT_USER\Software\Borland\Delphi\6.0Delphi 7:    HKEY_ ...

  9. 11.在Global的Application_Error处理错误示例

    Application_Error是在程序出问题时触发的事件. 这里面要用到错误页的情况,所以要配置web.config的customError项. 1.建立Global文件,在它的Applicati ...

  10. android 通过eclipse混淆代码 打包 + proguard 总结

    android应用程序的混淆打包 1 . 在工程文件project.properties中加入下proguard.config=proguard.cfg , 如下所示: target=android- ...