完整SIP:SDP媒体协商概论-WebRTC:ICE概览与ICE初始offer发送和接收详解
原文出处:完整SIP/SDP媒体协商概论-WebRTC/ICE概览
在前面的章节中,我们完整介绍了SIP中SDP的offer/answer交互流程。接下来,我们重点介绍关于SDP在WebRTC中的交互方式以及使用ICE来支持NAT处理的内容。一些读者可能注意到了,WebRTC技术虽然具有非常大的市场前景,但是因为本身和浏览器等其他工具的兼容性问题,发展的速度仍然没有想象的那么快,一些应用场景也不是太完善。很多针对ICE的规范也快速进行更迭。最近的一次更迭就是针对ICE的RFC5245规范已废除,使用了RFC8445。因为,RFC8445是2018年发布的最新的规范,因此,可能一些厂家的产品还没有完全实现对此规范版本的支持。所以,我们的讨论尽量按照RFC5245的规范来进行,也同时兼顾RFC8445的框架。笔者提前说明,WebRTC中关于ICE的规范流程非常庞杂,笔者需要分成多个章节来解释。所以,在WebRTC/ICE章节讨论中我们需要花费一定的时间,如果读者对ICE没有兴趣的话或者暂时没有使用WebRTC技术的话,可以跳过关于WebRTC/ICE的介绍直接阅读SIP/SDP的部分内容,或者,如果读者仅想了解WebRTC技术和基本的工作原理的话,可以阅读:
在本章节和后续章节的讨论中,笔者将根据RFC5245/RFC8445的规范架构,结合笔者的这篇历史文章来讨论WebRTC/ICE的详细技术内容。希望通过笔者完整的讨论,读者可以非常清楚了解WebRTC的流程,特别是关于ICE的处理。笔者在文章中使用的一些本规范的专有名词(例如,check 或者check list,事实上,在本规范中它具有特定的含义),可能没有直接以中文的名称来介绍,这样做的目的是为了保证不会产生歧义,所以笔者尽量使用规范中的专有名词来解释。所以,请读者在阅读时一定要注意。
说明:1)一些专有名词以前的章节中已经发布,这里不再介绍。2)笔者本人水平有限,文章中所使用的专有名词中文命名或解释可能和其他网上的的有所不同,建议读者参考 RFC原文理解或者发邮件给规范起草人获得支持。

笔者会在接下来的内容中重点讨论关于ICE的背景介绍,ICE概览,ICE使用/ICE候选地址采集和交互,ICE候选对象流程处理,执行连接性检查,完成ICE创建,轻量级ICE使用介绍等话题。

1 背景介绍
如果读者看过前面的SDP全解的读者可能已经了解,SIP使用了offer/answer结合模式,通过SDP消息来实现媒体传输,其最终目的是实现媒体流之间的创建和完整传输。ICE英文全名是Interactive Connectivity Establishment。RFC5245(更新的RFC6336)对ICE做了规定。笔者推荐的一般简单的定义是:ICE=STUN+TURN+协商机制+协商路径。
但是,因为网络越来越复杂,终端的环境也发生了根本变化,因此NAT问题也越来越多。RFC3235规范针对NAT(地址转换)发布了一个指导。很多相关的协议希望通过媒体流之间的点对点传输解决媒体本身的问题(例如,低时延,降低丢包,降低部署成本),但是在涉及到NAT环境时,通常会遇到一些更加复杂的问题,导致实际部署的难度大大增加。为了解决NAT的问题,很多针对性的技术规范增加了对NAT环境的支持,常见的规范包括:
- Application Layer Gateways (ALGs)
- Middlebox Control Protocol (RFC 3303)
- STUN(RFC 3489)
- Realm Specific IP(RFCs 3102和3103)
- SDP拓展支持(RFC 4566和RFC 3605)
随着技术本身的不断发展,这些针对NAT支持的规范也带来了很多突出的问题,它们都存在各自的优缺点。这样的话,网络管理就会增加很多的不确定性,给系统管理带来很多问题。为了解决这些问题,一些相关规范组织希望使用一种统一的解决方式或规范,并且这种协议可以提供灵活性来满足目前网络环境对NAT的支持。目前,大家一致的共识就是使用ICE技术(Interactive Connectivity Establishment)。此规范通过offer/answer模式定义ICE来支持基于UDP的媒体流NAT问题(当然,ICE也可以通过拓展支持ICE-TCP传输协议)。
ICE是offer/answer模式的一种拓展形式,通过在SDP的offer/answer消息中包含多个地址和端口,使用这些地址端口和其交互消息来检测点对点的连接性。在SDP中的地址和端口,以及其连接性检测是通过STUN来完成。

注意,关于STUN的最新规范已经再次更新(更新时间为2020年),如果读者有兴趣做进一步研究,可参考RFC5389和RFC8489。除了STUN以外,ICE也使用了STUN的另外一个拓展协议-TURN(RFC5766)实现穿越转发。另外,因为ICE对每个媒体流进行了多地址和端口交互,它也允许地址选择支持多宿主和双栈主机(IPv4/IPv6),因此也不再支持RFC4091和RFC4092。
2 ICE概览
在一个比较典型的ICE部署环境中,至少需要两个终端需要互相进行通信。终端之间可以通过一些信令结合SDP的offer/answer模式来实现,例如,我们前面讲的SIP协议。读者需要注意,ICE的目的不是为了某些协议(例如SIP)的NAT穿越,关于SIP相关的NAT穿越是通过RFC5626规定的,如果读者有兴趣的话,可以查阅此规范。这里,ICE假设终端之间可以创建协议连接。具体来说,在ICE流程处理开始阶段,agents就忽略了它们本身的技术属性。终端也可能在或不在NAT后(例如后面将的轻量级部署终端),ICE允许终端获取到技术属性的足够信息,然后找到一个或多个潜在的路径,创建数据会话实现相互连接。

在以上示例是一个非常典型的ICE部署场景示例。两个终端分别标识为L(左边)和R(右边)。两个终端都有各自的NAT环境,双方也不清楚对端NAT的状态属性,左右双方终端都有意愿通过ICE候选对象交互实现通信。很多时候,双方的交互可能都使用SIP协议。对于双方终端,SIP服务器和NAT来说,在网络中,ICE经常使用STUN和TURN来保证所有对象的协同。每个终端agents可以支持自己独立的STUN和TRUN服务器,也可以同一STUN和TURN同一服务器。基本的ICE工作理念是: 针对传输协议(这里重点讨论UDP)每个agent都有各种候选地址(绑定了IP地址和端口),通过这种候选地址和对端agent实现通信。候选地址可能包括:附加到网络接口的传输地址(server reflexive 地址,其地址是STUN发现的地址,在NAT外),NAT中公网侧的转换后的传输地址,从TURN分配到的传输地址(转发地址)。客观上存在这种可能,任何L侧的候选传输地址都用来和任何R侧的候选传输地址进行通信。但是,在实际场景中,过多的候选传输地址组合可能不能工作。例如,如果L侧和R侧双方都在NAT后的话,直接附加的网络接口地址可能不会直接通信,因此这里需要ICE介入。这里,ICE的目的是发现何种候选地址配对可以工作。ICE的工作方式是通过系统地尝试所有可能的候选配对(排序处理后),直到ICE找到一组或者多组可以工作的候选配对。ICE对Peers的检测配对需要经过几个核心步骤:

3 采集候选地址
如果需要执行ICE的话,agent首先需要确认候选地址。候选地址是一种特别的地址,它是由IP地址和端口的组合构成,其目的是支持传输协议(我们这里仅讨论UDP)。RFC8445定义了三种候选地址,一种是来自于物理网络接口,另外一种来自于网络的逻辑接口,还有一种是通过STURN或者TURN发现的候选地址。第一种类别的候选地址支持的传输地址直接从本地网络接口获得。这样的候选地址我们称之为“host candidate”,这样类型的本地地址可以从本地网络,WIFI,远端网络或者VPN的方式获得。对agent来说,这样的地址都可以通过分配获得,并且作为本地接口来使用。如果agent是一个多宿主主机的话,它可以从不同的IP地址获得候选地址。具体的候选地址如何获得取决于网络中peer(会话中另外一个agent)的位置。例如,如果agent支持了两个不同的IP地址的话(一个是内网地址,一个是外网地址),不同的peer就可以通过不同的地址和agent通信。
另外一种候选地址是agent使用STUN或TURN获得的额外的候选地址,这些候选地址主要表现为两种地址形式:NAT公网侧的已转换地址(server-reflexive 候选地址),TURN服务器分配的转发地址(relayed 候选地址)。这里读者一定要注意两种地址的获取方式。当使用TURN服务器时,以上两种候选地址都来自于TURN服务器分配地址;当仅使用了STURN服务器的话,agent只能获取到serverreflexive地址。以下是候选地址的关系示例图:

