前言
最近研究 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 地址
参考
iOS 近场通信(蓝牙开发, WiFi 开发)
作者: 落影 loyinglin
链接: https://www.jianshu.com/p/56e6a67cc214
来源: https://www.thinksaas.cn/group/topic/838898/