lss/
|-- bin/
|-- config/ 存放配置文件
|-- config.json 配置文件
|-- log/ 手动创建存放log的目录
|-- build/ 存放编译产生的中间文件
|-- third_party/ 存放第三方库
|-- jsoncpp/
|-- src/
|-- base/ 存放基础函数库,该目录下的源文件将被编译成一个`libbase.a`的库
|-- CMakeLists.txt
|-- TTime.h
|-- TTime.cpp 时间常用函数,测量事件的时长以及当前时间,通过一个返回当前系统的UTC时间进行计算
|-- StringUtils.h
|-- StringUtils.cpp 字符串操作函数,实现字符串前缀和后缀匹配,文件名、文件路径的操作,字符串的分割
|-- NonCopyable.h 不可移动和复制的`基类`
|-- Singleton.h
|-- TestSingleton.cpp 测试单例模式
|-- Task.h
|-- Task.cpp 定时任务方法的实现
|-- TaskManager.h
|-- TaskManager.cpp 定时任务管理器方法的实现
|-- TestTask.cpp 测试定时任务方法
|-- Logger.h
|-- Logger.cpp 日志输出实现
|-- LogStream.h
|-- LogStream.cpp 构建和发送日志消息实现
|-- TestLog.cpp 测试日志
|-- FileLog.h
|-- FileLog.cpp 日志文件,将日志写入文件并进行切分
|-- FileManager.h
|-- FileManager.cpp 日志文件管理
|-- Config.h
|-- Config.cpp Log的配置文件
|-- AppInfo.h
|-- AppInfo.cpp 业务配置实现
|-- DomainInfo.h
|-- DomainInfo.cpp 域名相关配置
|-- network/ 高性能网络库的实现
|-- base/ 基础功能
|-- Network.h 定义日志信息
|-- InetAddress.h
|-- InetAddress.cpp 网络地址相关操作
|-- SocketOpt.h
|-- SocketOpt.cpp Socket相关操作
|-- MsgBuffer.h
|-- MsgBuffer.cpp 消息缓冲区相关操作
|-- net/ 网络协议
|-- tests/ 测试目录
|-- CMakeLists.txt 编译可执行文件并设置路径
|-- EventLoopThreadTest.cpp 测试事件循环线程的功能
|-- InetAddressTest.cpp 测试网络地址相关的操作
|-- SocketOptTest.cpp 测试Socket通信
|-- AcceptorTest.cpp 测试Acceptor
|-- TcpConnectionTest.cpp 测试TcpConnection
|-- TcpServerTest.cpp 测试TcpServer
|-- DnsServiceTest.cpp 测试DnsService
|-- TcpClientTest.cpp 测试TcpClient
|-- UdpClientTest.cpp 测试UdpClient
|-- UdpServerTest.cpp 测试UdpServer
|-- EventLoop.h
|-- EventLoop.cpp 实现事件循环的逻辑,用于处理网络事件
|-- Event.h
|-- Event.cpp 处理与事件循环相关的事件
|-- EventLoopThread.h
|-- EventLoopThread.cpp 创建一个在单独线程中运行的事件循环
|-- PipeEvent.h
|-- PipeEvent.cpp 处理与管道有关的事件
|-- EventLoopThreadPool.h
|-- EventLoopThreadPool.cpp 事件循环线程池处理事件
|-- TimingWheel.h
|-- TimingWheel.cpp 以时间轮的方式设置定时任务
|-- Acceptor.h
|-- Acceptor.cpp 接收连接相关的操作
|-- Connection.h
|-- Connection.cpp 网络连接相关的操作
|-- TcpConnection.h
|-- TcpConnection.cpp TCP相关操作
|-- UdpSocket.h
|-- UdpSocket.h UDP Socket相关操作
|-- CMakeLists.txt 指定编译的文件目录
|-- TcpServer.h
|-- TcpServer.cpp TCP Server相关操作
|-- DnsService.h
|-- DnsService.cpp DNS Service相关的操作
|-- TcpClient.h
|-- TcpClient.cpp TCP Client相关操作
|-- UdpClient.h
|-- UdpClient.cpp UDP Client相关操作
|-- UdpServer.h
|-- UdpServer.cpp UDP Server相关操作
|-- TestContext.h
|-- TestContext.cpp Context上下文相关操作
|-- mmedia/ 多媒体模块
|-- base/ 基础操作
|-- Packet.h
|-- Packet.cpp 多媒体数据包相关操作
|-- MMediaHandler.h 抽象基类,只定义纯虚函数
|-- MMediaLog.h 输出日志信息
|-- BytesReader.h
|-- BytesReader.cpp 从网络数据中读整型值
|-- BytesWriter.h
|-- BytesWriter.cpp 写一个整型值到发送缓存中
|-- rtmp/
|-- amf/
|-- AMFAny.h
|-- AMFAny.cpp 基类,定义解析接口,提供获取数据的接口
|-- AMFNumber.h
|-- AMFNumber.cpp 实现Number数据类型的解析
|-- AMFBoolean.h
|-- AMFBoolean.cpp 实现Boolean数据类型的解析
|-- AMFString.h
|-- AMFString.cpp 实现String数据类型的解析
|-- AMFLongString.h
|-- AMFLongString.cpp 实现LongString数据类型的解析
|-- AMFDate.h
|-- AMFDate.cpp 实现Date数据类型的解析
|-- AMFObject.h
|-- AMFObject.cpp 实现Object所有属性数据的解析
|-- AMFNull.h
|-- AMFNull.cpp 实现Null所有属性数据的解析
|-- RtmpHandShake.h
|-- RtmpHandShake.cpp rtmp握手
|-- RtmpHandler.h 实现rtmp的特殊接口
|-- RtmpServer.h
|-- RtmpServer.cpp rtmp server的相关协议及数据传递
|-- RtmpHeader.h rtmp协议头设计
|-- RtmpContext.h
|-- RtmpContext.cpp rtmp数据包的接收,发送,控制消息和用户消息
|-- RtmpClient.h
|-- RtmpClient.cpp rtmp client的相关协议及数据传递
|-- tests/
|-- HandShakeClientTest.cpp 客户端握手测试
|-- HandShakeServerTest.cpp 服务端握手测试
|-- RtmpServerTest.cpp rtmp server测试
|-- RtmpClientTest.cpp rtmp client测试
|-- CMakeLists.txt
|-- live/ 直播业务模块
|-- base/ 基础工具
|-- TimeCorrector.h
|-- TimeCorrector.cpp 时间戳修正操作
|-- CodeUtils.h
|-- CodeUtils.cpp 解码工具类
|-- LiveLog.h 输出日志头
|-- CodecHeader.h
|-- CodecHeader.cpp 处理音视频编解码相关的头信息
|-- GopMgr.h
|-- GopMgr.cpp GOP操作
|-- User.h
|-- User.cpp 用户类设计与实现
|-- PlayerUser.h
|-- PlayerUser.cpp 播放用户的相关操作
|-- Stream.h
|-- Stream.cpp 流管理操作
|-- RtmpPlayerUser.h
|-- RtmpPlayerUesr.cpp RTMP播放用户设计与实现
|-- Session.h
|-- Session.cpp 会话管理
|-- LiveService.h
|-- LiveService.cpp 直播业务管理
|-- tests/
|-- CodecHeaderTest.cpp 测试音视频编解码相关的头信息
|-- CMakeLists.txt
|-- CMakeLists.txt
|-- main/
|-- CMakeLists.txt 指定编译所需的依赖文件
|-- main.cpp 模块测试
|-- CMakeLists.txt 指定编译的源文件目录,生成程序名,编译完成后执行安装
|-- CMakeLists.txt 设置全局编译参数,程序输出目录,编译的子目录
|-- README.md实现单例模式,保证一个类仅有一个实例(只能自行创建实例;多线程初始化竞争,保证只创建一个实例;不可复制和移动),并提供一个访问实例的全局访问点。
C++的三/五法则:拷贝构造函数、拷贝赋值运算符、析构函数、移动构造函数、移动赋值运算符。实现了上述5个法则中的任意一个,编译器会默认合成另外的4个法则,反之删除其中任意一个法则,编译器则不会默认合成另外的4个法则。
定时任务:在规定的时间执行;可以单次执行,也可以重复执行;通过回调函数执行任务。
定时任务的运行(由谁检测任务时间?由谁执行定时任务?)--> 定时任务管理器:存放所有的定时任务;添加定时任务;删除定时任务;检测每个任务是否到点执行;执行任务;定时任务为全局唯一。
定时任务管理器的算法:
- 直接遍历(容器:unordered_set;插入:O(1);遍历:O(n));
- 实现简单;
- 任务事件重新更新后不需要调整任务列表;
- 全局定时任务数量少,局限于后续网络库中与事件循环绑定的局部定时任务;
- 最小时间堆:遍历任务,总是取堆顶任务进行判断,如果堆顶任务未到时间,退出遍历,否则继续执行任务,并设置下一个时间点,随后重建堆,保证堆顶时间为最小的时间(容器:vector、std::make_heap、std::push_heap、std::pop_heap;插入:O(logn);遍历:不稳定任务未到时间,直接退出,O(1),如果所有任务到了时间,需要继续执行任务,并设置下一个时间点,随后重建堆,O(nlogn));
- 时间轮:以固定的时间间隔执行当前指向的任务,最右边的时间轮转动一圈后,左边的时间轮转动一格,以此类推(容器:vector、queue、unordered_set;插入:O(1);遍历:O(1))。
日志的用途:
- 查问题:程序逻辑、业务逻辑;
- 输出业务信息: 负载、带宽、 access log。
日志库的设计:
- Logger:日志输出
- 日志级别:trace、debug、info、warning、error;
- 日志文件:负责具体的日志输出。
- LogStream: 日志格式
- 时间格式:YYYY-MM-ddTHH:mm:ss;
- 打印级别:TRACE、DEBUG、INFO、WARNING、ERROR。
- 文件位置:FILE, LINE;
- 函数名:func;
- 完整格式:2024-7-10T15:49:00 7821 DEBUG [RtmpContext.cpp:621][Parse] xxxx。
- FileLog: 日志文件
- 单个日志不上锁,使用append以原子锁的方式进行写入;
- 负责把日志信息写道文件;
- 负责完成文件切分。
- FileManager: 日志文件管理
- 日志文件申请与释放;
- 日志文件的切分检测。
配置文件:
- 说明:
- 系统用到的一些参数:线程数量、打印级别、服务器IP和端口号;
- 业务用到的一些参数:HLS支持、RTMP支持、FLV支持、帧队列长度、录制、时移等。
- 多个配置文件
- 管理:
- 启动加载;
- 热更新;
- 配置文件管理:本地配置、主动更新、被动更新;
- 文件格式:(INI、XML、
JSON、YAML、TOML等);- JSON库配置:
- 新建
/bin/config/目录并创建config.json;
- 在
lss/根目录下新建third_party/并下载jsoncpp/;
- 修改根目录下的
CMakeLists.txt,添加jsoncpp的编译;
- 将
jsoncpp中test模块的编译改为OFF;
- 链接
jsoncpp静态库jsoncpp_static.a。- Config: log的配置文件
- 加载和解析配置文件;
- 从JSON对象中提取日志级别、文件名和路径信息;
- 允许外部访问解析后的日志信息。
- ConfigManager: 热更新(在Config中实现)
- 通过接口返回config的智能指针;
- 加载新配置时,创建新的config智能指针替换原有的智能指针,使其相互独立,相互不产生影响;
- 智能指针的赋值线程不安全,需要加锁。
注意:在bin目录下手动创建log目录
注意:链接jsoncpp时,需要在/src/base/CMakeLists.txt中添加target_link_libraries(base_test base jsoncpp_static.a)
高性能服务器:
- 性能表现:
吞吐量大、延时低、资源使用率低;- 影响性能的因素:
上下文切换(线程切换、系统调用、锁)、内存拷贝、多线程/多进程。
流媒体服务器:
- 特点:
- 长连接;
- 并发量大:
- 万兆网卡的最大传输速率:10 000 000 000 bps;
- 一般视频流的码率:2 500 000 bps;
- 跑满网卡的80%(预留20%给其他程序通信):并发数 = 10 000 000 000 * 0.8 / 2 500 000 = 3200。
- 数据量大:
- 跑满网卡的80%(预留20%给其他程序通信):数据量 = 10 000 000 000 * 0.8 = 8 000 000 000 bps
- 平均延时低。
- 编程框架:
- 网络库接收数据,经过流媒体模块进行解析,最后由直播业务模块处理;
- 直播业务进行请求发送,流媒体模块封装,最后由网络库进行发送;
- 网络库是整个系统的输入输出,网络库的性能决定整个系统的性能。
高性能网络库:
- 功能:
- 事件循环:IO就绪事件监听,IO事件处理,事件管理;
- 任务执行:任务入队,任务执行;
- 定时任务:定时任务检测,定时任务执行。
- 选型:
- 同步IO模型 --> IO复用模型;
- Reactor模式 --> 事件循环在一个工作线程内;
- 线程池 --> 每个线程是一个事件循环。
- 针对性能的优化:
- 事件在同一个线程内循环 --> 减少线程的切换以及锁的使用;
- 减少IO调用 --> 分散读,聚集写;
- 减少内存拷贝 --> 发送队列只保存地址和长度。
- 事件循环:
- IO就绪事件监听;
- IO事件处理;
- 事件管理。
- EventLoop的特性:
- 一个线程只有一个EventLoop --> 通过线程局部变量实现;
- EventLoop中的Loop一定是在其所在的线程中运行;
- EventLoop中的Loop通过epoll实现IO就绪事件监听。
- Event:
- Epoll处理的是文件描述符的就绪事件;
- IO事件处理函数和文件描述符抽象成一个事件类Event;
- 通过epoll_event的data私有数据成员将Event和epoll_event绑定在一起。
- Event的设计:
- Event是一个基类,主要定义事件接口;
- Event可以处理的事件:读、写、关闭、出错;
- Event事件只在同一个EventLoop循环;
- Event事件由EventLoop管理。
- EventLoopThread的设计:
- EventLoopThread通过创建一个std::thread线程来运行EventLoop;
- EventLoopThread只运行一个EventLoop;
- EventLoopThread保证EventLoop的生命周期与std::thread相同。
- 线程池:
- 利用多个CPU或者多个核,提高并发能力,提高服务器性能;
- 统一管理线程,有利于利用CPU性能;
- 线程池预先创建好线程,减少启动时间;
- 线程池最终对外提供EventLoop来使用线程池的并发能力。
- EventLoopThreadPool的设计:
- 绑定CPU:std::thread运行的CPU不确定;
- 使用Linux提供的接口pthread_setaffinity_np将线程绑定到指定的CPU上;
- 配置起始CPU和线程数量。
- 任务队列:
- EventLoop提供执行任务的功能;
- EventLoop执行一个任务,有两种情况:
- 1.调用方所在线程和EventLoop所在线程是同一个线程,则直接执行;
- 2.调用方所在线程和EventLoop所在线程不是同一个线程,任务入队,由Loop执行。
- 任务队列需要加锁。
- TimingWheel定时任务:
- 每个EventLoop都有一个TimingWheel,定时任务只在自己的EventLoop内循环;
- TimingWheel固定一秒转一次;
- 定时任务提供天、小时、分钟、秒4种单位的时间刻度。
- 网络地址类:
- 网络编程经常需要用到IP和端口,传递两个值比较麻烦;
- IP和端口经常需要转换成其他形式;
- 有时候需要对地址进行分类检测;
- InetAddress类方便存储IP和端口信息,提供地址相关的操作。
- Accept:
- TCP服务器由Accept接收新连接;
- Accept每次从接收队列中取出第一个请求,接收队列中都是完成了三次握手的请求;
- 设置SO_REUSEPORT后,多线程可以同时监听同一个地址和端口。
- 非阻塞Accept:
- 非阻塞的监听sockfd,会马上返回,没有连接就会返回EAGAIN;
- 非阻塞的监听套接字配合epoll使用;
- 有新连接,epoll返回读就绪事件;
- 边缘触发模式下,一次读事件要一直读到返回EAGAIN错误为止;
- Acceptor是一个Event的子类,主要处理读事件。
- 网络连接:
- 一个TCP连接由四元组决定:(local ip、local port、remote ip、remote port);
- UDP也可以是连接的,但是这个连接只是由内核记录IP和端口;
- 一个连接有很多操作:可读、可写、可关闭。
- Connection的设计:
- Connection保存连接的相关信息:套接字,local address、remote address;
- Connection可以增加一些连接相关的私有数据;
- Connection存在空闲状态,需要主动去激活Connection;
- Connection是Event的一个子类。
- TcpConnection的设计:
- TcpConnection代表一个TCP连接,由TCP服务器创建;
- TcpConnection是Connection的子类,也是Event的子类;
- TcpConnection处理套接字的所有IO事件;
- TcpConnection的IO处理函数都需要在EventLoop循环中;
- 处理套接字IO事件:
- 读事件:有数据来,读数据后通过回调通知上层处理数据;
- TcpConnection是面向字节流的连接,字节流本身没有边界;
- TcpConnection的读事件由EventLoop监听,数据由TcpConnection读,并调用回调处理;
- 套接字读事件使用了epoll边缘触发模式,一次触发需要一直读到没有数据为止;
- 使用readv接口,可以减少读IO的调用。
- 写事件:保存要发送的数据,可以发送数据,从队列中取出数据进行发送,直到发送完队列中的数据,通过回调通知上层;
- 业务层会调用TcpConnection的发送接口发送数据,实际的发送由TcpConnection完成;
- TcpConnection把队列中的数据发送完,调用回调通知上层;
- TcpConnection只保存要发送的数据地址和长度,不拷贝数据,并且认为数据的生命周期足够长,直到数据发送完;
- TcpConnection的发送函数可能由其他线程调用,要保证发送事件在TcpConnection所在的EventLoop中;
- 通过writev可以减少写IO的调用。
- 关闭事件:关闭连接,通知上层关闭连接;
- EPOLLHUP表示描述符的一端或两端已经关闭或挂断,或者被其他错误关闭;
- 关闭事件除了关闭描述符外,还需要通知上层业务,告知连接关闭;
- 有可能描述符关闭,但是仍执行任务队列和超时检测,所以需要一个状态位来表示连接已经关闭;
- TcpConnection提供强制关闭连接的接口,由业务层调用。
- 出错事件:打印出出错信息,关闭连接,通知上层关闭连接;
- EPOLLERR表示描述符遇到错误,套接字变成可读;
- 通过getsockopt函数的SO_ERROR选项可以读取错误号;
- 出错的套接字需要回收资源。
- 超时事件:关闭连接,通知上层连接超时。
- 闲置的TcpConnection是一种浪费,需要关闭,释放资源;
- TcpConnection的超时通过定时任务来触发;
- 超时后,直接把TcpConnection关闭,并通知上层业务;
- 只要有数据交互,就延长TcpConnection的生命周期;
- TcpConnection通过增加定时任务智能指针的引用来延长生命周期。
- MsgBuffer:
- 源文件来源于陈硕的muduo;
- TCP的数据是字节流,读取到的数据,不足一个消息,需要持有不足一个消息的数据;
- MsgBuffer实现了一个环形缓冲区,先进先出;
- MsgBuffer提供了安全写和便捷读的功能。
- TcpServer:
- TcpServer通过Acceptor接收客户端连接,创建TcpConnection,并通知业务层;
- TcpServer管理所有的TcpConnection,包括分配资源,回收资源,加入事件循环,退出事件循环,设置各种回调等;
- TcpServer提供操作TcpConnection的接口,是业务层与连接的中间层;
- DnsService:
- 本地DNS缓存的原因,域名在一定的时间内解析不会更新;
- 同一个域名不必多次请求解析;
- 后台统一解析能减少响应时间;
- 域名的更新时间和频率可以通过配置文件设置。
- TcpClient:
- TcpClient是TcpConnection的一个子类;
- TcpClient主动发起连接,TcpClient和Server之间只存在一个连接;
- TcpClient需要处理非阻塞连接,并且管理连接状态;
- TcpClient连接成功后,通过TcpConnection处理IO事件;
- TcpClient负责注册事件,删除事件;
- 非阻塞连接:
- 非阻塞的connect会马上返回,返回值有3种情况:
- 返回0,表示连接成功;
- 返回-1,并且错误号为EINPROGRESS,表示连接中;
- 返回-1,错误号不为EINPROGRESS,表示出错。
- 连接中的套接字变成可读可写,如果不是出错,则表示连接成功。
- UdpSocket:
- UDP面向数据报,报文是有边界的;
- UdpSocket是Connection的一个子类;
- UdpSocket负责处理UDP套接字的IO事件。
- UdpClient:
- UdpClient是UdpSocket的一个子类;
- UdpClient可以通过调用Connect来记录服务端IP和端口;
- UdpClient负责注册事件,删除事件。
- UdpServer:
- UdpServer是UdpSocket的一个子类;
- UdpServer接收不同客户端的数据,通过客户端的IP和端口区分数据包;
- UdpServer负责注册事件,删除事件。
- 有限状态机:
- 在协议解析上的应用:
- 表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型;
- 状态有限,但是最少两个状态;
- 状态转移,既定的条件满足,状态才会发生转移;
- 动作是条件满足后状态转移执行的动作;
- 其作用主要是描述对象在它的生命周期内所经历的状态序列,以及如何响应来自外界的各种事件。
- 实现:
- 了解起始、终止、现态、目标状态、动作、条件;
- 识别状态,定义状态;
- 定义状态转移,明确转移条件;
- 定义每个状态转移执行的动作;
- 实现控制器。
- 上下文Contex:
- 程序执行依赖的变量的集合;
- 承上启下;
- 同时用到多个变量;
- 中断后需要再次执行;
- 实例TestContext:
- 测试协议:长度+字符串;
- 协议的实现需要用到有限状态机;
- 消息长度需要保存变量;
- 解析出来的消息,需要调用回调通知上层。
- 多媒体数据包Packet:
- 流媒体协议的数据包内容主要包含:音频包、视频包、Meta包、其他数据包;
- 存储流媒体数据;
- 分配内存和释放内存;
- 判断包的类型。
- 多媒体模块协议回调类(MMediaHandler):
- 抽象基类,只定义纯虚函数;
- 由于多媒体模块实现的流媒体协议,只负责解析和封装协议,并且多媒体数据包需要集中管理,所以,多媒体数据包由直播业务模块进行管理;
- 多媒体模块的协议数据回调:
- 把回调函数都集中在一起,抽象一个接口类出来;
- 优势:一方面,回调都集中在一起,传递一个基类指针就能调用所有的回调;另一方面,每种协议需要的回调可能不一样,只需要继承这个基类,加上自己需要的回调就可以。
- rtmp握手实现:
- 随机数生成;
- std::mt19937是一个高质量的伪随机数生成器;
- std::uniform_int_distribution是一个离散均匀分布类,用于生成指定范围的随机整数。
- 握手包大小;
- C0和C1固定为1个字节,并且内容都是0x03;
- C1和S1固定为1536个字节,C1和S1的digest需要保存,用于验证S2和C2;
- C0, C1和S0, S1可以一起发送一起接收,一共1537个字节;
- C2和S2固定为1536个字节。
- 复杂握手digest计算和验证;
- 算法hmac-sha256,使用openssl的接口实现;
- 客户端和服务端用不同的固定的key;
- offset需要针对两种结构分别计算;
- C1和S1选择digest(764 bytes)在前,key(764 bytes)在后的结构;
- C2和S2的验证需要用到S1和C1的digest部分。
- digest offset计算:
- offset = offset[0] + offset[1] + offset[2] + offset[3];
- 定位digest(764 bytes)部分,偏移量8或者8 + 764;
- digest(32 bytes)的位置 = offset % (764 - 4 - 32)+(偏移量8或者8 + 764)+ 4。
- 客户端握手状态机;
- 服务端握手状态机。
- RtmpServer:
- RtmpServer是TcpServer的一个子类;
- TcpServer重点在于网络数据, RtmpServer重点在Rtmp协议数据;
- RtmpServer的TcpConnection将需要进行Rtmp握手才算是建立起真正的Rtmp连接;
- RtmpServer通过协议接口类传递数据给直播业务模块。
- RtmpHandler:
- RtmpHandler是MMediaHandler的一个子类;
- RtmpHandler实现Rtmp特殊的接口:OnPlay,OnPublish,OnPause,OnSeek。
- RtmpHeader:
- rtmp数据包的传输,既有Message层面的流复用,也有Chunk层面的流复用;
- 需要从多个Chunk流中正确的解析RTMP Message;
- 多个Message Stream可能通过同一个Chunk Stream传输;
- 合适的协议存储结构,有利于解析RTMP Chunk包,还原Message包。
- BytesReader:
- 从缓存(二进制数据流)中读取整型数值,并且从网络字节序转成主机字节序。
- BytesWriter:
- 将整数值转换为二进制格式并写入数据流中,常用于网络协议或文件写入等场景。
- Rtmp 数据包接收:
- Rtmp Message重组实现:
- 一个Message只在同一个Chunk Stream传输,csid标识一路Chunk Stream;
- 针对不同的Chunk Type(fmt),Message Header的处理方法不一样;
- Extended Timestamp处理;
- Chunk Data处理;
- csid计算:
- 取Chunk第一个字节后6位,得到csid,csid非0,1则不需要重新计算;
- csid为0,则重新计算csid = 64 + chunk[1];
- csid为1,则重新计算csid = 64 + chunk[1] + 256 * chunk[2]。
- csid作用:
- 识别一路Chunk Stream;
- 识别Message Header;
- 识别Timestamp Delta;
- 识别Extended Timestamp;
- 识别Message Body。
- Chunk Type:
- Fmt = 0,Message Header 11 字节;
- Fmt = 1,Message Header 7 字节;
- Fmt = 2,Message Header 3 字节;
- Fmt = 3,Message Header 0 字节;
- Fmt不同值的含义:
- Fmt = 0:
- Timestamp 3个字节;
- Message Length 3个字节;
- Message Type 1个字节;
- Message Stream ID 4个字节。
- Fmt = 1:
- Timestamp Delta 3个字节, Timestamp = Delta + Prev Timestamp;
- Message Length 3个字节;
- Message Type 1个字节;
- Message Stream ID = Prev Message Stream ID。
- Fmt = 2:
- Timestamp Delta 3个字节, Timestamp = Delta + Prev Timestamp;
- Message Length = Prev Message Length;
- Message Type = Prev Message Type;
- Message Stream ID = Prev Message Stream ID。
- Fmt = 3:
- Timestamp = Prev Delta + Prev Timestamp;
- Message Length = Prev Message Length;
- Message Type = Prev Message Type;
- Message Stream ID = Prev Message Stream ID。
- Message Header解析实现:
- 按fmt的值,计算Message Header各个属性的值;
- 通过csid缓存Message Header,给下一次解析提供信息;
- 通过csid缓存delta值,用于fmt3类型计算timestamp。
- Extended Timestamp:
- 条件:Message Header中的 TimeStamp或者TimeStamp Delta字段值为0x00ffffff;
- 计算方法:Message Header后的4个字节;
- Fmt为3时, 前一个包有Extended Timestamp,则当前包也需要计算Extended Timestamp;
- Fmt不为1时,Extended Timestamp计算出来的是TimeStamp Delta。
- Message Body重组:
- 一个Chunk,除去Chunk Header(Basic Header + Message Header + Extended Timestamp),剩下的就是Chunk Data,属于Message Body;
- Message Body完成的条件是数据量达到Message Header描述的Message Length大小。
- RtmpContext设计*:
- 接收到的数据处理:
- 握手状态:由握手对象处理握手包;
- 解析Message状态:由RtmpContext解析Message。
- 接收数据状态转移。
- Rtmp 数据包发送:
- Rtmp Message发送实现:
- 一个Message只在同一个Chunk Stream传输,csid标识一路Chunk Stream;
- 根据Message Header的不同情况,选用不同的fmt构建Chunk头;
- 根据不同范围的csid,填写不同长度的basic header;
- Timestamp大于等于0xFFFFFF,启用Extended Timestamp;
- Message Body根据Chunk Size的大小,填充Chunk。
- csid作用:
- 识别一路Chunk Stream;
- 识别Message Header;
- 识别Timestamp Delta。
- csid输出:
- csid<64,csid写在Basic Header第一个字节的后6位;
- csid 大于等于64,小于(64+256),第一个字节后6位填0,第二个字节填(csid-64);
- csid大于等于(64+256),第一个字节后6位填1,第二三个字节填16位的(csid-64)。
- Chunk Type:
- Fmt = 0:
- 条件:强制,csid第一个包,timestamp比前一个小,message stream id跟前一个包不一样;
- Timestamp 3个字节;
- Message Length 3个字节;
- Message Type 1个字节;
- Message Stream ID 4个字节,小端存放。
- Fmt = 1:
- 条件:fmt0条件全部不成立(Message Stream ID一样),并且Message Length跟前一个不一样或者Message Type不一样;
- Timestamp Delta 3个字节, Timestamp Delta = Timestamp - Prev Timestamp;
- Message Length 3个字节;
- Message Type 1个字节。
- Fmt = 2:
- 条件:fmt0条件全部不成立(Message Stream ID一样),并且Message Length和Message Type跟前一个一样;
- Timestamp Delta 3个字节, Timestamp Delta = Timestamp - Prev Timestamp。
- Fmt = 3:
- 条件:fmt0条件全部不成立(Message Stream ID一样),并且Message Length和Message Type跟前一个一样,Timestamp Delta 跟前一个也一样。
- Message Header封装实现:
- 当前Message跟前一个Message对比,确定fmt的值;
- 根据fmt的值,封装Message Header;
- 通过csid缓存Message Header,给下一次封装提供信息;
- 通过csid缓存delta值,用于确定当前Message是否使用fmt3。
- Extended Timestamp:
- 启用条件:Message 的 TimeStamp值大于等于0x00ffffff;
- 存放在Message Header后的4个字节。
- Message Body发送:
- Message Body是Chunk Data的承载内容;
- 封装完Message Header和Extended Timestamp后,Chunk 剩下的大小(out_chunk_size – Chunk Header)存放部分Message Body;
- 剩下的Message Body封装成多个fmt3的Chunk 发送;
- 音视频数据包以及Meta数据包一般多个包封装chunk后,一起发送;
- Rtmp控制消息和命令消息单个包发送;
- RtmpContext保证数据发送完成前,数据不会被清理;
- Chunk Header和Chunk Data分开发送,减少拷贝;
- 单次发送完成,才能继续发送下一批数据。
- Rtmp协议控制消息:
- 设置和交换Chunk Stream传输参数;
- 协议控制消息的消息类型为 1,2,3,5,6;
- 消息的Message Stream ID固定为0;
- Chunk Stream ID固定为2;
- 协议控制消息不需要回应;
- 设置块大小消息:
- 消息内容为4字节的整数;
- 用于告知对端发送的Chunk的最大长度;
- 接收方把in_chunk_size设置成消息内容的值。
- 确认消息:
- 消息内容为4字节的整型;
- 用于告知对端接收到等同于窗口大小的字节;
- 接收方通过这个消息知道对端接收数据的情况。
- 确认窗口大小消息:
- 消息内容为4字节的整型;
- 用于告知对方发送确认消息的窗口大小;
- 接收方记录这个值,接收到这个值大小的数据后,回复确认消息。
- 设置带宽消息:
- 消息内容为5字节:前4个字节是确认窗口大小,后一个字节是限制类型;
- 用于告知对方出口带宽的大小;
- 接收方记录这个值,限制未被应答的消息数据大小。
- RTMP用户控制消息:
- 用于告知对方执行该信息中包含的用户控制事件;
- 用户控制消息的消息类型为 4;
- 消息的Message Stream ID固定为0;
- Chunk Stream ID固定为2;
- 用户控制消息在接收到后,需要马上生效。
- 用户控制事件处理:
- 服务端响应kRtmpEventTypePingRequest,返回kRtmpEventTypePingResponse;
- 服务端发送kRtmpUserStreamBegin。
- AMF解析:
- AMF:
- Message Type为0x14的包,携带AMF0序列化数据;
- Message Type为0x11的包,携带AMF3序列化数据;
- AMF3的object类型的body使用AMF0序列化;
- AMF3的object去掉第一个字节0x00,可以使用AMF0解析。
- AMF的数据类型:
- AMF的数据类型分为简单类型和复杂类型;
- 简单类型的内容为单一数据类型;
- 复杂类型的内容为多个简单类型和复杂类型;
- 简单类型:Number,Boolean,String,Null,LongString, Date;
- 复杂类型: Object,MixedArray,Array。
- AMF类型实现:
- AMF类型主要实现Number,Boolean,String,LongString, Date,Object;
- 为了方便操作,所有的类型派生于AMFAny;
- AMF数据以Object为单位来组织,Object有多个属性,每个属性为一个AMF类型值。
- AMFAny类:
- AMFAny是一个基类;
- AMFAny提供获取数据的接口,主要有Number,Boolean,String, Date,Object;
- AMF定义解析接口。
- AMFNumber类:
- AMFNumber是AMFAny的一个子类;
- AMFNumber实现Number数据类型的解析;
- AMFNumber实现返回Number的值;
- AMFNumber实现IsNumber;
- AMFNumber实现Dump。
- AMFBoolean类:
- AMFNumber是AMFAny的一个子类;
- AMFBoolean实现Boolean数据类型的解析;
- AMFBoolean实现返回Boolean的值;
- AMFBoolean实现IsBoolean;
- AMFBoolean实现Dump。
- AMFString类:
- AMFString是AMFAny的一个子类;
- AMFString实现String数据类型的解析;
- AMFString实现返回String的值;
- AMFString实现IsString;
- AMFString实现Dump。
- AMFLongString类:
- AMFLongString是AMFAny的一个子类;
- AMFLongString实现LongString数据类型的解析;
- AMFLongString实现返回LongString的值;
- AMFLongString实现IsString;
- AMFLongString实现Dump。
- AMFDate类:
- AMFDate是AMFAny的一个子类;
- AMFDate实现Date数据类型的解析;
- AMFDate实现返回Date的值;
- AMFDate实现IsDate;
- AMFDate实现Dump。
- AMFObject类:
- AMFObject是AMFAny的一个子类;
- AMFObject实现Object所有属性数据的解析;
- AMFObject实现返回Object的指针;
- AMFObject实现IsObject;
- AMFObject实现Dump;
- AMFObject实现Count。
- AMF封装:
- 封装AMF版本:AMF0;
- 实用为主,足够完成命令消息交互;
- 简单类型封装:Number,String,Boolean;
- 复杂类型封装:Object;
- 键值对封装:NameNumber,NameString,NameBoolean。
- 命令消息:
- NetConnection是客户端和服务器之间的双向连接的高级表达:
- 可以使用的命令:connect,call,close,createStream;
- 消息类型20,csid固定使用3;
- message stream id 为0。
- NetStream定义了通过NetConnection把音频,视频和数据消息流在客户端和服务器之间进行交换的通道;
- connect命令:
- 客户端在握手成功后,发起connect命令;
- 服务端收到connect命令后,执行connect操作,并用_result或者_error回复客户端;
- 命令处理:
- tcUrl属性描述推流的URL;
- app属性描述客户端的推流点;
- objectEncoding指示接下来使用的amf版本。
- 命令响应:
- 设置对端的确认窗口大小以及带宽大小,也可以设置对端的chunk size。
- connectStream命令处理:
- 客户端在收到connect响应后,发起createStream命令;
- 服务端收到createStream命令后,执行createStream操作,并用_result或者_error回复客户端;
- 第二个属性为Transaction ID,回复消息需要用这个值。
- play命令:
- 播放客户端在接收到createStream响应后,发起play命令;
- 服务端收到play命令后,执行play操作,发送用户控制消息通知流开始事件,并用onStatus通知客户端。
- play命令处理:
- 第二个属性为Transaction ID;
- 第四个参数为流的名称;
- 需要回复用户控制消息,通知事件kRtmpUserStreamBegin;
- 发送状态NetStream.Play.Start;
- 通知业务层。
- publish命令:
- 推流客户端在接收到createStream响应后,发起publish命令;
- 服务端收到publish命令后,执行publish操作,并用onStatus通知客户端。
- publish命令处理:
- 第二个属性为Transaction ID;
- 第四个参数为流的名称;
- 发送状态NetStream.Publish.Start;
- 通知业务层。
- _result命令处理:
- 第二个属性为Transaction ID,标识是哪一个命令的结果。
- _error命令处理:
- 第二个属性为Transaction ID,标识是哪一个命令的结果;
- 第四个对象中的description字符串描述出错的信息。
- RtmpClient:
- RtmpClient使用TcpClient收发数据;
- RtmpClient负责完成rtmp层面的功能,包括握手,调用RtmpContext解析rtmp协议数据;
- RtmpClient分为两种:拉流Client和推流Client。
- 直播管理概述:
- 在直播系统中,直播业务是一个核心模块,直播的所有业务都围绕实时音视频数据来进行:
- 推流业务接收推流端推上来的音视频数据;
- 拉流/播放业务根据客户端要求的协议,把实时音视频数据封装后,发送给客户端;
- 回源业务则是拉流功能的一个应用,从源站把实时音视频数据拉到边缘服务器;
- 录制业务把实时音视频保存到文件或者云端;
- 时移业务是录制业务的一个应用;
- 转码把实时音视频数据转成目标格式的数据。
- 直播配置:
- 直播服务器配置:
- 监听地址;
- 监听端口;
- 协议名称;
- 传输协议。
- 直播业务配置:
- 直播业务配置由domain+app唯一标识,一个domain下可能有多个app;
- DomainInfo是域名级别的配置;
- AppInfo是推流点级别的配置;
- 直播业务配置由单个文件描述,一个文件就是一个域名的直播业务配置。
- 直播业务配置文件目录:
- 配置的是文件路径,则直接解析;
- 配置的是文件目录,则遍历整个目录,解析其中的文件。
- 时间戳修正:
- 推流实时音视频的时间戳不允许回退;
- 实时音视频输出的时间戳需要保持平滑增加;
- 时间戳修正算法:
- 时间戳修正主要是通过原始时间戳的差值来决定修正的值;
- 正常的delta直接计算出新的时间戳;
- 差值太大(MaxDelta)或者太小(-MaxDelta),则使用默认的差值(DefaultDelta)来计算;
- 选定视频的时间戳为基准;
- 连续的音频的时间戳以音频自己为基准修正。
- CodecHeader:
- 播放器需要CodecHeader进行初始化;
- CodecHeader可能会变化;
- 播放用户可能在不同的时间点进行播放;
- 定义:
- RTMP推流携带的是FLV Tag封装格式的数据;
- 音频流的CodecHeader是AudioTag封装,第二个字节的为0,则是AAC sequence header;
- 视频流的CodecHeader是VideoTag封装,第二个字节为0,则是AVC sequence header;
- Meta的CodecHeader是ScriptTag封装,第二三个字节固定为0x000A。
- Meta解析:
- Meta数据包有两个AMF object,一个是一个字符串onMetadata,另一个是一个MIXEDARRAY,里面是一些音视频相关的属性值;
- 视频属性值:width,height,videocodecid,framerate,videodatarate;
- 音频属性值:audiosamplerate,audiosamplesize,audiocodecid,audiodatarate;
- 其他属性值:duration,encoder,server。
- Header查找:
- 使用将要发送的Packet的Index查找对应的Header;
- Index应该比Header的Index要大,查找第一个比Index小的Header。