语音通信是实时通信,一定要保证实时性,不然用户体验会很糟糕。IETF设计了RTP来承载语音等实时性要求很高的数据,同时设计了RTCP来保证服务质量(RTP不保证服务质量)。在传输层,一般选用UDP而不是TCP来承载 RTP包。下图给出了这三个协议所在的协议层次。
本文先简单讲一下这三个协议(网上好多文章都讲,这里主要讲关键点),然后讲软件实现注意点。
1,RTP
RTP全称是Real-time Transport Protocol(实时传输协议),它是IETF提出的一个标准,对应的RFC文档为RFC3550。一般用其承载实时性要求很高的数据形成RTP包,在语音通信中,把PCM数据编码后得到的码流作为RTP的payload。下图是其包头结构。
这里主要讲一些关键点:
a) 包头的版本信息等都是用几个比特位来表示的,共两个字节,在软件实现时要用位域的形式表示。在大小端情况下有不同的表示方式,主要是在一个字节里大小端表示时位置要互换,具体如下图RTP头数据结构定义。
b) RTP包都是以网络序/大端的形式在网络中传输,这样就有一个网络序主机序互转的过程。
c) M 位即Mark位,表示语音的开始,在通话刚开始的第一个语音包,M位要置1。如果 VAD使能,从VAD包切到语音包时,第一个语音包M位也要置1。
d) 在通话刚开始的第一个语音包中,sequence/timestamp/SSRC等都要是随机值。在后续的包中,SSRC代表通话的一方,在整个通话过程中都要保持不变。Sequence要每次加一。Timestamp要依据采样率以及帧长每次加本帧内采样的点数值,比如8000 Hz采用率,帧长为20ms, 每次timestamp要加160。
软件实现RTP协议时,先要初始化(主要是包头字段的初始化)。发送方把每一帧PCM数据编码后得到码流,将其作为RTP的payload,同时填充好包头中的字段,然后通过UDP socket发送到网络中去。接收方通过UDP socket收到RTP包,解析包头得到payload type/sequence等信息,同时也得到payload,然后将它们送给下一个模块处理。
实现完RTP后要检查实现的是否正确,抓包看是主要的方式。抓到包后把UDP转换成RTP后就可以看相关信息了,主要看格式是否对以及包头中的字段的值是否对。如果格式不对,抓包工具(wireshark)会提示。
2,RTCP
RTCP全称是Real-time Control Protocol(实时控制协议),它也是IETF提出的一个标准,对应的RFC文档为RFC3551,它的主要功能是:服务质量的监视与反馈、媒体间的同步。在RTP会话期间,各参与者周期性地(一般是5秒,应用层可以配)传送RTCP包,RTCP包中含有已发送的数据包的数量、丢失的数据包的数量、数据包到达的平均时间间隔等统计信息。
RTCP协议处理机定义了五种类型的报文,它们完成接收、分析、产生和发送控制报文的功能,如下表所示:
其中SR用来使发送端周期的向所有接收端用多播方式进行报告。RR用来使接收端周期性的向所有的点用多播方式进行报告。当参加者既发又收时就发SR,只收不发时就发RR。SDES给出会话中参加者的描述,包括参加者的规范名(CNAME)。BYE用来关闭一个数据流。APP能够定义新的分组类型。前四种类型经常用到,APP类型很少用到,我是没用过。
这五种类型中SR应用最频繁,就以它为例来讲,其他类型可以举一反三。它的封装结构见下图:
同RTP一样,这里也主要讲一些关键点:
a) 与RTP类似,RTCP包头中一些值也用比特位表示,实现时也要用位域表示,也有大小端的问题。
b) 算length时,计算公式是length = size/4 -1。其中size是SR包的真实大小(单位是字节)。
c) 算周期内丢包率(fraction lost)时,是以定点小数形式表示,即 fraction lost = (周期内丢包数 << 8) / 周期内期望接收包数.
d) 算DLSR时,是以1/65536秒为单位。
软件实现RTCP协议时,一般是几种类型的RTCP包组成组合包,所以一般先要判断要发几种类型的包。当处于SendReceive模式时,要发SR/SDES包,如果要停止通话,还要发BYE包;当处于ReceiveOnly模式时,要发RR/SDES包;如果要停止通话,还要发BYE包。RTCP中有RTP包的统计,所以实现RTCP前要先在RTP中把相关的统计做好,同时还要做好sequence number的管理等。实现RTCP时发送方先要实现SR或者RR包,然后是SDES包,如果是停止通话,还要加上BYE包。实现每种RTCP包时是把相应的字段值填好,然后再把包头填好。这些包都实现后拼在一起形成组合包,然后通过UDP socket发送到网络中去。接收方收到RTCP组合包后也是一个包一个包的去解析,然后把相应的信息报告给上层。
实现完RTCP后同样要检查实现的是否正确,抓包看同样是主要的方式。抓到包后把UDP转换成RTCP后就可以看相关信息了,主要看格式是否对以及包中相应的字段是否对。如果格式不对,抓包工具会提示。
个人觉得实现RTP相对简单,RTCP相对复杂一些,它是基于RTP的,有对RTP的各种统计,有对RTP sequence number的管理等。同时还要理解RTCP中为什么要设计这些字段以及它们的计算方法。这些都搞清楚了也就不难了。
3,UDP
UDP 全称是User Datagram Protocol(用户数据报协议),属于传输层协议(跟TCP在同一层),提供面向事务的简单不可靠信息传送服务,在IETF中对应的RFC文档为 RFC 768。至于为啥用UDP而不用TCP来传输实时性要求较高的数据,个人觉得主要有以下几点:TCP的重传机制(这时最主要的原因),TCP的包头较大浪费了带宽,TCP不支持组播。
实现时主要是调用系统提供的socket API。具体到linux上,我做过两种实现,一种是在user space里(这也是绝大多数使用者用的方法),另外一种是在kernel space里,用kernel 提供的socket API做。不管是user space还是kernel space里实现,主要是掌握socket API的使用,都是些套路,这里就不详细讲了。UDP的socket创建好后给RTP、RTCP用(RTP、RTCP各用一个socket,各有一个port号,一般RTP用的是偶数, RTCP的port号是相对应的RTP的port号加一)。