前言
最近研究iOS设备间的近距离实时通信,对其解决方案进行了解,整理如下:
其中
AirDrop
常用于iOS/OS X系统间分享图片、视频等,但实时性较差;CoreBluetooth
带宽较小;GameKit
已被弃用;Socket
方案需要iOS设备在同个局域网内;ExternalAccessory
不适用iOS设备间的场景;MultipeerConnectivity
从了解的信息来看,较为符合近距离实时通信的要求,本文便介绍如何使用MultipeerConnectivity
框架。
正文
用MultipeerConnectivity
进行实时通信分为两步,一是建立二进制流通道
,二是进行协议通信
。
一、建立流通道
demo需要使用两个iOS设备(手机A和手机B),分别命名为server
(手机A)和client
(手机B)。同时为了容易学习,demo分为两个工程(server和client),实际开发应是同一份工程,通过不同的Role来区分。
建立流通道的过程如下:
1、手机A发起广播
手机A作为server,需要先发起广播。
MCPeerID
是连接中表示本设备的标识,长度不能超过63 bytes(UTF-8 编码)。
MCAdvertiserAssistant
是广播管理类,提供广播发起接口、广播代理回调。
发起广播需要先创建MCPeerID
和MCAdvertiserAssistant
。
MCPeerID *peerId = [[MCPeerID alloc] initWithDisplayName:@"server"];
self.mSession = [[MCSession alloc] initWithPeer:peerId];
self.mSession.delegate = self;
self.mAdvertiserAssistant = [[MCAdvertiserAssistant alloc] initWithServiceType:@"connect" discoveryInfo:nil session:self.mSession];
self.mAdvertiserAssistant.delegate = self;
创建完,就可以调用startServer
,发起广播。
- (void)startServer {
[self.mAdvertiserAssistant start];
}
2、手机B搜索广播
手机B作为client,需要搜索并请求建立连接。建立连接前同样需要创建MCPeerID
和MCSession
。
MCPeerID *peerId = [[MCPeerID alloc] initWithDisplayName:@"client"];
self.mSession = [[MCSession alloc] initWithPeer:peerId];
self.mSession.delegate = self;
MCBrowserViewController
是系统提供的建立连接用的VC,会自动搜索附近的广播并展示在列表中,点击之后即可请求建立连接。
- (void)startClient {
if (!self.mBrowserVC) {
self.mBrowserVC = [[MCBrowserViewController alloc] initWithServiceType:@"connect" session:self.mSession];
self.mBrowserVC.delegate = self;
}
[self presentViewController:self.mBrowserVC animated:YES completion:nil];
}
3、手机A接受连接
当手机B请求建立连接之后,手机A会弹出建立连接的请求,如下:
点击
Accept
,完成连接的建立过程。连接成功建立之后,
MCSession
会回调MCSessionStateConnected
。
- (void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state {
if (session == self.mSession) {
NSString *str;
switch (state) {
case MCSessionStateConnected:
str = @"连接成功.";
break;
case MCSessionStateConnecting:
str = @"正在连接...";
break;
default:
str = @"连接失败.";
break;
}
NSLog(@"id:%@, changeState to:%@", peerID.displayName, str);
}
}
4、手机A创建输出流
手机A作为server,主动建立输出流。
注意,需要把mOutputStream
放入RunLoop,并调用open
。
if (!self.mOutputStream) {
self.mOutputStream = [self.mSession startStreamWithName:@"delayTestServer" toPeer:[self.mSession.connectedPeers firstObject] error:nil];
self.mOutputStream.delegate = self;
[self.mOutputStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[self.mOutputStream open];
}
5、手机B接受输入流并创建输出流
手机B作为client,接受server的输出流,并且以此创建client的输出流。
这里有两个注意点:
- server的输出流,在client的表现为输入流;
- 下面的回调方法是在子线程,所以加入主线程是
[NSRunLoop mainRunLoop]
,不是[NSRunLoop currentRunLoop]
;
- (void) session:(MCSession *)session
didReceiveStream:(NSInputStream *)stream
withName:(NSString *)streamName
fromPeer:(MCPeerID *)peerID {
if (self.mSession == session) {
NSLog(@"didReceiveStream:%@, named:%@ from id:%@", [stream description], streamName, peerID.displayName);
if (self.mInputStream) {
[self.mInputStream close];
}
self.mInputStream = stream;
self.mInputStream.delegate = self;
[self.mInputStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[self.mInputStream open];
}
}
6、手机A接受输入流
手机A作为server,接受client的输出流,完成流通道的建立。
二、协议通信
在建立完二进制流通道之后,server和client便可进行通信。
通信的基础是Protocal协议,为了简化,协议全部使用Int32。
ProtocolType.h
中的简单延迟测试协议如下:
typedef NS_ENUM(int32_t, ProtocolType) {
ProtocolTypeNone = 0,
//ProtocolTypeDelay A向B发送一条消息,B立刻返回,A接受到返回的消息,计算两次消息的延迟;
ProtocolTypeDelayReq = 11,
ProtocolTypeDelayRsp = 12,
};
整个延迟测试分为三步,手机A向手机B发送一条消息,手机B收到消息之后立刻回包,手机A接收到B的消息,计算整个过程的耗时,可以得到RTT(Round-Trip Time)的大小。
1、手机A发送延迟测试协议req
手机A作为server,主动发起延迟测试。
在发送ProtocolTypeDelayReq
协议的时候,还要记录此次发送的时间mDelayStartDate
,以便计算延迟。
int32_t type = ProtocolTypeDelayReq;
self.mDelayStartDate = [NSDate dateWithTimeIntervalSinceNow:0];
[self.mOutputStream write:(uint8_t *)&type maxLength:4];
2、手机B接收延迟测试协议req,并立刻回包
手机B作为client,收到消息之后,先解析协议类型。
- (void)onInputDataReady {
ProtocolType type = 0;
[self.mInputStream read:(unsigned char *)&type maxLength:sizeof(type)];
[self handleProtocolWithType:type];
}
当收到ProtocolTypeDelayReq
协议时,返回ProtocolTypeDelayRsp
协议。
- (void)handleProtocolWithType:(ProtocolType)type {
if (type == ProtocolTypeDelayReq) {
int32_t type = ProtocolTypeDelayRsp;
[self.mOutputStream write:(uint8_t *)&type maxLength:4];
}
}
3、手机A接收回包,并计算RTT耗时
手机A收到消息,同样进行消息解析。
当收到ProtocolTypeDelayRsp
协议时,进行往返耗时计算,得到本次RTT大小。
- (void)handleProtocolWithType:(ProtocolType)type {
if (type == ProtocolTypeDelayRsp) {
NSDate *rspDate = [ NSDate dateWithTimeIntervalSinceNow:0];
NSTimeInterval delay = [rspDate timeIntervalSinceDate:self.mDelayStartDate];
self.mAverageDelayTime += delay * 1000;
++self.mDelayCount;
NSLog(@"delay test with %.2lfms, average delay time:%.2lfms", delay * 1000, self.mAverageDelayTime / self.mDelayCount);
}
}
总结
demo有两处比较有意思的地方,一是MultipeerConnectivity的建立连接过程,二是通信协议的发送和解析。
MultipeerConnectivity建立连接的过程与TCP的三次握手有异曲同工之妙,感觉就很美妙。
通信协议的发送和解析,实质上是二进制流数据的处理。实际开发过程中,会添加更多的协议头、协议尾、校验字段,还有缓冲处理、粘包处理等等有意思的内容。
先写一篇简单的文章介绍MultipeerConnectivity框架,后面再写一篇项目中接入MultipeerConnectivity
的实际应用。
demo地址