在以上关系图中,两种类型的候选地址都是通过TURN服务器发现获得的。这里的地址端口配对中,大写X表示IP地址,小写x表示UDP端口。当agent对IP地址和端口发送一个(X:x)TURN地址分配请求时,NAT(假设这里有NAT)就会创建一个绑定关系X1':x1',映射server-reflexive地址到主机候选地址。从本地主机候选地址发送出去的数据包将会通过NAT地址转换变成一个server-reflexive候选地址。同样的道理,从server-reflexive候选地址进入到主机候选地址的数据也是通过NAT地址转换完成。这里,base是一个比较重要的概念,需要读者明确,“base"是一个本地主机候选地址,它关联一个给定的server-reflexive候选地址。base指的是一个agent为指定的候选地址发出数据的地址。因此,有时存在一个比较极端的场景,主机候选地址也可以有自己的base地址,这个base 地址和主机候选地址相同。

当agent和TURN服务器存在多个NAT穿越时,TRUN请求会为每个NAT创建一个绑定,但是agent仅发现最外部的server-reflexive候选地址(距离TRUN服务器最近的候选地址)。如果agent不在NAT后的话,base 候选地址和server-reflexive候选地址相同,server-reflexive候选地址就会被移除。
Agent发出的分配请求到达TURN服务器端后,TURN服务器将会从它的本地IP地址Y中分配一个端口y,并且生成一个分配响应,分配响应通知一个agent的转发候选地址。TRUN服务器也会通知一个agent的server-reflexive候选地址X1':x1',通知的方式是把分配请求中的源传输地址拷贝到分配响应中来实现。TRUN服务器的工作角色类似于一个在L侧和R侧之间的数据转发。如果R侧想对L侧发送数据的话,R侧需要发送数据到Y:y地址,然后TRUN服务器端前转到X1':x1'候选地址,然后经过NAT后映射到L侧agent。
当仅使用了STUN服务器时,agent发送一个STUN绑定请求给它的STUN服务器,STUN将会通知一个agent的server-reflexive候选地址X1':x1',其通知的方式是绑定请求中的源传输地址拷贝到绑定响应中。
4 连接性检查
一旦L侧agent采集到它所有的候选地址后,它会把这些地址重新按照从最高到最低的排序,然后通过信令通道发送给R侧agent。这些候选地址通过SDP的offer消息的属性参数发送到R侧agent(开始使用offer/answer交互模式)。当R侧agent收到offer消息后,R侧的agent也会执行一个同样的流程来采集候选地址,然后通过响应消息返回自己的候选地址。双方采集发送流程完成以后,每个agent都有自己的完整的候选地址和对待peer的完整候选地址。通过双方候选地址的配对处理,最后产生一个候选配对。候选地址配对产生以后,agent首先需要知道哪个候选地址配对是可以工作的,每个agent会按时设定一系列的检查。每个检查是一个STUN请求响应的事务,每个终端都会执行具体的候选地址配对流程,配对流程检查通过本地候选地址对远端候选地址发送一个STUN请求。连接性检查(Connectivity Checks)的基本原理非常简单:
对候选地址配对进行优先级排序
按照优先级排序顺序对候选配对发送检查请求
确认从其他agent收到检查状态
因此,在执行候选配对检查时需要一个四次握手的处理:

这里一定要注意,发送STUN请求的目的地和接收源的IP地址和端口,这是完全相同的IP地址和端口,使用此IP地址和端口来传输媒体流(包括RTP和RTCP)。因此,agent会通过数据内容多路分解STUN/RTP/RTCP相关数据,而不使用其接收端口来分解STUN/RTP/RTCP数据。相对于使用端口的方式来说,多路分解方式会更容易处理检查状态,特别是针对RTP和RTCP的数据。因为,STUN绑定请求是用来执行连接检查的,在STUN响应中包含了agent的已转译的传输地址,这个地址是agent和对端peer之间的NAT公网侧地址。如果这个传输地址和agent已学习过的候选地址不同的话,agent就会知道这个候选地址是一个新的地址(PEER REFLEXIVE CANDIDATE),这个新地址和其他候选地址一样,也是ICE测试过的地址。
作为一种优化方式,只要R侧获得了L侧的检查消息,R侧会在同一候选配对中按时对L侧发送一个连接检查消息。这样的话,ICE就会加快发现有效候选流程处理时间,这个过程也称之为“TRIGGERED CHECK”。在双方握手完成以后,双方都知道对对端可以接收或者发送端对端消息。到此为止,连接检查结束。
5 候选地址排序
根据连接检查的介绍,读者可能已经注意到了,前面我们讨论的候选配对查询算法还有一定的问题,假设候选配对存在的话,无论查询方式是何种顺序,查询流程会一直查询直到找到这一对候选配对。显然,查询的顺序肯定影响候选配对的查询结果。因此,为了快速生成候选配对结果,候选地址需要经过排序处理,最后,排序后的候选配对结果就是一个check list。关于排序算法笔者在发送初始offer的章节进行讨论。基本上,排序算法需要遵守两个基本原则:
- 每个agent给它的候选地址一个优先级数字标识,此优先级标识会随候选地址发送到对端peer。
- 本地和远端优先级合并,每个agent针对候选配对来说有一个同样的排序。
其中,第二个原则非常重要。如果双方L侧agent和R侧agent的都在NAT后的话,为了确保ICE工作,必须特别注意第二个原则。我们经常可以看到,NAT是不会允许一台外部主机发送数据进入到NAT后的网络环境中,只有在NAT后的agent通过NAT发送数据给这台主机后才被允许交互,外部数据才能进入到NAT后的环境中。这里要注意,直到agent双方都已通过自己相关的NAT穿越发送check信息后,双向的ICE check才能最终成功。agent需要通过check list才能启动工作,所以,它需要周期性地发送一个STUN请求获得列表中的候选配对。这个处理过程称之为 “ORDINARY CHECKS”。
通常情况下,优先级算法的设计是为了同类候选地址直接获得同样的优先级,和一些非直接处理的方式相比,优先级算法在处理候选配对是可能更加高效。因此,通过优先级算法的处理流程可以快速高效地实现直接路由访问,路由处理路径中仅需要几个媒体转发和几个NAT转发。如果采用非直接处理的方式的话,候选配对可能需要更多媒体转发和NAT转发,经过越多的转发就会导致越多的不可控因素。所以,这样的方式不是一种好的推荐的方式。但是,agent都有自己的决定权来优化算法。
6 锁定候选配对
前面的讨论中,我们仅仅描述了一种场景,那就是agents想使用一个组件模块(COMPONENT)创建一个媒体会话。这里,COMPONENT是一个媒体流。在典型的应用环境中,agents实际需要创建一个连接来支持更多的数据流程。网络环境中的很多属性和组件具有非常大的相似度。通常情况下,如果一个组件模块可以工作的话,我们会借用同样的信息来决定最近的候选地址。ICE使用这样的处理方式来处理候选配对流程,这种算法简称为frozen candidates或者锁定候选算法。每个候选地址都和自己的一个属性相关联,我们称这个属性为“FOUNDATION”。如果它们被认为是“相似”的话,我们就会认为他们具有相同的FOUNDATION属性,这表示它们有相同的类型,并且都是从同一host candidate,同样的STUN服务器,使用同样的协议获得的类型数据。否则,这样的候选地址就是不同的。
同样的道理,候选配对也有FOUNDATION属性。这个候选配对的FOUNDATION就是通过合并候选地址的FOUNDATION构成。在初始阶段,只有针对FOUNDATION是唯一状态的候选配对进行测试。其他的候选配对会被标识为“frozen” 或者锁定状态。当候选配对的连接检查是成功状态的话,其他带相同FOUNDATION属性的候选配对被unfrozen或者解封。通过这样的方式就可以避免了重复的连接性检查,增加检查的效率。但是,事实上,这种方式也有失败的可能。frozen candidates是ICE处理流程的一部分,ICE有优先级的算法会自动使用正确的顺序确保正确的候选地址被解封。
这里笔者再花费一点时间再一点补充说明。在笔者接触到的资源中,很多关于WebRTC/ICE的技术讨论,以及相关的官方中,包括RFC5245本身也没有非常完整地解释清楚“frozen”的定义。这里,笔者试图借用一定的示例来进一步明确说明frozen candidates的具体含义。首先,我们来解释一下在我们讨论的场景中具体的frozen定义。如果我们单纯从字面意思,笔者认为可以理解frozen为封冻或者锁定的含义。接下来读者就容易理解frozen candidates的完整含义。从前面的解释,我们可以看到frozen candidates就是一种候选地址的状态。

