跨平台开发_RTC程序设计:实时音视频权威指南 2

1.2.1 一切皆bit

将8 bit分为一组,我们定义了字节(Byte)。

1956年6月,使用了Byte这个术语,用来表示数字信息的基本单元。

最早的字节并非8 bit。《计算机程序设计的艺术》一书中的MIX机器采用6bit作为1Byte。8 bit的Byte约定,和IBM System/360大型机的研发有关,由其领导者布鲁克斯(Frederick P.Brooks,美国,1931-2022)推行开来。

对于单字节中的位,从左到右,我们依次编号为bit7~bit0

字节是计算机世界中计量大小的基本单位,例如:1KB=1024Byte,1MB=1024KB1GB=1024 MB。这里1024=210。为了方便表示二进制,人们将其4位一组,使用0~F这16个字符进行十六进制编码

字节序

例如,0x12345678,在内存中的存储可能是0x12、0x34、0x56、0x78(内存的高位地址存的是高位)

也可能是0x78、0x56、0x34、0x12。前者被称为大端法(Big-Endian),后者被称为小端法(Little-Endian)。

比如,在计算机系统的本地内存中,Intel与ARM架构常使用小端法表示法,也叫作本机序(Host 0rder)。而在网络传输上,人们习惯于使用大端法,即网络序(Net 0rder)。又如,BMP文件使用的是小端法,而MP4文件使用的是大端法。

位序

但是我们在读取位的时候,会存在两个方向:从bit0读到bit7,或从bit7读到bit0。

例如,对于0x39=0b 00111001,从不同的方向读取,就会得到00111001或者10011100两种结果。

后面我们会看到,位序对于理解图像格式和压缩算法都是非常重要的。例如,对于PNG、GIF图像格式,位序是从bit0开始的。但是对于JPEG图像格式,位序则是从bit7开始的。
比特流与比特率

对于计算机中连续存储的字节单元,我们可以将其看作一个比特流

为了衡量信息的传递速度,我们定义比特率为每秒钟传送的比特,单位为bps(bit persecond)。比特率也称为码率。

bps中的b是小写的 。对于字节,用大写的B表示Byte,1Bps=8 bps。

在音视频通话时,若带宽小于300 kbps ,则我们称之为低带宽。

低带宽下RTC优化标准为:纯音频最低支持60kbps,音视频最低支持100kbps。
Base64表示法

十六进制通过将4 bit分为一组,得到了0x00~0x0F这16种不同的值

在早期,由于历史原因,电子邮件只允许传输英文字符数据[10]。当传输非字符数据时,网Gateway)会将字节中8位的最高位置为0。

为了能将二进制值用英文字符表示出来,人们设计了Base64编码。

Base64编码将6 bit分为一组,并用26-64个不同的字符来表示。

由于字节是8 bit一组的,当字节数不是3的倍数时,比特数不能被6整除。

Base64编码使用等号字符=在最后做附加(padding),最终可能是3种情况:没有=,1个或者2个=,表示此Base64串末尾有多少个附加字节。

1.2.2 字符管理类

最简单的Buffer类,通常结构如下:

cpp 复制代码
#pragma once
#include "DTypes.h"
typedef struct tagBuffer {
	DByte* pBuf; // 指向二进制数据的开始位置
	DUInt32 nSize; // 二进制数据的字节
};

其中,pBuf指向二进制数据的开始地址,nSize为二进制数据的字节数,如图1-6所示。在开源web服务器Nginx中,字符串就是这样定义的。

我们可以用下面的代码进行一下测试:

cpp 复制代码
#include "Buffer.h"
#include <stdio.h>
int main() {
	tagBuffer buffer;
	//分配多少个字节
	int byteSize = 128;
	buffer.pBuf = (DByte*)malloc(sizeof(DByte) * byteSize);
	buffer.nSize = byteSize;
	//使用buffer
	if (buffer.pBuf) {
		//do something with buffer.pBuf
		for (int i = 0; i < 10; i++) {
			buffer.pBuf[i] = i;
		}
		//show buffer contents
		for (int i = 0; i < 10; i++) {
			printf("%d ", buffer.pBuf[i]);
		}
		//free buffer
		free(buffer.pBuf);
		buffer.pBuf = NULL;
		buffer.nSize = 0;
	}
	return 0;
}

