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(数据源传输)
在传输比较大型的文件时,我们通常使用数据源传输。
- 数据源传输的发送方式
在数据源传输中,我们可以通过KVO观察,来观察数据传输的状况。下面代码是观察数据发送者传送文件的进度。
KVO键值观察了解:http://blog.csdn.net/daiyibo123/article/details/45923575
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);
});
}