每个在check list中的候选配对都有自己的状态,具体的状态处理经过以下几个步骤:
- Frozen – 处于锁定状态,等待检查。
- Waiting – 从check list中选择最高优先级的候选配对进行处理。
- In-Progress – 为这个配对发送check请求,事务在处理流程中。
- Succeeded – 配对检查成功
- Failed – 配对检查失败
7 安全检查
大家知道,ICE的目的是发现两个agent之间的地址,通过这个地址来发送媒体流数据。当然,ICE必须确保这个地址不能出现安全问题,也必须避免把媒体数据发送到错误的地址。每个STUN的连接性检查都要经过消息认证码(MAC)的计算,消息认证码在信令通道中使用密钥交换的方式进行计算。因为MAC提供了消息完整性和数据原始认证,因此它可以杜绝攻击者伪造和修改连接性检查消息内容。一个比较典型的例子就是SIP的分叉呼叫处理。如果SIP呼叫用户正在使用ICE,并且此呼叫进行了分叉呼叫处理的话,ICE处理就会在每个独立的接收方之间进行。这种使用场景中,在信令通道中的密钥交换帮助每个ICE交换和其分叉的接收方进行交换处理。如果读者不了解分叉呼叫的内容,可以查阅本微信号的历史文章-关于分叉呼叫流程的处理。
8 完成ICE创建
通过前面的介绍,我们知道ICE检查是需要按照顺序处理的。ICE首先处理优先级最高的候选配对,然后再处理低优先级的候选配对。只要每个模块的媒体流检查都成功以后,ICE通过声明一个“成功”表示候选配对成功,整个ICE的创建就完成了。这种操作方式确实也是比较合理的,我们会在后续的文章中介绍具体的算法。但是,任何事情都有其两面性,通过这种按照优先级高低来执行候选配对检查的算法需要考虑一定的风险。如果最高优先级的候选配对产生了数据丢失的问题,这样就会耗费了稍长时间完成配对检查。这种情况下,ICE可能需要花费稍长时间,但是可能会产生比较好的结果。从根本上来说,RFC5245官方规定的优先级算法不一定会产生“优化”的结果。如果其算法的目的是选择低时延的媒体路径的话,使用了转发(转发可能是高延时)作为一种建议提示的话,这种转发建议实际上就没有多大关系。实际场景中可能使用往返时延(RTT)的衡量指标的方式可能比优先级的处理方式更好,在演示中场景中,低优先级的候选配可能比高优先级的候选配对取得更低的时延。
标识成功配对以后,接下来,ICE就会指定agent的功能角色。其中一个agent称之为CONTROLLING AGENT(主控agent),另外一个agnet是CONTROLLED AGENT(被控agent)。主控agent就会从有效的候选配对中指定一个配对来应对媒体处理。主控agent通过两种方式来实现指定候选配对:使用regular nomination或者使用aggressive nomination的方式来指定候选配对。