然而上述定义存在很多问题,比如,如何控制内存分配与释放,如何让多个线程安全地共享,如何在函数与线程间高效地传递,减少复制次数

Buffer作为传输通信中最常用的对象,我们需要为其实现更多的易用特性
极小栈消耗。
内存自动回收。
跨线程安全传递。
·写时复制(Copy-on-Write)
·十六进制表示。
·Base64转换。

简单定义

优化定义

本书提供的Buffer实现,采用内存布局。对比简单定义,新的内存布局有如下特点。

(1)栈区只使用了一个指向堆区的指针,这使得占用栈空间的代价极小

(2)栈指针指向堆区Buffer的起始地址,这使得可以随时访问Buffer的内容

(3)在堆区Buffer前面,还有两个成员:nRefCount用来维护Buffer的引用计数 ;nSize用来表示Buffer的大小

cpp 复制代码
struct DBufferData {
	DInt32 nRefCount;
	DInt32 nSize;
	DByte* pBuf;
};

typedef struct StackBuffer {
	DBufferData* pData;
};
cpp 复制代码
#include "Buffer.h"
#include <stdio.h>
#include <iostream>
using namespace std;
int main() {
	DBufferData data;
	data.pBuf = new DByte[10];
	data.nSize = 10;
	data.nRefCount = 0; // 初始为0

	StackBuffer stackbuffer;
	if (data.pBuf != NULL) {
		//我们让stackbuffer指向它
		stackbuffer.pData = &data;
		//让data的引用计数加1
		data.nRefCount++;
	}
	cout << "stackbuffer.pData->nRefCount = " << stackbuffer.pData->nRefCount << endl;
	stackbuffer.pData->nRefCount--; // 引用计数减1
	stackbuffer.pData = NULL;
	if (data.nRefCount == 0) {
		delete[] data.pBuf; // 引用计数为0,释放内存
		data.pBuf = NULL;
		data.nSize = 0;
	}
	return 0;
}

引用计数

引用计数可以有效地记录对象自身被线程持有的情况:

(1)当Buffer初始化时,其引用计数为1

(2)当需要读取访问或复制构造Buffer时,无须复制Buffer的内 容。我们仅需新增一个指向堆区的栈指针 ,并且让Buffer的引用计数加1 。这里的指针,可能来自同一个线程的栈,也可能来自不同线程的栈。

(3)当不再需要访问Buffer时,将其引用计数减1

(4)当引用计数减到0时,对象不再被任何线程持有,可安全地销毁[12]堆区Buffer。

(5)当需要修改Buffer时,根据引用计数的情况,决定是否需要写时复制

由于涉及多个线程之间的共享操作,引用计数必须用原子变量来实现。

在Common文件夹下,有一个DAtomic.h文件,提供了对C++11<atomic>的类型封装。

我们先来进行原子操作的定义。

cpp 复制代码
#pragma once
#ifndef DATOMIC_H
#define DATOMIC_H
#include <atomic>

template<class T>
class DAtomic {
public:
    // 构造函数初始化原子变量
    DAtomic(const T& initialValue = T()) : value(initialValue) {}

    // 原子加载
    T load() const {
        return value.load();
    }

    // 原子存储
    void store(T newValue) {
        value.store(newValue);
    }

    // 原子增加
    T fetch_add(T addValue) {
        return value.fetch_add(addValue);
    }

    // 原子减少
    T fetch_sub(T subValue) {
        return value.fetch_sub(subValue);
    }

private:
    std::atomic<T> value;
};

#endif // DATOMIC_H

测试atomic

cpp 复制代码
#include <iostream>
#include "DAtomic.h"
using namespace std;

