一. 为什么想到用组播
单台设备又要进行音视频数据的采集,同时又要担负对多台加入的设备进行实时推流的能力。显然在内存及并发推流能力上很快就已经进入到一个瓶颈了。
在用TCP进行推流的代码实现后,在实际的的测试过程中,在清晰度要求不下降的情况下,连接设置超过20台之后就开始出现明显的卡顿现象了。
因为推流端基于Extension(应用扩展)方式实现,在内存使用上不能超过50兆。所以不可能每个连接都开辟一份独立的发送缓存进行发送数据的缓充(在实现的前期,其实是实现了这样一套逻辑,这样可以保证每个连接上数据发送的连贯性,但在后期的上机测试后,不到几分钟就轻松越过了50兆内存的红线。可以预测到的原因是,数据发送的速度远远低于数据生成的速度,另一方面随着连接数的增加,重复缓存的数据不断增多,发送的效率不断下降)。
所以在实现上,当数据生成并封包完成后,会分别以异步的方式调起系统层发送缓存,分别向各个接收端进行数据的发送。代码实现上是相对简单的,但总有一种不靠谱的感觉。但直得庆幸的是,令人头痛的内存问题终于解决了。在代码实现后进行真机测试时,因为成功把内存压力传递到了系统层,测试得出“应用扩展”的消耗内存几乎一直处于一个非常稳定的水平,在20台设置并发推流的情况下,内存稳步在10兆以下。
但受限于当前发送端数据发送的能力及可手带宽有限,在测试过程中发现部分接收设备出现显示卡顿现象。但会议投屏对于实时播放清晰度的硬性要求。在处理数据发送的并发性问题上我陷入了思考。
解决方案一:建立一个中间推流服务,推流数据端以单一的TCP连接方式与“推流服务器”建立连接。数据接收端以订阅的方式与“推流服务器”建立多对一的连接。“推流服务器”分别对多个接收端进行推流。一方面“推流服务器”无论在内存还是数据处理能力上都比一台手机平板要好上多个数量级。另一方面网络传输能力上也是远远无法比拟的。当要人数达到一定数量的情况下,还可以对“推流服务器”进行扩展或以 CDN的方式进行分流,玩法真是多种多样。
这个方案其实也是项目一开始我就提出来的,但基于诸多原因,第一个版本先简实现。于是就有了方案二的出现。
解决方案二:用UDP + 组播的方式进行组播范围内推流。一方面解决了推流端的并发性问题,同时也不会产生内存问题。接收端只要监听指定的组播IP,打开数据接收逻辑,即可实现组播内数据的复制与共享。推流端只要单一向指定的组播IP发送数据,对于组播内接入的接收端数量,推流端不受其影响。
二. 对数据进行分片操作
上一章已经对UDP的数据格式做一简单说明,而去到发送前,必不可少的是要对数据进行分片操作,以保证每个发出去的UDP报文:
- 不会超过通讯网络的MTU值。
- 不会引起“网络层”对数据进行二次分片处理。
不触发“网络层”的二次分片更有利于后面对UDP进行丢包处理,这个也是应用层主动分片的另一个很重要的原因。当然第一个原因就是sendto函数本来对单次调用发送的数据有数量限制,详细可参考上一篇文章。
对于分包逻辑,每个人都可以有不同的实现方式,在不考虑流量控制等情况下,下面给出简单的代码以供参考:
#pragma mark- 对【发送数据】进行udp分片
+ (NSMutableArray*)toSplitPackage:(NSData*)bodyData {
if(bodyData != nil){
//数据包存储队列
NSMutableArray *packageArray = [[NSMutableArray alloc] init];
int packageID = [self identity]; //数据包id
int count = ((int)bodyData.length) / PACKAGE_MTU; //整除数量
int otherElse = bodyData.length % PACKAGE_MTU > 0? 1 : 0; //整除余数
int totalSize = (int)bodyData.length; //数据包大小
if(count == 0){
UDPFragment *fragment = [[UDPFragment alloc] init];
fragment.packageID = packageID;
fragment.packageSize = totalSize;
fragment.fragmentCount = count + otherElse; //有【余数】,整除数量+1
fragment.fragmentIndex = 0; //index 从0开始
fragment.fragmentType = 1; //分片数据类型 1: 数据, 2: fec
fragment.fragmentSize = totalSize; //少于mtu值,value = total_size
fragment.data = bodyData;
[packageArray addObject:fragment];
}else{
// 1.处理整除
for (int i = 0; i < count