来自于互联网资源使用regular nomination正常指定候选配对的话,主控方agent会一直发送请求消息,让check处理进行处理,直到找到至少一个有效的候选配对可以支持媒体。然后选择其有效的候选配对。接下来主控agent仍然会继续发送第二个STUN请求消息,但是,在第二个STUN请求中,主控agent会附加一个flag,通知对端,主控agent已经指定了一个候选配对,将要使用那个候选配对来处理媒体流。当携带flag的这个STUN事务完成以后,双方都取消了进一步的check流程。ICE将使用最终指定的那个有效配对发送媒体。ICE正在使用的这对候选配对称之为“SELECTED PAIR”,成为已选配对。所以,读者一定要明白,ICE是真正使用selected pair来进行媒体处理的,前面所有的候选配对都是为ICE最后选择配对进行准备。
上面,笔者介绍了正常指定配对的方式,这里我们再介绍一下aggressive nomination的使用方式。aggressive nomination的方式相对比较激进或主动一点。主控agent使用aggressive nomination时,主控agent会在每个STUN请求中附加一个flag,一旦找到第一个成功的配对的话,ICE就会结束其他的检查流程,然后使用其配对处理媒体,主控agent不会再发送第二个STUN请求。此selected pair具有最高的优先级。所以,我们通过两种方式的介绍,我们可以看出,aggressive nomination检查速度快,但是缺少灵活性;regular nomination则处理速度慢,但是可能会找到低时延的媒体路径。
一旦所有的媒体处理完成后,如果媒体中“m=”和“c=”行中的候选地址(默认候选)不能匹配selected pair(已选配对)的话,主控端会发送一个更新offer消息。如果ICE结束后,为了支持agent的媒体流,任何一方agent都可以在任何时间重新启动ICE。重新启动ICE可以通过agent发送一个更新offer的方式来处理。
9 ICE 轻量级部署讨论
为了让呼叫支持ICE,双方agent必须都使用ICE。但是,在实际环境中,某些agent本身就带了公网地址,它可以从任何通信端接收数据或者进行通信。为了让这类终端能够方便地支持ICE,ICE定义了一种特别的部署方式,称之为lite implementation,或者轻量级部署方式。当然,相对于lite implementation的支持方式,ICE支持正常的full implementation全部署方式。因为这类带国外地址的终端本身带有公网地址,所以,轻量级的部署方式会减少很多中间处理环节。首先,它不采集候选地址,它也不包含支持媒体的主机候选地址(一般的内网地址)。另外,虽然它们需要返回响应消息,但是,这类agent不会生成连接检查或运行状态机。当两个轻量级agent连接时无需发送检查请求响应消息;可是,当使用轻量级agent和全部署方式连接时,这时,全部署方式的agent会变为主控agent来控制检查流程,轻量级agent就会变为被控agent。笔者将会在后续章节或者文章中讨论这两种部署方式的具体细节。读者需要特别注意,轻量级的部署是针对全部署方式的一种补充方式,是在RFC5245中稍晚时间增加的功能支持。针对那些支持公网地址的终端来说,如果可以实现全部署方式的话,RFC5245推荐使用全部署方式。
以上都是关于ICE的热身流程,包括了基本的概念以及大概的处理流程。后续文章中,笔者将继续介绍ICE处理流程和具体细节,从真正的第一步处理流程开始-初始化offer的处理,包括初始化offer中的Full Implementation 要求和其具体步骤。
参考资料
- https://anyconnect.com/stun-turn-ice/
- https://tools.ietf.org/id/draft-ietf-ice-rfc5245bis-13.html
- https://tools.ietf.org/html/rfc8445
- https://en.wikipedia.org/wiki/Interactive_Connectivity_Establishment
- https://www.ietfjournal.org/interactive-connectivity-establishment/
- https://ietf.org/documents/144/IETF_ICE_intro_92.pdf
原文出处:完整SIP/SDP媒体协商概论-ICE初始offer发送详解
在前面的章节中,我们主要讨论了ICE概览,介绍了ICE的基本处理流程和候选地址配对的算法概论和轻量级ICE部署(Lite Implementations)的讨论。和前面介绍中讨论的SIP中offer的处理一样,在此文章中,笔者也将首先介绍ICE处理过程中初始offer的发送处理。因为轻量级的ICE部署场景不是RFC5245推荐的场景,本身协商也忽略了很多检查流程,所以笔者还是按照规范的的重点内容来讨论全部署场景(Full Implementations)中关于初始offer的处理,结尾部分将讨论轻量级ICE部署的场景和SDP解码。
1 全部署场景处理要求
在全部署场景中(Full Implementations),初始offer的处理大概需要经过五个主要的步骤:候选地址获取->候选地址优先级处理->移除冗余候选地址->选择默认候选地址,最后计算发送SDP offer。下面,笔者将会一步步介绍其中最主要的前四个步骤。
首先,笔者介绍一下候选地址获取或者采集(Gathering Candidates)。在日常生活环境中,如果我们计划出去旅游的话,我们一般都会提前准备旅游线路的一些背景资料,例如交通信息,旅游资料方便我们找到最佳的旅游路线,在最低成本的前提下获得最佳的旅游体验。同样,候选地址获取也是一样的。当agent之间的通信即将开始时,agent需要获取候选地址。agent或者offer提供方可以通过操作界面的提示或外部的初始请求来发起一个offer消息。每个候选地址就是一个传输地址,它也包括其类型和base。候选地址的base实际上也是一种候选地址(一个基准地址),当agent使用候选地址时,agent必须从此地址发送。在RFC5245中,规范定义了四种候选地址类型,它们分别是host candidates,server reflexive candidates,peer reflexive candidates和relayed candidates。其中,server reflexive candidates是通过STUN或者TURN服务器获得,relayed candidates是通过TURN服务器获得,peer reflexive candidates则是通过ICE协商过程中获得(它事实上是一种连接性检查的结果,不做讨论)。关于以上四种类型的定义,笔者在前面很多历史文章中结合一些图例有一些介绍,这里为了能够更好配合SDP的介绍,保持关于SDP内容讨论的完整性,笔者再花费一点时间重新梳理一次前三种候选地址类型。
Host candidates(主机候选地址)是首先需要获取的地址。主机地址是通过端口和IP地址绑定获得。这里的IP地址可以此主机所属的物理网口IP地址,虚拟地址或者VPN地址。agent想调用UDP媒体流的话(也可以通过拓展支持TCP),agent应该获得本地主机支持的一个候选地址,这个候选地址是针对每个媒体流构件的,这些媒体流通过主机所支持的IP地址来传输。agent通过绑定一个具体的IP地址和端口来获得每个候选地址。每个媒体构件有一个component ID,基于RTP的媒体流,它本身就有一个ID,这个ID是1,RTCP的ID是2。如果agent使用了RTCP的话,它必须获取一个候选地址;如果agent使用RTP和RTCP的话,agent有k个IP地址的话,它必须以2*K的主机候选地址来最终地址计算方式。针对每个主机候选地址来说,base基准地址是候选地址自己本身。
Agent除了需要获得主机候选地址以外,agent应该获得Server Reflexive (反射地址)和 Relayed Candidates(转发地址)两种候选地址。读者注意,这里使用的是应该而不是需要或者必须,使用的要求取决于提供者的环境变量要求。在有一些使用场景中,ServerReflexive 和 Relayed Candidates都是和公网绑定的,因此agent在一个封闭网络环境中,或者从来不连接公网的话,没有必要使用Server Reflexive 和Relayed Candidates地址。如果agent支持了双网络协议或者支持多宿主地址的话,agent应该使用全部署方式来处理候选地址。部署TURN服务器的成本是非常高的,需要考虑实际场景。当使用ICE时,agent双方都在NAT后,它们需要一个地址和端口映射处理时,用户才需要考虑使用NAT部署。在后续部署使用过程中,可能一些用户会把使用NAT地址映射作为一种用户场景来满足agent的支持能力,因此,通常这种可选的边缘处理的用户场景仅作为一种可选方案,可能也不会经常使用。因此,如果agent不采集Server Reflexive(反射地址) 和 Relayed Candidates(转发地址)的话,RFC5245规范推荐关闭这种部署文件配置。如果将来网络环境发生了变化,agent可以通过配置开启采集地址的功能。这里再次重复一次笔者前面提到的废话。如果agent正在采集Server Reflexive(反射地址)和 Relayed Candidates(转发地址)的话,agent需要使用TURN服务器。如果agent仅采集反射地址的话,agent使用STUN服务器。部署场景决定以后,agent开始其候选配对的处理流程,agent可以通过界面配置方式或者其他的侦测手段(通常是通过GUI界面配置STUN/TURN服务器地址),通过STUN或者TURN服务器对其每个主机候选地址进行配对。如果已配置好了STUN或者TURN服务器的话,规范推荐配置一个domain名称,使用DNS处理流程来发现STUN服务器和TURN服务器地址。
这里的DNS处理流程遵从两个规范(RFC5389和RFC5766),分别实现对STURN服务器的查询和TURN服务器的查询。其中,查询STUN服务器地址的流程是通过Service Records (SRV),使用 “stun” 服务来完成的,具体的流程按照RFC 5389的DNS流程实现。查询TURN服务器地址的流程是通过SRV,使用“turn”服务来完成,具体的流程是按照RFC5766规范DNS流程实现。在具体的应用场景中,agent可能会通过学习从SRV查询中获得多个STUN或者TURN地址。根据RFC5245规范的说明,为了提升ICE的处理性能,在一个具体的会话中,对于所有候选地址来说,agent应该仅使用单个STUN或者TURN地址。查询的结果就是通过STUN或TURN服务器的一系列主机候选配对。agent然后从一系列配对中选择一个配对,从主机候选地址对STUN或者TRUN服务器端发送绑定或分配请求。特别注意,对STUN服务器来说,发送绑定请求是不需要签权的,响应中任何服务器属性(ALTERNATESERVER)都会被忽略掉,同时agent必须支持向后兼容的模式支持绑定请求(Binding request),此绑定请求在RFC5389中说明。分配请求(Allocate requests)应该需要通过签权认证流程,agent端可以设定一定的安全机制来实现签权流程。
在ICE采集候选地址阶段,ICE设置了一个定时器(Ta),每Ta毫秒后,agent就会生成一个新的STUN或TURN事务。这个事务可以是针对前一个事务的重试,在这个新事务中可以携带一定的错误消息(例如,认证错误)。这个事务也可以是一个新的事务,新的事务可支持新的主机候选配对和STUN/TURN服务器配对。为了保证其ICE采集流程的稳定性,不影响其他定时器的使用(例如,STUN重传定时器RTO的),agent不应该在每Ta毫秒内过于频繁生成新的事务。关于这两种定时器的使用方式,我们在后续章节会涉及,这里不再深入讨论。发送了绑定请求或分配请求后,agent将会收到一个绑定或分配请求的响应消息。如果收到的是一个成功的分配响应消息的话,消息中会包含Server Reflexive地址(反射地址,从映射地址中获得) 和 Relayed Candidates(转发地址)。其中,转发候选地址是在响应消息的XOR-RELAYED-ADDRESS属性设置中。如果分配请求被拒绝的话,那是因为服务器端没有可提供的资源来支持这个请求,agent应该对服务器端发送一个绑定请求来获得反射候选地址,绑定请求将会对agent提供一个反射候选地址(也是从映射地址中获得)。反射候选地址的基准地址是一个主机候选地址,这个主机候选地址是发送绑定请求和分配请求的地址。转发候选地址的基准地址是候选地址本身。对主机候选地址来说,如果转发候选地址是确认状态(很少发生的极端情况),那么,这个转发候选地址必须要丢弃。
除了采集反射候选地址和转发候选地址以外,最后,agent还要对每个候选地址分配一个foundation。Foundation是一个身份标识符,它在会话中使用。Foundation ID 用来决定两个候选地址是否相同。计算foundation目前没有特定的说法,它仅针对配对的计算,保持其唯一性。如果要保证两个候选地址具有相同foundation ID的话,以下四个条件需要都为真:
- 候选地址具有同样的类型(主机候选地址,转发候选地址,反射候选地址或者peer 反射地址)
- 它们的基准地址具有同样的IP地址(端口可以不同)
- 对反射候选地址和转发候选地址来说,通过STUN或者TRUN服务器获得这些地址,这些地址必须具有同样的IP地址
- 通过同样的传输协议(TCP/UDP,或者其他)获得候选地址
反之亦然,如果以上其中一个条件不为真的话,则说明候选地址具有不同的foundation。
采集到反射候选地址和转发地址以后,为了保证ICE的正常工作,这种绑定关系需要一定的维护机制来保证此地址的连接性。因此,反射地址和转发地址一旦分配以后,这些地址必须保持一个存活状态。在后续章节中,笔者将继续介绍关于释放候选地址话题。对于通过绑定请求学习到的反射候选地址来说,这种绑定关系必须通过对服务器端发送其他的额外请求才能继续维持。分配请求可以通过刷新事务的方式来实现,具体的处理方式,读者可以参考RFC5766-7章节。刷新请求也同样刷新了反射候选地址。
到此为止,笔者讨论了采集候选地址所关注的几个主要话题(主机候选地址,反射地址,转发地址,foundation计算和候选地址的状态维护)。接下来,笔者将继续讨论关于针对候选地址优先级排序以及其计算方式和指导原则。
为了保证ICE的传输取得最佳的性能,候选地址需要进行优先级的处理。优先级处理针对每个候选地址指定了一个优先级标识。传输媒体流的每个候选地址必须有一个唯一的优先级,此优先级必须是一个正数,取值范围在1到(2**31 - 1)之间。ICE将使用此优先级取值来决定检查连接性的依据,和对候选地址进行修改偏好的选择参数。Agent将使用计算公式来计算其优先级,计算公式所使用的参数根据规范的指导原则进行。如果agent使用不同的计算公式的话,双方agent对连接性检查流程可能出现不协调的情况,因此,ICE将会耗费更长的时间汇聚数据交互。下面,笔者将针对计算公式和具体的指导原则进行说明。