int main() {
	DAtomic<int> counter;
	int currentValue = counter.load();
	cout << "Initial value: " << currentValue << endl;
	counter.store(10);
	cout << "New value: " << counter.load() << endl;
	int newValue = counter.fetch_add(5);
	cout << "New value after fetch_add(5): " << newValue << endl;
	newValue = counter.fetch_sub(3);
	cout << "New value after fetch_sub(3): " << newValue << endl;
	cout << "Final value: " << counter.load() << endl;
	return 0;
}

写时复制

当引用计数为1时,我们可以正常地随意读写Buffer内的所有数据。

但当引用计数大于1时,如要修改Buffer的内容,我们就需要对其数据进行复制。这样各个线程仍可以像自己独占Buffer一样,访问各自的Buffer数据,使读写互不影响。

写时复制(Copy-On-Write,简称COW)是一种优化技术,用于减少内存使用和提高性能。其核心思想是,当多个用户共享同一份数据时 ,只要这些用户只读取而不修改数据,那么它们可以共享同一份物理副本。只有当某个用户尝试修改数据时,才会创建数据的独立副本供该用户使用

  1. 初始状态

    • 当创建一个新的Buffer对象时,它的引用计数为1,表示只有一个所有者。
    • 当通过复制构造函数或赋值操作符创建新的Buffer对象时,引用计数会增加,因为多个对象共享同一份数据。
  2. 读取操作

    • 读取操作不会改变数据,因此不需要创建新的副本。所有共享同一份数据的对象都可以安全地读取数据
  3. 写入操作

    • 当某个Buffer对象尝试修改数据时,首先检查引用计数。
    • 如果引用计数大于1,说明有其他对象也在共享这份数据。此时,需要创建一个新的副本,以便修改操作不会影响其他共享者。
    • 创建新的副本后,修改新的副本,然后更新当前对象的内部状态,使其指向新的副本。
cpp 复制代码
//buffer 原子操作
class DBufferAtomic {
public:
	DBufferAtomic(size_t size) : buffer(size) , m_nRefCount(1) {}
	DBufferAtomic(DBufferAtomic& other) : buffer(other.buffer){
		other.m_nRefCount.fetch_add(1);
		m_nRefCount.store(other.m_nRefCount.load());
	}
	DBufferAtomic& operator=(DBufferAtomic& other) {
		if (this!= &other) {
			other.m_nRefCount.fetch_add(1);
			m_nRefCount.store(other.m_nRefCount.load());
			buffer = other.buffer;
		}
		return *this;
	}
	
	const std::vector<char>& getData() const {
		return buffer;
	}
	void modifyData(size_t index, char value) {
		if (m_nRefCount.load() > 1) {
			//复制一份数据
			std::vector<char> temp(buffer);
			temp[index] = value;
			buffer = temp;
			(*this).buffer = std::move(temp);
		}
		else {
			buffer[index] = value;
		}
	}
private:
	DAtomic<int> m_nRefCount;
	std::vector<char> buffer;
};
相关推荐
nibabaoo2 天前
前端开发攻略---H5页面手机获取摄像头权限回显出画面并且同步到PC页面
javascript·websocket·实时音视频·实时同步·录制
拉拉尼亚4 天前
WebRTC 完全指南:原理、教程与应用场景
前端·typescript·实时音视频
王家视频教程图书馆7 天前
另外还有一个小问题,就是两个部手机进行测试的时候,进行外部 RTC 通话的时候。我使用 QQ 音乐播放背景音乐,但是背景音乐没有加上。有冲突
实时音视频
海水冷却11 天前
2026年实时音视频服务计费模式指南
实时音视频
番茄灭世神13 天前
PN学堂GD32教程第8篇——RTC
实时音视频
runner365.git13 天前
RTC实现VoiceAgent(二)
大模型·webrtc·实时音视频·voiceagent
xuxie9913 天前
N18 RTC
单片机·嵌入式硬件·实时音视频
runner365.git14 天前
RTC会议实时翻译系统
实时音视频
runner365.git14 天前
如何使用RTCPilot配置一个集群RTC服务
webrtc·实时音视频·音视频开发
深念Y15 天前
从WebSocket到WebRTC,豆包级实时语音交互背后的技术演进
websocket·网络协议·实时互动·webrtc·语音识别·实时音视频