iOS近距离实时通信解决方案

前言

最近研究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是广播管理类,提供广播发起接口、广播代理回调。
发起广播需要先创建MCPeerIDMCAdvertiserAssistant

    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,需要搜索并请求建立连接。建立连接前同样需要创建MCPeerIDMCSession

    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开发)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,637评论 18 139
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,934评论 6 13
  • 计算机网络学习的核心内容就是网络协议的学习。网络协议是为计算机网络中进行数据交换而建立的规则、标准或者说是约定的集...
    杵臼凉薄yo阅读 32,236评论 5 51
  • 前言: WebRTC,名称源自网页实时通信(Web Real-Time Communication)的缩写,简而言...
    涂耀辉阅读 50,675评论 134 430
  • “请问你有看到我的猫吗?他叫汤姆,英国蓝猫。” “汤姆?是动画片《汤姆和杰瑞》里面的汤姆吗?” “是,是,是,就是...
    墙角废纸团阅读 971评论 1 2