通过以上公式可以看出,使用格式计算优先级时,永久性的结果计算关联了三个主要的参数(针对每个候选地址类型的偏好,本地IP地址和component ID)。其中,每个候选地址类型的偏好取决于候选地址的类型(host candidates,server reflexive candidates,peer reflexive candidates和relayed candidates)。如果agent是一台多宿主的主机,其偏好取决于主机的IP地址。类型偏好(type preference)必须是整数,取值范围从0到126范围内,表示四种候选地址的偏好。126是最高偏好取值,0是最低偏好取值。如果对候选地址类型的偏好设置为0,则表示这个类型的候选地址将为最后的一种选择。同样类型偏好的候选地址必须要确认其相同性,不同的类型偏好的也需要确认其不同。以上四种候选地址类型的优先级也具有不同的判断级别。其中,peer reflexive candidates的类型偏好优先级必须高于其他反射候选地址类型偏好。本地偏好(local preference)必须是整数,取值范围从0到65535范围内。它表示的偏好是针对具体的IP地址的,从这个具体的IP地址获得候选地址,agent是一台多宿主主机。65535是最高偏好取值,0是最低偏好取值。当本地主机只有单个IP地址时,偏好取值应该设置为65535。通常情况下,如果针对一个具体的媒体流的特别构件支持了多个候选地址的话,此特别构件的多个候选地址具有相同的类型的话,针对每个候选地址的本地偏好必须是唯一的。在RFC 5245规范中,以上这种情况仅发生在多宿主主机环境中。因为多宿主主机是一个双栈主机,本地偏好应该等同于IP地址的优先值。关于优先值的取值读者可以参考RFC3484-2.1,因为现在很多场景中已经部署了IPv6,,因此,可能在某些场景中,IPv6的优先级高于IPv4的优先级。具体的优先值计算取决于具体的场景中。component ID是候选地址的component ID,它的取值范围必须在1到256之间。介绍完计算公式以后,笔者接下来需要讨论选择类型偏好和本地偏好的四个指导原则或标准。
第一个指导原则是选择类型偏好和本地偏好需要根据一定的标准。类型偏好和本地偏好取值是主要标准,具体来说,就是媒体的中间介质的使用,例如TURN服务器,VPN或者NAT。在使用这些媒体中间介质时,如果媒体发送到这些介质的候选地址,在收到媒体之前,媒体需要首先被发送到中间介质的候选地址。转发候选地址就是其中一种候选地址类型,它涉及了媒体中间介质。另外一种媒体中间介质就是通过VPN接口获得的主机候选地址。显然,当媒体通过媒体中间介质以后,它会增加发送和接收方之间的时延。同时,因为媒体经过了多个路由器hops,增加了丢包的风险。当然,因为媒体需要进出媒体中间介质的服务器,服务提供商也需要相应增加部署成本。如果用户觉得笔者前面说的这些因素是非常重要的,因为考虑到这些风险的重要性,转发候选地址的偏好设置就应该要低于本地偏好设置。因此,针对偏好取值的取值可能需要做一定的调整。推荐的办法是,本地候选地址偏好设置为126,反射候选地址偏好设置为100,peer reflexive candidates设置为110,转发候选地址的偏好设置为0。进一步说,如果agent是一台多宿主主机,它本身带多个IP地址的话,从VPN接口获得的主机候选地址的本地偏好设置为0。

选择偏好的第二个指导原则是基于IP组选择方式。ICE可以在IPv4和IPv6网络环境中工作。ICE提供了一种工作机制,可以保证双栈主机选择IPv6,但是万一IPv6网络出现故障时(例如,在6to4路由器转发失败-RFC3056),它也允许主机回退到IPv4的网络环境中。ICE也可以帮助主机获得原生的IPv6地址和6to4地址。如果是我们以上所说的这种情况的话,相对比较高的本地偏好将会关联到IPv6的地址,然后是6to4的地址,最后才是IPv4的地址。这样安排的目的是允许agent可以马上优先使用原生IPv6地址,如果出现连接问题或者对端agent没有启用原生IPv6时,可以退回到6to4的地址继续和对端通信。
第二个原则选择偏好的原则是基于主机IP地址的类型。第三个原则是基本上是基于第二个原则的基础上,针对网络最重要的问题(安全机制)的选择。因此,根据安全机制选择偏好也是一种非常重要的指导原则。现在国际上很多公司的员工都是远程办公(特别是WebRTC终端方式),如果远程办公员工提供家庭私人网络访问企业内部网络时,员工端希望通过企业内部网络访问语音流量,通过企业通信系统呼出到其他外部目的地。这样的话,员工私人网络和企业网络之间就需要一个VPN的连接或者SBC的连接(笔者已经介绍过很多关于SBC的使用,这里仅指VPN)。如果是这样的部署场景的话,和其他后续地址相比,VPN地址将具有比较高的本地偏好优先级。
第四个选择偏好的指导原则是关于网络拓扑意识。企业网络的拓扑设计也是选择偏好时需要关注的地方。候选地址的处理如果可以灵活地充分利用网络优势也可以优化地址的选择。对于后续地址来说,它可以利用其中间介质的多种已存网络架构实现偏好选择。在某些网络环境中,如果agent可以预设或者动态发现中间介质,这些中间代理介质可能是最佳(或最近)的路径地址包括候选地址的话,agent应该设定此候选地址的本地偏好为比较高的优先级。
前面,笔者介绍了候选地址采集和候选地址的优先级处理。接下来,我们继续介绍优先级处理以后的另外一个话题,这就是关于候选地址冗余移除的问题。为了保证agent端本身存储和管理的问题,agent会移除一些冗余的候选地址。如何判断候选地址是重复或是一个冗余地址呢?根据RFC5245的说明,如果一个候选地址的传输地址等于其他候选地址的传输地址,并且此候选地址的基准地址等于其他候选地址的基准地址,那么此候选地址就是一个冗余候选地址。另外,如果候选地址的传输地址相同,但是它们的基准地址不同,这些候选地址不是冗余候选地址。很多情况下,当agent没有在NAT背后时,反射候选地址和主机候选地址是冗余或者重复的,agent应该设置一个比较低的偏好优先级移除这些冗余候选地址。
采集到候选地址以后,接下来就需要考虑如何设置一个默认的候选地址实现媒体流的进一步传输处理。如果一个候选地址被看作是一个默认的候选地址的话,从非-ICE用户端来说,它将作为一个媒体目标。这个媒体目标称之为DEFAULT DESTINATION(默认目的地)。如果当agent和一个ICE-aware peer(能够感知到ICE的远端)通信时,如果ICE算法没有选择候选地址的话,ICE处理流程完成以后,agent要求一个updated offer/answer 来修复或者纠正SDP,这样的话,媒体默认的目的地地址将会匹配ICE已选的候选地址。当然,如果ICE选择了默认的候选地址,agent就没有必要发送更新的offer/answer。
Agent 必须选择一组候选地址,每个在使用的媒体流的每个构件的一个候选地址作为默认的候选地址。如果端口不是0的话,说明媒体流正在使用其构件端口。这个结论我们在前面的讨论中已经说明。接下来处理中,尽管a=inactive状态或者bandwidth设置为0,媒体仍然会置于使用状态。
RFC5245规范推荐选择默认的候选地址是基于候选地址的概率,具体来说,这些候选地址和对端peer已经关联在一起的概率,或者它们之间的一起工作的概率来决定。规范推荐的默认候选地址是,转发候选地址(如果有转发地址的话),反射候选地址(如果有反射候选地址的话)和最后主机候选地址。
2 轻量级部署要求
前面所讨论的都是基于全部署场景的内容。轻量级部署(Lite Implementation)相对比较简单,它仅使用了主机候选地址。轻量级部署必须为每个媒体流的每个构件分配0个或者一个IPv4的地址。轻量级部署它可能分配0个或者一个IPv6地址,但是,主机不能使用多余1个以上的IPv6地址。因为很多时候,每个媒体流的每个构件可以支持多个IPv4候选地址,如果agent支持多个IPv4地址的话,它必须从分配的候选地址中选择其中一个地址。如果主机支持的双栈地址的话,RFC5245推荐分配一个IPv4候选地址和一个全局IPv6地址。在轻量级的部署场景中,ICE不能用来动态选择一定范围内的候选地址。在完整的ICE流程中,通过连接性检查才能真正决定使用这个地址或者那个地址。因此,规范也不推荐从一个特定的网络区域内包含一个以上的候选地址。
每个构件/模块都会设定一个ID,我们称之为component ID或者模块ID。对于RTP媒体流来说,RTP自己的ID为1,RTCP为2。如果agent正在使用RTCP的话,它必须首先获得一个候选地址。
每个候选地址会设定一个foundation。两个从不同IP地址获得的两个候选地址,这两个候选地址的foundation肯定是不同的,如果从相同的IP地址获得的候选地址的话,foundation看到是相同的。这里的foundation计算方式和全部署场景的要求有所不同(同时也要求检查端口)。对于优先级的计算公式来说,对每个IP地址来说仅依靠一个简单的整数递增是不够的。另外,针对同样的媒体流,在所有候选地址中,每个候选地址必须设定一个唯一的优先级标识。优先级的计算公式如下:

在以上格式中,如果主机仅有IPv4地址的话,IP precedence设置为65535。如果主机是IPv6地址或者是双栈地址的话,IP precedence应该是RFC3484中的precedence值。 接下来,agent将会为每个媒体流的每个构件选择一个默认的候选地址。如果主机地址仅支持IPv4地址,主机将仅对每个媒体流的每个构件选择一个候选地址,因此,这个候选地址是默认地址。如果主机支持支持IPv6或者双栈地址,默认地址的选择取决于本地策略。默认候选地址的可能是一个经常用来和对端peer通信的候选地址(参考前面章节的第四种指导原则)。简单来说,默认候选地址更多倾向于使用以前和对端通信成功的候选地址。如果主机仅支持IPv6的地址,那就是全局的IPv6地址。对支持双栈的主机来说,RFC5245规范推荐使用IPv4地址作为默认候选地址。
3 SDP解码
关于SDP的解码处理流程,全局部署场景和轻量级的部署场景的流程是一样的。Agent将针对每个媒体包含一个m行来表示它将要使用这个媒体。SDP中的媒体顺序和ICE相关。ICE将首先执行连接性检查第一个m行检查,接下来媒体流会进行传输。如果有媒体流的话,首先agent将媒体流置于SDP中,然后处理最重要的媒体流。
针对每个媒体流,每个候选地址将设置一个候选地址属性。关于此属性的构建需要遵从一定的规则,具体的规则参考RFC5245-15。属性将会负责传递IP地址和端口和候选地址所使用的传输协议,另外还有配合对端工作的ICE的一些属性:priority,foundation和component ID。除了以上这些属性参数因为,属性中还提供了关于候选地址的问题排查和其他函数功能,例如,候选地址类型和相关的传输地址。
基于agent之间的STUN连接性检查中使用的认证方式是短期认证机制,具体关于认证的处理在规范RFC5389中定义。相对于短期认证方式,RFC5389还定义了长期认证的方式。短期认证设定了一个时间限定,它仅在每个时间范围内有效。长期认证是通过订阅的方式实现认证。RFC5389-10章节有非常完整的介绍,读者可以查阅此章节细节获得更多内容。短期认证的处理机制是依赖于终端和服务器端之间通过协议使用用户和密码交互的方式实现。在ICE部署场景中,终端和服务器端则通过offer/answer的模式进行交互。安全用户名称是agent用户名称,通过冒号隔离。每个agent为了发送和接收消息,它使用密码来计算消息的完整性。用户名称和用户密码的交互通过ICE的ice-ufrag和ice-pwd属性来实现。用户名称和密码是为了保证其终端的认证以外,用户名称还有另外一个重要作用。Agent为了提供了针对媒体的安全设置,用户名称提供了针对媒体流的歧义和相关性检查。关于STUN用户名称的重要性的讨论,读者可以查阅RFC5245-B.4附录内容。
如果agent是一个轻量级的部署终端的话,agent必须在SDP中包含一个“ice-lite”的会话级属性。如果agent是全场景部署的终端的话,则一定不要包含此属性。
在SDP中添加默认的候选地址作为一个默认媒体目的地地址。RTP和RTCP的添加方式有所不同。如果媒体是基于RTP的媒体的话,把RTP候选地址中的IP地址和端口存入SDP中的c行和m行来完成。如果agent使用了RTCP的话,它必须使用a=rtcp属性对RTCP候选地址进行解码。具体RTCP的解码参考RFC3605-2.1章节。如果agent没有使用RTCP的话,agent必须说明未使用RTCP,具体的说明方式是通过b=RS:0和b=RR:0来表示。此SDP拓展是在RFC3556-2章节中声明。
当agent和一个非ICE端进行通信时,传输地址将是默认媒体目的地地址的话,这个传输地址必须作为候选地址出现在SDP中,可以以一个或者多个a=candidate行来表示。
ICE可提供拓展支持,拓展实现方式可通过在offer或answer中包含一系列的令牌来实现,agent使用其令牌可以确认ICE的拓展。具体来说,如果agent支持ICE拓展,它必须包含一个令牌,使用此令牌定义这个拓展,令牌定义在SDP中使用ice-options选项属性。以下是一个具体的包含ICE消息的SDP示例:
v=0
o=jdoe 2890844526 2890842807 IN IP4 10.0.1.1
s=
c=IN IP4 192.0.2.3
t=0 0
a=ice-pwd:asd88fgpdd777uzjYhagZg
a=ice-ufrag:8hhY // agnet 用户名称
m=audio 45664 RTP/AVP 0
b=RS:0 // 使用全部署场景
b=RR:0
a=rtpmap:0 PCMU/8000
a=candidate:1 1 UDP 2130706431 10.0.1.1 8998 typ host
a=candidate:2 1 UDP 1694498815 192.0.2.3 45664 typ srflx raddr
10.0.1.1 rport 8998
一旦agent已发送了offer或者answer消息,它必须准备接收在每个候选地址上的STUN和媒体数据包。在全部署场景和轻量级部署场景中,媒体发送的流程有一定区别,更多关于不同部署场景中发送媒体包的讨论将在后续文章中做介绍。媒体包可被发送到一个候选地址中,此候选地址是在以前的offer或answer消息中出现的媒体默认目的地地址。
笔者通过以上章节的内容,重点介绍了发送初始化offer的一些流程,主要包括三个部分的内容,其中包括了全部署场景中的要求(采集候选地址,候选地址排序,移除冗余候选地址,选择默认候选地址),轻量级部署要求,SDP解码。
在下一个章节中,笔者将介绍接收初始化offer的处理流程。
参考资料
- https://tools.ietf.org/html/draft-ietf-mmusic-ice-sip-sdp-39
- https://www.rfc-editor.org/rfc/rfc5245
- https://www.rfc-editor.org/rfc/rfc8445
- https://www.rfc-editor.org/rfc/rfc3264
原文出处:完整SIP/SDP媒体协商概论-ICE初始offer接收详解
在前面的章节中,笔者具体讨论了关于发送初始offer的细节。这里,我们将讨论接收初始offer的一些具体内容。关于接收初始offer的流程主要包括几个部分的讨论:验证ICE支持,决定主控/被控方角色,采集候选地址,候选地址优先级排序,选择默认的候选地址,SDP解码, 检查列表的构建和定时检查。其中,在接收offer的内容中,候选地址采集,候选地址排序,选择默认候选地址和SDP解码和前面关于发送offer的流程非常相似,因此,在本章节中,这些内容的介绍可能相对比较简洁,笔者将花费更多时间在验证ICE,决定接收角色,检查列表构建和定时检查的讨论中。下面,我们将根据agent接收offer的处理步骤,开始讨论这些具体的处理流程。
1 验证ICE支持能力
Agent收到初始offer以后,首先,它需要验证ICE的支持能力。对于每个在SDP中收到的媒体流来说,如果媒体流的每个构件的默认目的地地址出现在了候选地址属性中,agent将会根据RFC5245规范中ICE的处理流程来进行处理。如果我们进一步做具体理解的话,我们可以参考RTP的处理方式来说明。例如,使用RTP时,在c行和m=行的IP地址和端口出现在了候选地址属性参数中;使用RTCP时,RTCP值也会出现在候选地址属性中。如果前面所说的这个条件不成立,或者这些媒体构件没有出现在候选地址属性中的话,agent必须按照另外一个流程来处理SDP(RFC3264)值,而无需按照ICE的处理机制来处理。这个流程就是我们在前面文章中所介绍的关于SIP针对offer/answer交互模式来处理。读者可以查看历史文档来回顾笔者介绍的相关基础内容。如果按照RFC3264处理的话,读者也需要注意几个例外条件:
- 所有agent必须遵从RFC5245-10中的会话存活保持流程。
- 如果agent没有根据RFC5245 ICE的处理流程处理的话,是因为有a=候选地址属性,但是这些a=候选属性没有匹配媒体流的默认目的地地址,agent在其answer消息中必须包括一个a=icemismatch属性(无匹配)。
- 如果默认候选地址是一个从TURN服务器学习获得的转发地址,agent必须在TRUN服务器端创建一个授权许可,这个许可支持SDP中收到的,从对端的peer学习到的IP地址。大家需要注意,如果权限设置有问题的话,对端发过来的初始数据包将会丢失。
2 决定主控/被控制方角色
在上一篇文章中,笔者介绍了主控agent和被控方agent的角色。在会话中,每个agent需要充当一个角色来执行不同的操作流程。主控方负责选择最终候选配对和被控方进行通信,如果是全部署环境下的agent,这表示ICE使用挑选的候选配对对每个媒体流进行传输,并且,当agent需要时,可基于ICE的选择生成更新的offer消息。如果是轻量级的部署环境中的agnet,选定为主控方agent表示选择了候选地址配对,这个候选配对是基于offer和answer消息中的配对(在IPv4中仅有一对),并且,当agent需要时,生成一个更新的offer消息来反映这个选择。被控方agent被告知使用的候选配对来传输媒体流,并且针对这个单个信息不会生成一个更新offer。下面,笔者将讨论决定双方角色的规则和处理流程对双方的的影响。关于决定双方角色的规则和其流程的影响,事实上,这取决于双方agent所处的部署环境,这里有三种不同的部署环境需要讨论:双方都在全部署场景,双方各自在全部署场景/轻量级部署场景,双方都在轻量级部署场景。
双方都在全部署场景中,如果一个agent生成了一个offer消息,并且启动了ICE处理流程,这个agent必须扮演一个主控方agent的角色。另外一侧agent则必须扮演被控方角色。双方agent将会构建检查列表,运行ICE状态机和连接检查的流程。主控方将会根据全部署场景流程执行处理逻辑,挑选候选配,ICE根据这些候选配对提供进一步的选择,最后双方agent通过更新offer来更新或者结束ICE处理流程。当然,在实际部署场景中,不排除一些特殊使用环境,例如因为其他通信因素,双方的角色认定发生冲突。双方agnet错误地认为自己是主控方或者自己是被控方。因此,为了避免类似情况的发送,每个agent必须选择一个任意号码,RFC5245规范称之为tie-breaker,在连接检查中使用此数值检测修复这种情况,其取值范围一律发布在0和(2**64) -1之间(它是一个64位的正整数)。
双方一方是全部署场景agent,另外一方是轻量级部署场景agent中,全部署场景的agent必须扮演主控方agent的角色,轻量级agent则必须扮演被控方agent的角色。全部署agent将会构建检查列表,运行ICE状态机和生成连接检查。主控方将会根据全部署场景流程执行处理逻辑,挑选候选配对,ICE根据这些候选配对提供进一步的选择。轻量级agent将会监听连接检查,接收响应检查消息,按照轻量级部署的结束ICE处理流程,最后对ICE进行结束处理。因为双方的角色不同,从某种程度来说,主控角色一般都是一直处于运行状态。因此,对轻量级部署agent来说,针对每个媒体流来说,ICE处理状态被认为是运行状态,所有ICE流程也是运行状态。
Agent双方都是轻量级部署的agent的话,如果一个agent生成了offer消息,并且启动了ICE处理流程,这个agent必须扮演一个主控方agent的角色。另外一侧agent则必须扮演被控方角色。这种环境中,双方从来都不会发送连接检查。准确地说,一旦双方的offer/answer交互模式完成以后,每个agent将执行ICE结束处理流程,它们无需经过连接检查的流程。和第一种场景中所描述的一样,同样的角色冲突问题也可能出现在双方都是轻量级部署agent的环境中。它们都可能认为自己是主控方agent或者被控方agent。这种情况下的处理方式和全场景部署中的角色决定的处理方式不同。轻量级部署环境中角色冲突时,双方agent通过在信令中承载的offer/answer交互,和交互消息中所支持的检测能力来确定双方角色。对轻量级部署agent来说,针对每个媒体流来说,ICE处理状态被认为是运行状态,所有ICE流程也是运行状态。
在会话中,一旦双方角色确定以后,除非ICE重新启动,否则,它们将一直持续充当各自的角色。补充说明,因为ICE重新启动以后,双方需要重新决定各自的角色,如果是全部署场景agent的话,它们需要重新对tie-breaker赋值计算。
3 候选地址采集
关于针对候选地址的采集处理流程,answerer应答方和offerer提供方的处理方式是完全一样的。笔者在前面的文章中已经非常详细地做出了说明。用户可以阅读此文章来了解完整SIP/SDP媒体协商概论-ICE初始offer发送详解。根据RFC5245的推荐,提供方收到offer,早于对用户提醒之前马上执行采集流程。当agent启动时,这样的候选地址采集方式就可能开始。
4 候选地址优先级排序
关于针对候选地址的排序处理流程,answerer应答方和offerer提供方的处理方式是完全一样的。笔者在前面的文章中已经非常详细地做出了说明。用户可以阅读此文章来了解完整SIP/SDP媒体协商概论-ICE初始offer发送详解。
5 默认候选地址选择
关于针对候选地址的默认候选地址选择的处理流程,answerer应答方和offerer提供方的处理方式是完全一样的。笔者在前面的文章中已经非常详细地做出了说明。用户可以阅读此文章来了解完整SIP/SDP媒体协商概论-ICE初始offer发送详解。
6 SDP解码
关于针对SDP解码的处理流程,answerer应答方和offerer提供方的处理方式是完全一样的。笔者在前面的文章中针对全部署场景和轻量级部署场景的规定已经非常详细地做出了说明。用户可以阅读此文章来了解完整SIP/SDP媒体协商概论-ICE初始offer发送详解。
7 检查列表构建
构建检查列表是由全部署场景来实现的,如果是轻量级的部署场景,无需构建检查列表。因为offer/answer交互模式的使用,在媒体使用的过程中需要一个检查列表。为了对媒体流构建一个检查列表,agent需要经过几个必要的步骤来构建检查列表,agent需要经过的步骤是:构建候选地址配对,计算候选配对优先级和排序,优选配对,最后计算配对状态。接下来,笔者将分别讨论这四个主要的步骤。
首先,为了实现对媒体流的支持,agent选择自己本地候选地址和从对端peer收到的候选地址进行配对处理。这里,本地候选地址称之为LOCAL CANDIDATES,远端的候选地址称之为REMOTE CANDIDATES。选择过程中,agent同时还要考虑安全的问题,为了防止选择的地址被攻击,agent可以设置从offer或answer中接收候选地址的数量。关于STUN被攻击的可能性讨论,请读者参考RFC5245-18,后期文章中我们将讨论这个话题,现在不做讨论。如果本地候选地址和远端候选地址配对成功的话,它们必须具有相同的component ID,和同样的IP地址版本。当然,实际环境中,本地候选地址和远端候选地址也完全可能存在不匹配或者匹配不成功的可能,或者,远端候选地址和本地候选地址匹配不成功的可能。有时也可能发生这样的问题,例如,针对一个媒体流来说,agent没有包含候选地址来支持此媒体流的所有构件模块。如果是这样的情况的话,此媒体流的构件数量就会受到影响而减少,并且会认为这个数量等于双方agent所要求的构件最低数量,此最低数量值针对媒体流的所有component构件,并且来源于双方agent所提供的最大component ID(关于ID取值范围读者可参考前面的介绍和历史文档)。
除了上面所介绍的一些情况以外,还有几个比较特殊的情况和读者说明。在涉及到RTP/RTCP的使用场景中有可能发生这样的情况,一方agent提供了RTCP的候选地址,但是对端可能没有提供类似地址。有时,offer消息提供方可在同一端口多路复用RTP/RTCP数据,并且通过SDP属性中指示了这样的实现方式(多路复用RTP/RTCP参考RFC5761-5和RFC8035)。但是,如果answerer执行了多路复用RTP/RTCP的话,offerer则不知道answerer执行了这样的流程,offerer就会按照默认的设置方式使用各自独立的RTP和RTCP,这样处理的结果就会导致每个媒体流中在offer消息中包含两个components。answerer方执行了多路复用RTP/RTCP,对每个候选地址来说,它将仅包含单个component。因此,此component将是RTP/RTCP mux的合并值。如果此候选地址只有单个component的话,ICE结束执行候选地址配对。毫无疑问,如果配对中本地候选地址和远端候选地址都是默认候选地址时,这个候选配对被认为是默认的候选配对。
如果agent双方不是ICE感知的agent的话,媒体流构件使用此默认候选配对传输媒体流。读者可以参考以下示例来理解check list(检查列表)核心概念和与其他模块之间的关系。

构建候选地址配对完成以后,需要进行后续地址配对的优先级计算。优先级计算是根据以下格式来计算的。其中,G表示主控方agent提供的候选优先级,D表示被控制方提供的候选地址优先级。这里,G>D?1:0是一个表达式,如果G大于D,则取值为1,否则为0。
pair priority = 2 ^ 32 *MIN(G,D) + 2 *MAX(G,D) + (G > D?1:0)
关于以上优先级的计算,很多开源项目有类似的处理方式,读者可以参考一些开源项目来做进一步了解。以下一段代码是一个配对计算源代码示例,读者可以参考:
// PairPriority computes Pair Priority as in RFC 8445 func PairPriority(controlling, controlled int) int64 {
var (
g = int64(controlling)
d=int64(controlled) )
// pair priority = 2^32*MIN(G,D) + 2*MAX(G,D) + (G>D?1:0) v := (1<<32)*min(g, d) + 2*max(g, d)
if g > d {
v++ }
return v }
https://github.com/gortc/ice/blob/master/pair.go
一旦优先级被设定以后,agent就会按照顺序对候选地址配对进行排序。排序的规则按照优先级顺序递减的方式进行。如果两组候选地址配对有相同的优先级,它们两组配对的实现可以任意排序。
获得了候选地址配对排序以后,此优选候选地址会生成一个排序后的配对列表。ICE将会按照排序列表逐一进行连接检查。在每个检查流程将会涉及发送请求的流程,agent需要从本地候选地址发送检查请求到远端候选地址。这里注意,因为agent不能直接从反射地址发送请求,它仅能从base基准地址发送请求,所以agent需要通过排序后的配对列表来发送请求。对每个配对来说,如果本地候选地址是一个反射地址的话,base基准地址必须替换这个反射候选地址。一旦替换流程完成后,agent必须过滤或筛选此列表。过滤配对列表的流程通过移除配对的方式来实现。具体来说,如果它(需要移除的配对)的本地候选地址/远端候选地址和优先级列表中较高优先级的一对配对相同的话,则需要移除配对相同的较低优先级的配对。
另外,为了安全的考虑,防止STUN服务器被攻击,agent必须限制连接检查数量,在一定的数值设置环境中,agent的检查将会覆盖所有连接列表的地址,这个特定数值必须是可配置的数值。规范RFC5245推荐默认数值是100。候选配对列表一直保持低于100的限定设置,一些低优先级的候选配对将被强制丢弃。在可能的情况下,RFC5245推荐尽量使用比较低的设置限定,在实际生产环境中也可能看到一些用户针对配对检查设置了最大检查限定。要求支持可配置的限定设置也是为系统提供了一个工具,如果发现问题以后,可以通过此限定值来排查问题。
完成了候选配对地址的挑选以后,检查列表需要进行状态计算。每个候选配对支持了一个foundation和一个state(状态)。实际上,这里的foundation是合并了本地候选地址的foundation和远端的候选地址的fundation而生成的一个新的fundation。一旦开始计算foundation时,每个配对将会设定一个状态值,根据检查结果的不同,候选配对需要经过五个可能或潜在的状态计算(完整SIP/SDP媒体协商概论-WebRTC/ICE概览)。

ICE开始运行时,一个候选配对将迁移到以下任何一种状态。笔者再次重复一次每个状态的任务然后介绍具体流程处理:
- Frozen:处于锁定状态,等待检查
- Waiting:检查还没有启动,等待从check list中选择最高优先级的候选配对进行处理
- In-Progress:为这个配对发送check请求,事务在处理流程中
- Succeeded:配对检查成功,生成成功结果
- Failed:配对检查失败,既没有生成任何响应也没有生成任何还原响应
在检查列表中的每个候选配对的初始状态需要经过一个状态计算。其状态计算需要按序经过以下几个步骤:
- agent将会把每个检查列表中所有的候选配对设置为Frozen 封冻状态。
- agent将会为第一个媒体流查询检查列表(第一个媒体流出现在SDP offer和answer中的第一个m行中)。然后针对此媒体流做状态设定处理。对所有具有同样foundation的配对,agent将会设置一个比较低级别component ID的配对状态,设定这些配对进入等待状态。如果有多个类似这样的配对,agent则首先使用具有最高优先级的配对。
这里有两个特定的列表称谓需要读者注意。其中检查列表中的一部分配对会进入等待状态,另外一部分则进入到封冻状态。如果检查列表中至少有一对配对是在等待状态的,这样的列表称之为活动检查列表。如果检查列表中所有配对都是封冻状态的,这样的列表称之为封冻检查列表。
除了检查列表中的配对检查有状态存在,检查列表自己本身也关联一个状态,针对正在工作的媒体流,这个状态用来捕捉ICE检查的状态。检查列表具有三种状态:
- 运行状态:针对正在运行的媒体流,ICE检查状态也在运行中。
- 完成状态:在这个状态下,ICE检查已经生成了一个经过挑选的配对支持媒体流构件模块。接下来,ICE成功完成处理任务,开始发送媒体流。
- 失败状态:在这个状态下,针对此媒体流的支持,ICE检查还没有成功。
作为一个offer/answer交互的结果,检查列表首先构建的就会被ICE迁移到运行状态中。ICE处理流程覆盖所有的媒体流过程中,因此,ICE也和这个流程本身有一个关联绑定的状态。当ICE运行时,这个状态等于运行状态。当ICE处理流程完成后,这个状态将是完成状态,如果ICE处理流程失败的话,这个状态就是失败状态。关于以上几个状态之间切换的规则笔者将在定时检查的讨论中做更多说明。
8 定时检查
前面我们一直在讨论关于check的流程,check是由全部署场景生成的,所以,如果agent是轻量级的部署方式的话,可以跳过此内容讨论。Agent执行两种check,一种是ordinary checks,另外一种是triggered checks。两种check都是由一个定时器来控制,针对媒体流数据,定时器会周期性地触发check生成事件。
除了定时器以外,agent也会维护一个先进先出的队列,这个队列称之为triggered check队列,队列维护候选配对,如果有check的机会的话,这个队列将会发送配对进行检查。当定时器触发以后,agent将会从triggered checks队列顶部移除一个配对,对这个配对执行连接检查,最后把这对候选配对的状态设置为正在处理的状态(In-Progress)。如果在triggered checks队列中没有配对的话,agent将会发送ordinary checks。
一旦完成构建检测列表计算的流程,agent将会针对每个active check list设置一个定时器。每Ta*N秒触发一次定时器,这里的N是active check list的数量(初始阶段,至少有一个active check list)。Ta和RTO这两个定时器会在未来的讨论中加以介绍,这里不再展开讨论。Ta乘以N将允许整个check的吞吐量发布到所有active check list中。在实际部署环境中,这个定时器触发的频率可以低于上面的设置,同时,也应该考虑定时器传播的问题,尽量不要同时对每个媒体发布定时器。第一个定时器马上触发后,agent在这一瞬间(offer/answer交互模式已完成)执行连接检查,然后在Ta秒后执行下一个检查(因为在第一个定时器触发时只有一个active check list)。
如果定时器触发以后,agent没有triggered checks需要发送的话,它必须按照以下规则选择一个ordinary checks:
- 找到在等待状态的check list中最高优先级的配对(注意以下两种状态下配对处理流程)。
- 如果有这样的配对的话:首先,从本地候选配对发送一个STUN check请求到远端候选配对。此STUN check请求将会进行相关处理。关于STUN check请求的处理流程,笔者在后续文章中介绍。然后对候选配对的状态进行设置,设置其状态为In-Progress状态。
- 如果没有这样的配对的话:agent需要找到在封冻状态的check list中最高优先级的配对。如果有这样的配对的话,对此配对解除封冻状态,然后对此配对执行check流程,使其状态切换为In-Progress状态。如果没有这样的配对的话,结束针对此check list的定时器。
配对流程完成以后,需要保证其检查数据的完整性。如果要计算check消息的完整性,agent需要使用远端用户名称和密码来完成计算。远端用户名称和密码是agent学习远端peer发送的SDP中的用户名称和密码获得。
在接下来的章节中,笔者将继续分享初始应答接收的话题(验证ICE支持,决定角色等话题)。
参考资料
- https://www.rfc-editor.org/rfc/rfc5245
- https://www.rfc-editor.org/rfc/rfc8445
- https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidatePairStats/state
- https://github.com/gortc/ice/blob/master/pair.go
此文章将讨论agent的应答接收流程。agent收到应答从远端peer以后,它需要按照一定的流程来进行处理。具体的处理流程包括验证ICE支持能力,决定双方的 角色,构建检查列表,最后执行ordinary checks。
特别说明,如果使用SIP协议进行协商的话,当发生SIP分叉处理时,一个单个offer可能会生成多个answer消息。在这种情况下,ICE针对每个answer进行完全独立并行处理,把这个offer和其每个answer的合并体看作一个独立的offer/answer交互模式。合并的offer/answer交互模式生成自己独立的候选配对,检查列表,状态计算等属性。其中,当释放候选地址时,这些独立的answer可能会和其他answer产生相互影响。关于这个影响的处理流程,读者可参考RFC5245-8。
下面,笔者将会按照初始应答的接收流程来一步步介绍这些具体的细节。
1 验证ICE支持能力
应答方的ICE支持能力的验证和前面笔者介绍的offer中的验证流程基本一样,一个另外就是offerer提供方从来不会在SDP中生成一个a=ice-mismatch属性。
在一些使用场景中,answer可能忽略媒体流中的a=candidate属性,但是会包含a=ice-mismatch属性,此属性用来支持一个或多个媒体流。如果是这样的设置的话,对offerer来说,answerer提供ICE支持,但是在此会话中并没有使用ICE处理流程。会话中没有使用ICE处理的原因是针对媒体构件来说,因为中间信令修改了默认目的地地址,但是没有修改相应的候选地址属性。这样的情况是完全可能发生的,可能会导致安全问题。这里涉及了关于ICE的安全问题,笔者在未来文章中会进行讨论,这里不再介绍。另外,如果类似这样的场景发生的话,RFC5245并没有提供一个明确的指导说明来说明agent如何处理这样的失败场景。
2 决定主控/被控方角色
关于应答中角色决定的处理流程,读者可以参考完整SIP/SDP媒体协商概论-ICE初始offer接收详解,流程完全一致,无特别说明。
3 构建检查列表
构建检查列表也是针对全场景部署agent来定义的,关于应答中构建检查列表的处理流程,读者可以参考完整SIP/SDP媒体协商概论-ICE初始offer接收详解,流程全一致,无特别说明。
4 执行 ordinary checks
执行ordinary check的流程也是针对全场景部署agent来说的,读者可以参考完整SIP/SDP媒体协商概论-ICE初始offer接收详解,流程完全一致,无特别说明。
在接下来章节,笔者将讨论关于连接性检查的具体细节,包括STUN客户端流程流程和服务器端流程。
参考资料
- https://www.rfc-editor.org/rfc/rfc8445
- https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidatePairStats/state
- https://github.com/gortc/ice/blob/master/pair.go