java操作sip
一、基础理论介绍
1.1 sip的主要功能
SIP的主要功能是允许用户或设备通过消息传递来产生互动,这些消息可以满足以下四种目的:
- 用户向系统注册;
- 邀请用户参加互动会议;
- 协商会议媒体内容的格式;
- 建立两端点或更多人之间的媒体流;
- 结束会议
1.1.1 用户向系统注册
SIP提供了注册的机制将用户和系统做连接。当SIP电话或终端设备为在线状态时,通常第一件要做的事就是发送REGISTER消息给Registration服务器,告知目前所在的联络位址和相关信息,而Registration服务器则会将用户ID和IP地址结合起来记录在Location Server的数据库里,以供日后使用。注册的机制可以使用户具可移动性的优点,用户只需随时向服务器更新自己的IP地址,便可使用相同ID,在不同地区或设备上让其他联络人找到自己,而不需固定在某一IP地址上。
注册的机制并不是必要的,SIP电话在知道双方IP地址的条件下,也可以不用透过任何服务器达成建立连线的目的。注册的机制通常都有时效的限制,而使用者则必须每隔一段时间和服务器联络,以随时更新目前的位址。
1.1.2 邀请用户参加互动会议
在SIP的系统里,建立会议的定义就是通过发送INVITE消息给一到多个设备建立双方连接,等建立连线后双方便可传送语音、影像或其他数据资料。在SIP里定义的位址可能是传统的电话号码、直接的IP地址或SIP URIs等,当使用者想邀请另一方加入会议,使用者只需在设备里键入对方的位址,并等待对方的回应。
在等待对方回应的过程中,SIP设备会建立INVITE消息并将它传送到网络上,透过Proxy或Redirection Server到Location Server寻找对方真实的IP地址,有了Location Server提供的资讯,系统便可以判断如何将邀请传达给对方。
1.1.3 协商会议媒体内容的格式
建立SIP会议的主要目的是用来传送双方的媒体内容,所以在SIP传送的讯息内容中会附加可提供的媒体格式供对方参考。和email服务相同,SIP协议只关心所传送的讯息内容,并不管所附加的内文是什么,而在SIP里最常使用到的附加内文则采用Session Description Protocol (SDP, RFC 2327)协议格式。
当接收方收到INVITE讯息的时候,接收方可以选择接受或拒绝这次的邀请。有时候接收方拒绝邀请的原因是因为它没有足够的能力接受对方所提供的媒体需求,例如接收方可能不提供视讯流服务,这时候只能拒绝对方的邀请。
1.1.4 建立两端点或更多人之间的媒体流
当对方接受了邀请,代表着此次会议已成功建立,接下来进行传送的便是媒体位元流(media stream)。在SIP里,媒体位元流和SIP协议的讯息是分开建立,并且使用Real-time Transport Protocol (RTP, RFC 1889)进行传送。
和SIP讯息传递不同的是,媒体位元流直接使用得到的IP地址进行点对点的传输,而不需要再次经过SIP讯息用来建立会议的servers。
1.1.5 结束会议
当使用者结束会议挂断电话的时候,此时设备便会传送SIP的BYE讯息给对方用来结束媒体位元流,连线便正式终止。
1.2 sip的元件架构
在SIP的标准里定义了几个SIP的元件,用来建立和传送SIP讯息,其中主要包含两大类,SIP User Agents和SIP servers。
SIP User Agents是用来建立和接收呼叫的终端装置,它们可能是实体IP电话,或是在PC上执行的软件。SIP Servers则包含了许多会议控制系统,用来提供location、proxy、redirection和registration服务。以下则是这些元件的介绍。
1.2.1 SIP User Agents
在SIP协议里,电话装置被定义为User Agents (UAs),UAs可视为user agent clients (UACs)和user agent servers (UASs)的通称。当开始建立会议时,UAC是用来建立和传送原始request的一方,而UAS则为所有server型态的泛称,可用来接收requests讯息,并回传response讯息。SIP UAs可用硬件实现,例如IP电话,也可以是在个人电脑里的软件电话。
1.2.2 SIP Server
尽管 UA(用户代理) 包含 server 元素,但当我们提到 SIP servers 时,通常指的是在分散式系统中扮演主要控制角色的 server。在这里,我们讨论四种在 RFC 3261 中定义的 SIP server 类型:
- Location server(位置服务器):Redirect 或 Proxy server (重定向或代理服务器)可以使用位置服务器获取被呼叫端可能的地址信息。
- Proxy server(代理服务器):代理服务器扮演中介者的角色,可以同时是 client(客户)端和server(服务器)端。代理服务器的目的是接收并转发收到的消息到其他服务器。
- Redirect server(重定向服务器):重定向服务器负责将收到的 SIP 请求,找出对应于旧地址的新地址,并返回给客户端。与代理服务器不同的是,重定向服务器不能处理对话,但可以产生 SIP 响应消息,指示用户连接其他 SIP 地址。
- Registrar server(注册服务器):注册服务器负责接收 REGISTER 消息,并将用户发送的地址信息记录在位置服务器中,以供代理或重定向服务器查询用户地址信息。
1.2.3 sip的呼叫单元
在我们进一步探讨 SIP 消息的功能之前,我们必须先定义 SIP 组成呼叫的单元,其中包括最基本的消息(Message),以及由消息所组成的事务(Transaction)和会话(Dialog)。
- 消息(Message)是指在用户代理之间传递的数据,它是 SIP 协议的一部分,可以用来建立、修改、结束会议,并分成由客户端发送到服务器端的请求消息(request),以及服务器端发送到客户端的响应消息(response)。
- 事务(Transaction)发生在客户端和服务器之间,它包含单一的请求(request)和针对该请求消息所回复的所有响应消息。不同的请求消息形成不同的事务。图中的建立对话过程包含三个事务,其中步骤1-3是第一个事务,包含一开始 UAC 发送的 INVITE 请求,以及接下来 UAS 发送的 180 Ringing 和 200 OK 响应,步骤4 的 ACK 请求则自己成为一个事务,步骤6-7 是第三个事务,其中包含 UAC 发送的 BYE 请求和 UAS 回复的 OK 响应。
- 对话(Dialog)是 SIP 定义的两个相同用户从会议开始到结束的这一段连续时间里所传递的所有消息。我们可以使用 Call-ID、From tag 和 To tag 这三个不同的标头来表示对话。如果两个消息中的 Call-ID、From tag 和 To tag 都相同,则表示它们属于同一个对话。通过图2.1 可以清楚地了解消息、事务和对话三者之间的关系。
1.2.4 sip消息和简单的呼叫建立
上一节提到了 Message 的定义,其中 Message 又可分为 request 和 response。SIP 定义了许多用来传递信息的 request 和 response 消息,以下介绍几个常见的消息,以及如何使用这些消息达成会议的建立。
1.2.4.1 sip request 信息
Request消息指的是从客户端发送到服务器端的消息。SIP协议将Request消息定义为各种不同的类型,每种类型都可以视为一种动作,用于请求用户代理或服务器提供特定的服务。以下介绍几个常见的Request消息。
- INVITE:初始会议
- ACK:确认会议建立
- BYE:结束会议
- CANCEL:取消尚未建立的会议
- REGISTER:向SIP服务器登记用户地址。
1.2.4.2 sip response 信息
Response指的是从服务器端发送到客户端的消息,用于响应客户端发出的Request消息,Response消息可能包含额外提供的信息,但也可能只是简单的回应,用于防止客户端再次发送Request消息。在SIP协议中,Response消息被分成六大类,其中前五大类借用了HTTP协议,第六大类则由SIP协议建立。以下介绍这六大类Response消息。
a. 1xx Responses: Information Response
代表请求已收到,并且正在执行Request消息所要求的事件。Example: 180 Ringing
b. 2xx Responses: Successful Responses
代表 Request 要求的动作已被成功接收、了解并接受。Example: 200 OK
c. 3xx Responses: Redirection Responses
表示为了完成Request消息需要进行额外的动作。Example: 302 Moved Temporarily。
d. 4xx Responses: Request Failure Responses
代表使用者所发出的Request消息包含了语法上的错误,所以server端无法满足此Request消息的要求。Example: 400 Bad Request。
e. 5xx Responses: Server Failure Responses
代表Server端发生错误而无法满足Request要求。Example: 500 Server Internal Error。
f. 6xx Responses: Global Failure Responses
代表使用者的要求在所有的Server端皆无法被满足。Example: 600 Busy Everywhere。
1.2.4.3 基本呼叫建立
图表示SIP基本呼叫建立和结束的流程,UAC通过拨打对方的IP地址建立新的连接,SIP设备则会向UAS发送INVITE消息。
以下为图所有消息的描述:
a. INVITE:当用户拿起电话开始拨打对方的IP地址时,此时UAC会向UAS发送INVITE消息。INVITE消息通常包含SDP协议的内容,用来告诉UAS可接受的媒体格式。
b. 180 Ringing:当UAS端接收到INVITE消息后,此时UAS的电话铃声就会响起,并回传180 Ringing给UAC端。此回传的Response消息包含了SDP格式的可接受编解码媒体格式。
c. 200 OK:当UAS端的用户接起电话时,此时UAS就会回传200 OK的Response消息给UAC端。
d. ACK:在UAC接收到180 Ringing和200 OK的Response消息之后,为了回应200 OK消息,必须回传ACK request消息给对方。此时就完成了消息传递的三方握手机制,而连接也正式建立。
e. 媒体会话:一旦SIP的连接建立完成,双方用户的媒体通道也会立刻建立,在这里使用Real-time Transport Protocol(RTP,RFC 1889)实现即时媒体传送的目的。
f. BYE:当双方通话结束时,UAC端会向UAS端发送BYE request消息,代表UAC要挂断电话。
g. 200 OK:当UAS端接收到BYE request消息则会回传200 OK消息,此时会议结束,并且媒体通道也会跟着中断。
1.2.5 头部信息
每个有意义的SIP消息都包含许多头部信息。以下是由UAC生成的INVITE消息的头部信息:
To: "The Little Blister" <sip:LittleGuy@there.com>
From: "The Master Blaster" <sip:BigGuy@here.com>;tag=12345 20
Call-ID: a4a08ea5f3563c128b4c1bbf219ca9b3@127.0.0.1
CSeq: 1 INVITE
Via: SIP/2.0/UDP 127.0.0.1:5060; branch=z9hG4bK1d32hr4
Max-Forwards:70
To
代表消息预期接收的地址,在这个例子里是:
To:"The Little Blister" <sip:LittleGuy@there.com>
其中,The Little Blister
代表接收端的显示名称,sip:LittleGuy@there.com
则是接收端的SIP URI。To头部的值不会随着经过的服务器改变。
From
所有的请求和响应消息都需要From的头部位,它代表了消息生成者的地址,在这里的例子是:
From:"The Master Blaster" <sip:BigGuy@here.com>;tag=12345
其中,The Master Blaster代表发送端的显示名称,sip:BigGuy@here.com则是发送端的SIP URI。From头部由用户事先提供,并有代表发送端的tag位。
Call-ID
提供了一个全世界独一无二的识别码,用来辨别每一次的会议。当UAC开启会谈时就会建立一个Call-ID以便与其他会谈区隔,而所有在同一个会谈中传递的请求和响应消息皆拥有相同的Call-ID。在这里的Call-ID的例子是:
Call-ID: a4a08ea5f3563c128b4c1bbf219ca9b3@127.0.0.1
其中,a4a08ea5f3563c128b4c1bbf219ca9b3
是用32-bit
随机密码所组成的,而127.0.0.1
则是邀请端的IP地址。
Cseq或command sequence
包含sequence number和请求消息类型。在这里的例子是:
CSeq: 1 INVITE
其中1为 sequence number
,INVITE则为请求消息的类型。所有的请求和响应消息都需要Cseq头部。Sequence number
的初始值不一定要是1,但必须是整数。而在同一个Call-ID的情况下,每传送一个新的请求消息,Cseq值就会增加。接收端则可藉由不同的Cseq值分辨这是一个新的请求,而不是重新传送的请求。重新传送的Cseq值是相同的。Cseq可以用来区隔和排序每一次的请求消息,而响应消息则可藉由Cseq头部得知所回应的请求消息是什么。
Via
Via头部位记录使用的transport以及所有请求消息经过的地址,以供响应消息找回请求消息发出的地方。在这里的例子是:
Via: SIP/2.0/UDP 127.0.0.1:5060; branch=z9hG4bK1d32hr4
从以上的例子可以知道,此消息协议为SIP,版本号码是2.0,并透过UDP协议传输。发送端的地址和port number则为127.0.0.1和5060。同一个消息的Via头部可能不只一个。如果有两个以上的Via头部,则所有Via顺序是非常重要的,因为响应必须沿着这些地址才能回到请求生成的地方。假设请求消息经过SIP Proxy,则此Proxy会将地址记录在Via头部的最上端。
头部里的branch位是由Request-URI、To、From、Call-ID和CSeq number,利用hash function所产生的。branch参数可用来当作transaction id。每产生一个新的transaction,branch值就会不同。
Max-Forwards
Max-Forwards头部是用来限制请求消息到目的地之前可以经过的hop数目。每经过一个hop,max-forwards的值就会依次遞减1。当proxy收到一个max-forwards的值为0的请求消息,则会将此请求消息丢掉,并回传483 Too Many Hops响应。Max-forwards的建议预设值为70。
二、JAIN SIP
2.1 概述
JAIN SIP是Java API for SIP的缩写,是Java平台上的一个SIP协议栈,用于开发基于SIP的应用程序,如VoIP电话、会议、即时消息等。JAIN SIP提供了一组API,使开发人员可以轻松地创建、发送和接收SIP消息。它基于标准的SIP协议,支持RFC 3261规范,并提供了一些扩展功能,如负载均衡、可靠传输和安全传输等。JAIN SIP可以与其他Java平台技术(如Java Servlet、JavaBeans和Java Media Framework)无缝集成,使得开发SIP应用程序变得更加容易和灵活。
2.2 JAIN SIP Packages
JAIN SIP包含了四个主要的packages,它们分别是:
javax.sip
:这个package包含了JAIN SIP的核心API,提供了创建、发送和接收SIP消息的基本功能,同时也提供了一些与SIP相关的辅助类和接口,如地址解析、会话管理等。
javax.address
:这个package含了代表 SIP 协议中 Addressing 部分的内容,其中定义了 URI 接口,而通常被称为 URI 接口又可以分为 SipURI 和 TelURI。
javax.sip.header
:该package包含了SIP头相关的API,SIP头用于在SIP消息中传递额外的信息,如发件人、收件人、会话参数等。
javax.sip.message
:该package包含了SIP消息相关的API,提供了SIP消息的创建、解析和操作等功能,同时也提供了一些与SIP消息相关的辅助类和接口。
2.3 JAIN SIP 对象介绍和对象间关系
2.3.1 以 SipFactory 为中心生成其他接口和类
在JAIN SIP的主要架构中,大量使用了面向对象的工厂模式,使得应用程序的开发与JAIN的实现类架构无关,提高了应用程序的可移植性。在JAIN的架构中,应用程序以SipFactory为中心生成其他相关类或接口,包括AddressFactory、HeaderFactory、MessageFactory和重要的SipStack,如图所示。我们可以通过使用getInstance()方法获取唯一的SipFactory。
2.3.2 JAIN SIP 的主要对象结构
当使用 SipFactory 生成其他接口或类时,其中最重要的是 SipStack 接口。我们可以将 SipStack 视为与外界通信的网络接口卡,该接口可用于接收网络上传来的消息或对象,并将其传递给内部应用程序进行处理,或将内部的消息或对象发送到网络上。每个网络接口卡都有一组自己的IP地址,因此在同一个应用程序中,一个IP地址只能对应一个SipStack
。
在生成SipStack对象时,必须先指定该对象的属性(Property),属性的内容包括IP地址、堆栈名称、出站代理等参数,并通过SipFactory的createSipStack(Properties)方法进行建立。
在获得SipStack接口之后,接下来我们需要为应用程序建立SipProvider接口。如图三所示,SipProvider是JAIN SIP事件模型的中心。通过它,我们可以处理从SipStack发送过来的事件,并调用相应的函数进行处理。
每个SipStack可能会有一个以上的SipProvider,每产生一个SipProvider则需要给定一个port端口号,因此不同port端口号的SipProvider则负责处理接收属于自己端口号传来的消息。我们可以通过SipStack的createSipProvider(ListeningPoint)方法来建立新的SipProvider。在JAIN SIP中,端口号是用ListeningPoint接口代表的。
当拥有了SipStack和SipProvider之后,最后一个JAIN SIP中的重要接口为SipListener。SipListener必须向SipProvider注册,以接收从SipProvider传送过来的事件。每个SipListener可以向多个SipProvider注册。我们可以将SipListener接口视为开发者所实现的应用程序。
当实现SipListener接口时,必须包含processRequest()、processResponse()以及processTimeout()三种方法,分别处理接收到的request、response和timeout事件。
当SipProvider接收到来自SipStack的Message时(Request或Response),SipProvider会将该Request或Response加入Event对象内,并透过对应的Event Handler通知SipListener。例如,当接收到Request时,SipProvider会呼叫已登记SipListener的processRequest()方法,此时应用程序便可作适当的处理。
2.3.3 使用MessageFactory生成的Message对象
当我们介绍SipFactory接口时,提到SipFactory可以帮助我们创建MessageFactory,使用MessageFactory可以创建SIP协议中的Request和Response消息对象。在JAIN SIP中,Message对象可视为Request和Response的泛型,目的是将Request和Response共用的方法集合并在同一接口中。Request和Response分别对应SIP协议中的各种Request和Response类型,使用MessageFactory创建时,应用程序需要指定所需的类型信息(例如INVITE、ACK或180、200等)。
2.3.4 使用 AddressFactory 生成的 Address 对象
AddressFactory是由SipFactory生成的,生成后应用程序利用此对象生成Sip协议的各种URI相关对象,其中URI为SipURI和TelURL的泛型(Generic Type),目的是将SipURI和TelURL两者共用的方法集中在同一界面上,并预留日后扩展之用。另外值得注意的是,Address对象除了包含URI对象之外,还包含用来显示用户名的display name,display name通常会显示在用户界面上,以方便用户判断来电者为何。
2.3.5 使用HeaderFactory 生成的 Header 对象
HeaderFactory是由SipFactory生成的,生成后应用程序利用此对象生成Sip Protocol的所有Header相关对象,例如ToHeader、FromHeader、CseqHeader等。JAIN SIP规格将每一个不同类型的头定义为对象,并且所有在RFC3261中定义的Header在API中都有相应的类别。
三、实际代码
import 包
import javax.sip.*;
import javax.sip.address.*;
import javax.sip.header.*;
import javax.sip.message.*;
pom
具体版本可以参考 https://mvnrepository.com/artifact/javax.sip
<dependency>
<groupId>javax.sip</groupId>
<artifactId>jain-sip-api</artifactId>
<version>1.2.1.4</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.sip/jain-sip-ri -->
<dependency>
<groupId>javax.sip</groupId>
<artifactId>jain-sip-ri</artifactId>
<version>1.3.0-91</version>
<!-- <version>1.2.2370</version>-->
<scope>provided</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.8</version>
</dependency>
因为jainsip 用到了log4j,所以pom里也要加入,不然会报错
Caused by: java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at javax.sip.SipFactory.createStack(SipFactory.java:314)
... 26 more
Caused by: java.lang.NoClassDefFoundError: org/apache/log4j/Priority
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:264)
at gov.nist.javax.sip.SipStackImpl.<init>(SipStackImpl.java:799)
... 31 more
Caused by: java.lang.ClassNotFoundException: org.apache.log4j.Priority
at java.net.URLClassLoader.findClass(URLClassLoader.java:387)
at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)
at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
... 34 more
initialization
当使用 SipFactory 生成其他接口或类时,其中最重要的是 SipStack 接口。我们可以将 SipStack 视为与外界通信的网络接口卡,该接口可用于接收网络上传来的消息或对象,并将其传递给内部应用程序进行处理,或将内部的消息或对象发送到网络上。
每个网络接口卡都有一组自己的IP地址,因此在同一个应用程序中,一个IP地址只能对应一个SipStack。在生成SipStack对象时,必须先指定该对象的属性(Property),属性的内容包括IP地址、堆栈名称、出站代理等参数,并通过SipFactory的createSipStack(Properties)方法进行建立。
SipFactory
SipFactory sipFactory = SipFactory.getInstance();
SipStack
每一个SipStack对象都有属于自己的名称、IP地址等属性,如图所示。
因此,在建立SipStack之前,我们必须建立Properties对象以存储这些属性,并使用这些属性生成SipStack。
名称 | 描述 |
---|---|
javax.sip.IP_ADDRESS | SIP Stack 的 IP地址 |
javax.sip.OUTBOUND_PROXY | 如果SIP Stack没有通过代理服务器发送,则直接输入终端地址作为对外代理地址。 |
javax.sip.STACK_NAME | SIP Stack 名称,可任意输入 |
Gov.nist.javax.sip.MAX_MESSAGE_SIZE | 可接收的最大信息长度 |
在建立SipStack对象之前,必须先创建用于存储SipStack属性的Properties对象。
首先创建新的properties对象。
Properties properties = new Properties();
properties.setProperty("javax.sip.IP_ADDRESS", "localhost");
properties.setProperty("javax.sip.OUTBOUND_PROXY", "fs地址或者kamilio地址:端口/udp");
properties.setProperty("javax.sip.STACK_NAME", "JainSipPhone");
// 设置最大的信息长度,这里设置 2^20,超过就认为是恶意攻击
properties.setProperty("gov.nist.javax.sip.MAX_MESSAGE_SIZE", "1048576");
比如
properties.setProperty("javax.sip.IP_ADDRESS", "111.111.111.000");
properties.setProperty("javax.sip.OUTBOUND_PROXY", "111.111.111.111:4160/udp");
properties.setProperty("javax.sip.STACK_NAME", "JainSipPhone");
properties.setProperty("gov.nist.javax.sip.MAX_MESSAGE_SIZE", "1048576");
有了SipFactory对象之后,我们就可以使用它的createSipStack()方法创建新的SipStack。在创建时,必须提供预先设置好的Property对象。
try {
SipStack sipStack = sipFactory.createSipStack(properties);
} catch (PeerUnavailableException e) {
System.err.println(e.getMessage());
}
HeaderFactory
HeaderFactory headerFactory = sipFactory.createHeaderFactory();
AddressFactory
AddressFactory addressFactory = sipFactory.createAddressFactory();
MessageFactory
MessageFactory messageFactory = sipFactory.createMessageFactory();
ListeningPoint
如图所示,我们可以通过SIPStack对象创建新的ListeningPoint,ListeningPoint代表网络接口的端口号,因此在创建时必须指定端口号和使用的传输协议。
同样地,我们也可以通过SIPStack对象创建SIPProvider对象,用于接收和处理SIPStack发送的事件。在创建时,必须提供之前创建的ListeningPoint,表示此SipProvider在接收事件时侦听的端口号。
以下是创建ListeningPoint的示例代码。
有了SipStack对象之后,我们便可以使用它来建立ListeningPoint。以下是建立一个使用UDP传输的ListeningPoint对象的示例代码:
ListeningPoint lp = sipStack.createListeningPoint("myIpAddress", myPort, "udp");
同样地,我们也可以建立一个使用TCP传输的ListeningPoint对象:
ListeningPoint lp = sipStack.createListeningPoint("myIpAddress", myPort, "tcp");
在这里,myPort参数是我们用来发送消息的端口号,通常为5060。建立完ListeningPoint对象后,我们可以将其绑定到SIPProvider对象上,以接收和处理SIPStack发送的事件。
SipProvider
通过SipStack的createSipProvider()方法,我们可以创建属于此SipStack的SipProvider对象。在创建时,必须提供之前创建的ListeningPoint对象作为参数。以下是示例代码:
SipProvider sipProvider = sipStack.createSipProvider(listeningPoint);
这样,我们就可以使用该SipProvider对象接收和处理SIPStack发送的事件。
SipListener
我们在实现应用程序时需要实现SipListener接口,该接口的方法将在下一节中介绍。在这里假设我们实现的SipListener名称为JainSipPhone,因此我们将JainSipPhone指定为this(其实就是implements SipListener
),表示当前编写的应用程序
JainSipPhone listener = this;
每个SipListener都可以向SipProvider注册,以接收从SipProvider传送过来的消息。在JAIN API中,我们可以通过以下方式将SipListener注册到SipProvider上
sipProvider.addSipListener(listener);
这样,我们就可以使用该SipListener对象接收和处理从SipProvider发送的SIP消息
UAC Create and Send INVITE Request
在成功建立JAIN SIP的基础对象之后,我们可以开始编写程序的第一步:UAC向UAS发送INVITE消息。
在构建消息之前,我们必须先构建消息中包含的所有头部。以INVITE消息为例,成功构建一个INVITE消息需要包含六个头部对象:CSeqHeader、FromHeader、ToHeader、ViaHeaders、MaxForwards、CallIDHeader。
以下是构建和发送INVITE消息的原始代码:
// 创建FromHeader对象
SipURI fromUri = addressFactory.createSipURI("user1", "example.com");
Address fromAddress = addressFactory.createAddress(fromUri);
fromAddress.setDisplayName("User1");
FromHeader from = headerFactory.createFromHeader(fromAddress, generateRandomString());
其中generateRandomString() 是为了获取一个随机的 tag
public static String generateRandomString() {
int length = 10;
String characters = "abcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
int randomIndex = random.nextInt(characters.length());
char randomChar = characters.charAt(randomIndex);
sb.append(randomChar);
}
return sb.toString();
}
//有 setDisplayName 的样例:
//From: "User1" <sip:user1@example.com>;tag=12345
//没有 setDisplayName 的样例:
//From: <sip:user1@example.com>;tag=12345
// 创建ToHeader对象
SipURI toUri = addressFactory.createSipURI("user2", "example.com");
Address toAddress = addressFactory.createAddress(toUri);
toAddress.setDisplayName("User2");
//代表 接收端的toTag 必须由接收端设定,所以先设置成 null
ToHeader to = headerFactory.createToHeader(toAddress, null);
//有 setDisplayName 的样例:
//To: "User2" <sip:user2@example.com>
//没有 setDisplayName 的样例:
//To: <sip:user2@example.com>
// 创建ViaHeader对象
ViaHeader via = headerFactory.createViaHeader("本地ip", 监听端口, "传输方式", null);
// 创建Via头域
// 由于 via 头域通常包含许多经过的地址信息,所以新增一个 ArrayList
ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
ViaHeader viaHeader = headerFactory.createViaHeader("10.221.146.253", 5060, "udp", null);
viaHeaders.add(viaHeader);
// maxforwardsHeader 请求最大次数
MaxForwardsHeader maxForwards = headerFactory.createMaxForwardsHeader(70);
// 创建CSeq头域
CSeqHeader cSeqHeader_invite = headerFactory.createCSeqHeader(1L, Request.INVITE);
CSeqHeader cSeqHeader_register = headerFactory.createCSeqHeader(1L, Request.REGISTER);
// 创建CallIDHeader对象
// 通过 SipProvider 的 getNewCallID() 方法获取新的 CallIDHeader。使用 SipProvider 提供的方法可以获取代表此对话的唯一 Call ID,而 HeaderFactory 提供的方法则需要手动输入 Call ID 字符串,并且不能保证唯一性。
CallIdHeader callId = sipProvider.getNewCallId();
// 创建INVITE请求消息
Request inviteRequest = messageFactory.createRequest(toAddress.getURI(),
Request.INVITE, callId, cseq, from, to, via, maxForwards);
// 也可以重新创建一个 SipURI
SipURI requestURI = addressFactory.createSipURI(toUser,registrarAddress);
Request request = messageFactory.createRequest(requestURI, Request.INVITE, callId, cseq, from, to, via, maxForwards);
// 设置消息体
inviteRequest.setContent("Hello world!", contentTypeHeader);
// 发送INVITE请求消息
// 在有了INVITE请求消息后,需要创建一个新的ClientTransaction对象将消息发送到网络上。可通过SipProvider的getNewClientTransaction()方法获取新的ClientTransaction对象,该对象是根据给定的请求消息生成的。生成后,该对象将专门处理该请求消息,无法用于处理其他消息。
ClientTransaction inviteTransaction = sipProvider.getNewClientTransaction(inviteRequest);
inviteTransaction.sendRequest();
UAS Receive INVITE and Send Response
在介绍 UAS 端如何处理收到的 INVITE 消息并回传 Response 消息时,我们先介绍 SipProvider 和 SipListener 之间的事件处理关系,如图 3.12 所示。
SipListener 是我们实现应用程序的一部分,其目的是为了接收从外界传来的事件。这些事件包括 Request Event、Response Event 和 Timeout Event,这些事件通过 SipProvider 发送给 SipListener 实现的方法。例如,SipProvider 通过 processRequest(RequestEvent) 方法发送 Request 事件。当 SipListener 接收到事件后,将进行相应的处理。在这里,我们需要编写处理这些事件的代码。
假设JainSipPhone 是我们实现的应用程序。
public void JainSipPhone::processRequest(RequestEvent requestEvent) {
//一开始我们必须先获取接收到的请求对象,在这里使用 RequestEvent 的getRequest() 方法获取接收到的请求事件。
Request request = requestEvent.getRequest();
// 接下来判断 request 的类型,在这里我们以 INVITE 信息为例
if (request.getMethod().equals(Request.INVITE)) {
//当收到 INVITE 消息之后,应用程序必须回传 response 消息给对方,首先是建立 180/Ringing 的 response 消息,response 消息是根据 request 消息而来的,所以建立时除了给定 response 的 code number (180)之外,还包括相对的 request 对象,建立时使用 MessageFactory 的 createResponse() 方法。
Response response = messageFactory.createResponse(180, request);
// 使用 Response 对象的 getHeader() 方法获取 response 的 ToHeader 对象,使用时需给定 Header (ToHeader.NAME)名称。其中 String NAME = "To";
ToHeader toHeader = (ToHeader)response.getHeader(ToHeader.NAME);
// 使用 ToHeader 的setTag() method 设定 toHeader tag为4321
toHeader.setTag("4321");
//在获得response对象后,我们可以将其发送到网络上。首先,使用SipProvider的getNewServerTransaction()方法获取与此请求对应的服务器事务。在使用该方法时,需要提供接收到的request信息。
ServerTransaction serverTransaction = sipProvider.getNewServerTransaction(request);
// 最后,使用 ServerTransaction 的 sendResponse() 方法将响应发送到网络上。在使用时需要提供要发送的响应。
serverTransaction.sendResponse(response);
// 在发送完180/Ringing响应后,接下来要建立200/Ok响应。建立200/Ok响应的方法和180/Ringing响应大致相同。
response = messageFactory.createResponse(200, request);
// 获得 toHeader 对象
toHeader = (ToHeader) response.getHeader(ToHeader.NAME);
// 设定 response toHeader tag = 4321
toHeader.setTag("4321");
//我们使用之前建立的ServerTransaction对象将200/OK消息发送到网络上。
serverTransaction.sendResponse(response);
}
}
UAC Receive Response and Send ACK
现在介绍UAC如何处理UAS所发送过来的Response消息,并发送ACK Request来完成三方握手协议。处理Response的方法是实现processResponse()方法,在这里我们以收到200/OK response为例。以下是processResponse()的原始代码。
public void JainSipPhone::processResponse(ResponseEvent responseReceivedEvent) {
Response response = responseReceivedEvent.getResponse();
log.info("[processResponse]:" + response);
//通过使用ResponseEvent的getClientTransaction()方法获取与该Response对象相关的ClientTransaction,该ClientTransaction可以帮助发送响应的Request对象。
Transaction tid = responseReceivedEvent.getClientTransaction();
// invite 后,fs会下发 100trying,,180Ringing,然后才是 200OK。response.getStatusCode() == 200:获取响应的状态码,判断是否为 200,即 OK 状态码,表示请求成功。
// ((CSeqHeader)response.getHeader(CSeqHeader.NAME)).getMethod().equals(Request.INVITE):从响应中获取 CSeq 头,判断其中的方法类型是否为 INVITE。CSeq 头中包括了 SIP 请求的序列号和方法类型,因此可以通过这个头来判断响应对应的请求是否为 INVITE 请求。
// 综合起来,这段代码的作用是判断 INVITE 请求的响应是否成功,如果响应状态码为 200 并且响应对应的请求方法为 INVITE,则认为请求成功,并将 flag 设置为 true。
boolean flag = response.getStatusCode() == 200 && ((CSeqHeader)response.getHeader(CSeqHeader.NAME)).getMethod().equals(Request.INVITE);
if (flag) {
// 使用ClientTransaction的getDialog方法获取属于此事务的Dialog
Dialog dialog = tid.getDialog();
try {
//利用Dialog的createRequest()方法建立ACK请求对象,建立时必须指定请求名称。
Request ackRequest = dialog.createRequest(Request.ACK);
//使用Dialog的sendACK()方法将ACK请求发送到网络上。
dialog.sendAck(ackRequest);
} catch (SipException e) {
throw new RuntimeException(e);
}
}
}
UAS Receive Ack
当UAS收到ACK消息之后,代表SIP会议已经成功建立,接下来则需要开启媒体比特流,进行媒体会议。
在本节中,我们将讨论UAS如何通过SipListener实现的processorRequest()方法处理ACK请求,以下是处理ACK请求的原始代码。
public void JainSipPhone::processRequest(RequestEvent requestEvent) {
//首先先获得 Request 对象
Request request = requestEvent.getRequest();
// 判断是否收到 ACK
if (request.getMethod().equals(Request.ACK)) {
// 接下来是 media transmission
...
}
}
四、sdp
简介
在网络多媒体通话场景下,会议的参与者往往是用 SDP 协议来传递、协商媒体详细信息、网络地址和其他元数据。SDP 协议的全称是 session description protocol,含义是会话描述协议,它不是一种传输协议,而是由 ITEF 组织下的 MMusic 工作组设计的一种会话描述格式。
SDP 应用
SDP 最常用于 RTC 实时通话的协商过程,在 WebRTC 中,通信双方在连接阶段使用 SDP 来协商后续传输过程中使用的音视频编解码器(codec)、主机候选地址、网络传输协议等。
在实际的应用过程中,通信双方可以使用 HTTP、WebSocket、DataChannel 等传输协议来相互传送 SDP 内容,这个过程称作 offer/answer 交换,也就是发起方发送 offer,接收方收到 offer 后回复一个 answer。例如在下图的服务端架构中,客户端将 offer 发送给信令服务器,信令服务器转发给媒体服务器,媒体服务器将 offer 和自身的能力进行比较后得到 answer,信令服务器再将 answer转发给客户端,随后客户端和媒体服务器就可以进行 RTP 通信。
SDP 格式
SDP 协议的设计可以参考 rfc4566 文档。它是一种具有特殊约定格式的纯文本描述文档,也就是它的内容都是由 UTF-8 编码的文本,有点类似于 JSON/XML。一个 SDP 会话描述包括若干行 type=value 形式的文本,其中 type 是一个区分大小写的字母,例如 v、m 等,value 是一个结构化的文本,格式不固定。通常 value 由若干分割符隔开的字段组成或者是一个字符串, 整个协议文本区分大小写。"=" 两侧不允许有空格存在。
SDP 整体结构
SDP 由一个会话级描述(session level description)和多个媒体级描述(media level description)组成。会话级描述的作用域是整个会话,在 SDP 中,从 "v=" 行开始到第一个 "m=" 行之前都是属于会话级描述的内容。媒体级描述对某个媒体流的内容进行描述,例如某个音频流或者某个视频流,从某个 "m=" 行开始到下个 "m=" 行之前是属于一个媒体级描述的内容。
总之,除非媒体部分重载,会话级的值是各个媒体的缺省默认值(就是说媒体级描述其实也是一个会话级描述,只不过没写出来的会话级描述参数都用的缺省值)。
SDP 中字段的含义
SDP 中有的字段是必须的,有的字段是可选的,可选的字段在如下的示例中都使用 *
进行标记。 SDP 中 type 出现的顺序是固定的,按照如下顺序进行排列,这样可以增强解析器错误检测的能力,另外也可以简化解析器的实现。
Sessiondescription 会话级描述
v= (protocol version) 必选,SDP协议版本号
o= (owner/creator and session identifier) 必选,创建者,会话信息
o=<用户名> <session id> <会话版本> <网络类型><地址类型> <地址>
s= (session name) 必选,session名称
i=* (session information) 会话信息
u=* (URI of description) URI描述
e=* (email address) Email地址
p=* (phone number) 电话号码
c=* (connection information - notrequired if included in all media) 可选,媒体链接信息
c=<networktype> <address type> <connection address>
b=* (bandwidth information) 可选,带宽信息
One or more time descriptions (seebelow)
z=* (time zone adjustments) 时区调整
k=* (encryption key) 加密秘钥
a=* (zero or more session attributelines) 0或多个会话属性行
Zero or more media descriptions (seebelow)
Time description 时间描述
t= (time the session is active) 必选,会话的开始时间和结束时间。单位为秒。
t=<start time> <stop time> 假如<stop time>为零表示过了<start time>时间后会话一直持续。当<start time> 和<stoptime>均为零时表示持久会话。
r=* (zero or more repeat times) 0或多次重复次数
Media description 媒体描述
m= (media name and transport address) 必选,一个会话描述包含几个媒体描述
m=<media><port> <transport> <fmt list>
i=* (media title) 媒体标题
c=* (connection information - optionalif included at session-level)
b=* (bandwidth information) 带宽信息
k=* (encryption key) 加密秘钥
a=* (zero or more media attributelines) 0或多个会话属性行
上面的有些行是必需
有的,有些行是可选
的。可选的行有*号标记
。必需的是v,o,s,t,m
(这是对于会话级描述和媒体及描述总体而言的,对于媒体级描述而言只有m=是必须的)。
注意所有的描述项必须按照上面的顺序给出。
可以看到 type 都是单个小写字母,它们是不可扩展的,如果 SDP 解析器不能解析某个 type 则必须忽略,a=
是 SDP 的主要扩展手段,来针对某中特定类型的媒体或应用做扩展。
有一个很好的网站:webrtchacks.com/sdp-anatomy… 可用于学习 SDP,这个网站里面鼠标移动到 SDP 某一行时,就会显示这一行 SDP 的具体含义。在这里简单介绍一下 SDP 协议中常用的 type 以及对应的含义。
v: protocol version
v=0
v 的含义是 SDP 协议的版本号,目前 v 都是 0。
o: originator and session identifier
o=<username> <session-id> <session-version> <nettype> <addrtype> <unicast-address>
会话所有者有关的参数,包括用户名、session 信息,地址信息等。 (Owner/creator and session identifier)。
- username: 会话发起者的名称。如果不提供则用"-"表示,用户名不能包含空格;
- session-id: 主叫方的会话标识符;
- session-version: 会话版本号,一般为 0;
- nettype: 网络类型,目前仅使用 IN 来表示 Internet 网络类型;
- addrtype: 地址类型,可以是 IPV4 和 IPV6 两种地址类型;
- unicast-address:会话发起者的 IP 地址。
s: session name
s=<session name>
本次会话的标题或会话的名称(Session name)。
t: time the session is active
t=<start-time> <stop-time>
会话的起始时间和结束时间(Time session starts and stops),如果没有规定这两个时间的话,都写为 0 即可。
m: media name
m=<media> <port>/<number of ports> <proto> <fmt> ...
媒体行,描述了发送方所支持的媒体类型等信息(Media information)。
- media:媒体类型,可以为 "audio"、"video"、"text"、"application"、"message",表示音频类型、视频类型、文本类型、应用类型、消息类型等,以后也可能扩展其他类型;
- port/number of ports: 流传输端口号。表示在对应的本地端口上发送流;
- proto:流传输协议。举例说明:
- RTP/SAVPF 表示用 UDP 传输 RTP 包;
- TCP/RTP/SAVPF 表示用 TCP 传输 RTP 包;
- UDP/TLS/RTP/SAVPF 表示用 UDP 来传输 RTP 包,并且使用 TLS 加密;
- 最后的 SAVPF 还有其他几种值:AVP, SAVP, AVPF, SAVPF。
- AVP 意为 AV profile
- S 意为 secure
- F 意为 feedback
fmt 表示媒体格式描述,它可能是一串数字,代表多个媒体,这个字段的含义与 proto 字段的类型相关。在后面,可以使用"a=rtpmap:"、"a=fmtp:"、"a=rtcp-fb" 等扩展字段来对 fmt 进行说明。
c: connection data
c=<nettype> <addrtype> <connection-address>
一个会话描述必须在每个媒体层都包含“c=”字段或者在会话层包含一个“c=”字段。如果这两个层都出现的话,则媒体层出现的“c=”会覆盖会话层出现的“c=”字段的值。
- nettype: 是一个文本字符串,目前只定义了“IN”,表示“Internet”,未来会定义其他值。
- addrtype: 目前只定义了 IP4 和 IP6。
- connection-address: 标志连接的地址。取决于 addrtype 字段的不同,在 connection-address 之后可能也会跟随其他的字段。
b: bandwidth
b=<bwtype>:<bandwidth>
这个字段的意思是本会话或者媒体所需占用的带宽。 bwtype 可以为 "CT" 或者 "AS",给出了 bandwidth(单位 kbps)数字所代表的含义:
CT:表示会话所占的所有的带宽的大小。当用于 RTP 会话时,表示所有的 RTP 会话所占用的带宽。
AS:这个带宽类型是针对特定应用的。通常,这表示某应用所占用的最大带宽。当用于 RTP 会话时,表示单一 RTP 会话所占用的带宽,可以理解为 CT 代表的是整个通话过程的带宽,AS 代表的是某个流的带宽。
Encryption Keys ("k=")
k=<method>
k=<method>:<encryption key>
如果在一个安全的信道上传输 SDP 消息,那么 SDP 之中也可以携带密钥,携带的方式就是采用字段 "k="。当然这种方式目前已经不推荐了。
字段 "k=" 可以是全局的,也可以是放在某个 "m=" 中的,分别代表应用于所有的媒体流,或者单独应用于某条媒体流。定义格式有如下几种:
- k=clear:
在这种方法中,密钥是没有经过任何转换的。除非传输通道是绝对安全的,否则不应当使用这种方法。
- k=base64:
在这种方法中,密钥经过 base64 的编码。除非保证传输通道绝对安全,否则不应当使用这种方法。
- k=uri:
在这种方法中,给出一个 URI。通过这个 URI,可以获得密钥,访问 URI 的过程中可能还需要认证。
- k=prompt
在这种方法中,没有给出密钥。但是加上这个字段后,当用户加入会话时会提示其输入密钥。这种方式目前也不推荐。
a: attributes
a=<attribute>
a=<attribute>:<value>
a 表示的是属性。a 字段是扩展 SDP 的主要方式,有会话层属性和媒体层属性。会话层的属性应用于所有的媒体流,媒体层的属性只应用于当前的媒体流。
属性有两种方式:
- 特性属性,a= 表示,例如:"a=recvonly"
- 值属性,以a=:表示,例如"a=ice-ufrag:khLS"
常用的属性列表如下:
属性 | 含义 | 例子 |
---|---|---|
a=rtpmap: | RTP/AVP(Audio Video Profile) list | m=audio 54278 RTP/SAVPF 111 103 104 0 8 106 105 13 126 a=rtpmap:111 opus/48000/2 |
a=fmtp: | Format transport | a=fmtp:111 minptime=10 |
a=rtcp: | Explicit RTCP port (and address) | a=rtcp:54278 IN IP4 180.6.6.6 |
a=mid: | Media identification grouping | a=mid:audio |
a=ssrc: | "ssrc" indicates a property (known as a"source-level attribute") of a media source (RTP stream) within an RTP session | a=ssrc:189858836 msid:GUKF430Khp9jEQiPrdYe0LbTAALiNAKAIfl2 ea392930-e126-4573-bea3-bfba519b4d59 |
a=ssrc-group: | Ssrc identification grouping | a=ssrc-group:SIMULCAST 32040 32142 a=ssrc:32040 imageattr:96 [x=1280,y=720] a=ssrc:32142 imageattr:96 [x=640,y=480] |
a=ice-ufrag: | a=ice-pwd: The "ice-ufrag" and "ice-pwd" attributes convey the username fragment and password used by ICE for message integrity | a=ice-ufrag:kwlYyWNjhC9JBe/V a=ice-pwd:AU/SQPupllyS0SDG/eRWDCfA |
a=ice-pwd: | ||
a=fingerprint: | A certificate fingerprint is a secure one-way hash of the DER (distinguished encoding rules) form of the certificate. | a=fingerprint:sha-256 D1:2C:BE:AD:C4:F6:64:5C:25:16:11:9C:AF:E7:0F:73:79:36:4E:9C:1E:15:54:39:0C:06:8B:ED:96:86:00:39 |
a=candidate: | It contains a transport address for a candidate that can be used for connectivity checks. | a=candidate:4022866446 1 udp 2113937151 192.168.0.197 36768 typ host generation 0 |
a=ptime: | Length of time in milliseconds for each packet | a=ptime:20 |
a=recvonly | Receive only mode | a=recvonly |
a=sendrecv | Send and receive mode | a=sendrecv |
a=sendonly | Send only mode | a=sendonly |
a=sdplang: | Language for the session description | |
a=framerate: | Maximum video frame rate in frames per second | a=framerate:15 |
a=inactive | Inactive mode | a=inactive |
SDP 协商过程
SDP 协商过程对应的 RFC 文档是 rfc3264。
在 WebRTC 通信过程中,连接的两端通过 offer/answer
交换过程来互相进行 SDP 协商,发起方发送 offer 给接收方,其中包含了发起方的媒体能力和其他信息(包括 ICE、DTLS fingerprint,SSRC 等),接收方收到 offer 后和自己支持的能力进行比较取出交集,回复 answer 给发起方,其中包含了协商之后的媒体能力和其他信息(ICE、DTLS fingerprint,SSRC 等),要注意这个过程中只有媒体能力是协商的,也就是发起方和接收方共同支持的媒体能力,例如支持的编解码格式,以及是否支持 RTX、FEC、TCC 等 QoS 功能,而 ICE、DTLS、SSRC 的信息是直接给出通知对方,是连接双方各自的信息。
在 offer/answer 交换完成之后,双方就可以使用这些共同支持的媒体能力进行通信。
协商的过程如下图所示:
A 向服务器发送 offer 携带自己的 SDP 信息,包括:
- 地址信息是 UDP 10.124.17.11:32132,A 通过这个地址来和服务端进行媒体通信;
- ICE 信息,服务器通过 ICE 信息来验证客户端 A 的身份;
- 在接下来的媒体通信过程,客户端 A 只发不收 RTP;
- A 音频支持 48kHz 的双声道 Opus 编解码,使用 SSRC=1111 来发送;
- A 视频支持 VP8 或者 VP9 或者 H264 编解码,使用 SSRC=2222 来发送; 服务器在收到 A 的 offer 后,回复给 A 一个 answer,这是双方协商出来的媒体能力:
- 地址信息是 UDP 10.108.27.64:8888,服务器通过这个地址来和 A 进行媒体通信;
- ICE 信息,A 通过 ICE 信息来验证服务器的身份;
- 在接下来的媒体通信过程,服务器对于 A 来说只收不发 RTP;
- 协商出来的音频能力是 48kHz 的双声道 Opus 编解码;
- 协商出来的视频能力是 H264 编解码。 B 向服务器发送 offer 携带自己的 SDP 信息,包括:
- 地址信息是 UDP 192.168.1.1:32222,B 通过这个地址来和服务端进行媒体通信;
- ICE 信息,服务器通过 ICE 信息来验证客户端 B 的身份;
- 在接下来的媒体通信过程,客户端 A 需要收发 RTP;
- B 音频支持 48kHz 的双声道 Opus 编解码或者 G711 编解码,使用 SSRC=1234 来进行发送;
- B 视频支持 H264,使用 SSRC=4567 来进行发送; 服务器在收到 B 的 offer 后,回复给 B 一个 answer,这是双方协商出来的媒体能力:
- 地址信息是 UDP 10.108.27.64:8888,服务器通过这个地址来和 B 进行媒体通信;
- ICE 信息,B 通过 ICE 信息来验证服务器的身份;
- 在接下来的媒体通信过程,服务器对于 B 来说可以收发 RTP;
- 协商出来的音频能力是 Opus 编解码、48KHz,双声道,使用 SSRC=1122 进行发送;
- 协商出来的视频能力是 H264 编解码,使用 SSRC=3344 可以发送。 从以上两个 offer/answer 交换过程中可以看出,上图中的媒体服务器音频支持 Opus 编解码,但不支持 G711 编解码;视频支持 H264 编解码,但不支持 VP8、VP9 编解码。
SDP 样例
在介绍完 SDP 的文本结构和 SDP 的协商过程中后,这里我们举一个实际传输的 SDP 内容来帮助理解。实际工程中,客户端和服务端都会支持好几种音视频媒体编解码,并且可能支持好几种能力,例如 FEC、NACK、TCC 等等,所以实际工程中的 SDP 都会非常长。为了方便阅读,下文将直接在 SDP 中每一行上方的注释中解释其含义。这个 SDP 中包括了 audio 和 video 两种流,video 的内容有部分也在 audio 中出现过,因此不再重复解释。
// SDP 版本信息
v=0
// session 信息
// o=<username> <session-id> <session-version> <nettype> <addrtype> <unicast-address>
o=- 1873022542326151139 2 IN IP4 127.0.0.1
// s=<session name>
s=-
// t=<start-time> <stop-time>,如果不规定开始和结束时间,两个都填 0 即可
t=0 0
// 使用 "a=" 来扩展的 bundle 属性,其含义是 audio 和 video 使用同一个端口发送/接收,具体可以参考下方的 RFC 文档:
// https://tools.ietf.org/html/draft-ietf-mmusic-sdp-bundle-negotiation-54
a=group:BUNDLE audio video
// 列出当前SDP中所有的 media stream id,以空格分割
// WMS 的含义是这里面的 media stream id 适配 webrtc 的 media stream
// 参考 RFC 文档: https://datatracker.ietf.org/doc/html/draft-ietf-mmusic-msid-01#section-3
a=msid-semantic: WMS 34b34ced3c5623ea4213vx3
// m=<media> <port> <proto> <fmt> ...
// port=10 无实际含义,真正通信使用的端口由 ICE Candidate 指定
// proto=UDP/TLS/RTP/SAVP 表示用 UDP 来传输 RTP 包,并使用 DTLS 加密
// 后面的一串数字是 fmt,表示所有 codec 的 payloadtype
m=audio 10 UDP/TLS/RTP/SAVPF 111 114 115 116 123 124 125
// c=<nettype> <addrtype> <connection-address>
c=IN IP4 0.0.0.0
// a=rtcp:<port> [nettype addrtype connection-address]
a=rtcp:10 IN IP4 0.0.0.0
// ICE 信息,参考 RFC 文档: https://tools.ietf.org/html/rfc5245#section-15.4
a=ice-ufrag:aZ/b
a=ice-pwd:3tFwvgPAA2PK3pPWoJjVz4FJ
a=ice-options:trickle renomination
// DTLS 信息,参考 RFC 文档: https://tools.ietf.org/html/rfc4572#section-5
a=fingerprint:sha-256 5F:78:37:05:D7:83:46:05:F7:3F:17:35:2A:7E:81:D3:2D:26:71:87:8B:9F:57:02:53:30:E3:3E:B6:3E:49:D5
// a=setup:<role>
// role可选active/passive/actpass/holdconn,
// 分别表示端点将发起一个传出连接、端点将接受传入连接、
// 端点愿意接受传入连接或启动传出连接、端点暂时不想建立连接
// 参考 rfc: https://tools.ietf.org/html/rfc4145#section-4
a=setup:actpass
// a=mid:<token>
// 这个 token 在 a=group 那一行中也有出现,
// 也就是说这里描述的媒体正是需要被 bundle 的
// 参考 rfc: https://tools.ietf.org/html/rfc5888#section-6
a=mid:audio
// 以下是这个媒体支持的所有 RTP 扩展头,
// 参考rfc: https://tools.ietf.org/html/rfc8285
// a=extmap:<value>["/"<direction>] <URI> <extensionattributes>
// value=ID
// direction 可选 sendonly/recvonly/sendrecv/inactive,默认值 sendrecv
// URI 就是这个扩展头的 URI,通信双方可以通过 URI 标明扩展头的含义让双方都能理解
// 这里表示 ID=1 的扩展头是 audio level 扩展头,表示 RTP 包中会携带音频包音量大小
// 参考 https://tools.ietf.org/html/rfc6464#section-4
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
// rtp stream 信息,参考 rfc: https://tools.ietf.org/html/draft-ietf-avtext-rid-09
a=extmap:13 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
// 流的方向,sendrecv 表示可以收也可以发
// 参考 rfc:https://tools.ietf.org/html/rfc3264
a=sendrecv
// 这一行表示 rtcp 和 rtp 复用一个端口,
// 参考 rfc:https://tools.ietf.org/html/rfc5761
// 和 rfc:https://tools.ietf.org/html/rfc8035
a=rtcp-mux
// a=rtpmap:<payload type> <encoding name>/<clock rate> [/<encoding parameters>]
// opus codec 的 payload,
// 表明 fmt=111 就是用来传输 opus 数据的
// 参考 rfc: https://datatracker.ietf.org/doc/html/rfc7587
a=rtpmap:111 opus/48000/2
// a=rtcp-fb:<payload type> [...]
// 表示支持的 rtcp 反馈报文类型
// 这个反馈报文是 tcc 带宽探测用的
// 参考 https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=rtcp-fb:111 transport-cc
// nack,表示 fmt=111 支持 nack 重传包
a=rtcp-fb:111 nack
// a=fmtp 用来描述 codec 的一些特性,例如这里表示期望的 opus 最小打包时间是 10ms,并且使用 inbandfec
a=fmtp:111 minptime=10;useinbandfec=1
// 指明了音频 RTX 包的 payloadtype
// 参考 rfc:https://tools.ietf.org/html/rfc4588#section-8.6
a=rtpmap:114 rtx/48000/2
// apt 表示 fmt=114 的 RTX 包是用来重传 fmt=111 音频的
a=fmtp:114 apt=111
// 指明了 rsfec 包的 payloadtype
a=rtpmap:123 rsfec/48000/2
// 指明了 red 包的 payloadtype
// 参考 https://tools.ietf.org/html/rfc2198
a=rtpmap:124 red/48000/2
// 指明了音频 RTX 包的 payloadtype
a=rtpmap:125 rtx/48000/2
// apt 表示 fmt=125 的 RTX 包是用来重传 fmt=124 的 red 包的
a=fmtp:125 apt=124
// ssrc-group 指明了一组 ssrc 之间的关系,FID 表明后一个 ssrc 是前一个 ssrc 的 rtx
// https://tools.ietf.org/html/rfc5576#section-4.2
a=ssrc-group:FID 2952055605 1713037948
// cname 的内容是一个 16 位 Base64 字符串,含义是传输级的标识符,同一个 PeerConnection 的值相同
// 参考 https://datatracker.ietf.org/doc/html/rfc8834#section-4.9
a=ssrc:2952055605 cname:vqdagKn92E0lhuXn
// 这里出现了两个字符串,
// 前一个是 media stream id,后一个是 sender track id
// media stream 主要用于音视频同步,每个 track 以 media stream id 作为 sync label 进行同步
// 参考 https://datatracker.ietf.org/doc/html/draft-ietf-mmusic-msid
a=ssrc:2952055605 msid:34b34ced3c5623ea4213vx3 34b34ced3c5623ea4213vx3a0
// media stream id
a=ssrc:2952055605 mslabel:34b34ced3c5623ea4213vx3
// sender track id
a=ssrc:2952055605 label:34b34ced3c5623ea4213vx3a0
// video media
m=video 10 UDP/TLS/RTP/SAVPF 96 97 101 102 103
c=IN IP4 0.0.0.0
a=rtcp:10 IN IP4 0.0.0.0
a=ice-ufrag:aZ/b
a=ice-pwd:3tFwvgPAA2PK3pPWoJjVz4FJ
a=ice-options:trickle renomination
a=fingerprint:sha-256 5F:78:37:05:D7:83:46:05:F7:3F:17:35:2A:7E:81:D3:2D:26:71:87:8B:9F:57:02:53:30:E3:3E:B6:3E:49:D5
a=setup:actpass
a=mid:video
// 传输时间偏移扩展头
// 参考 https://datatracker.ietf.org/doc/html/rfc5450
a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
// abs-send-time 扩展头,gcc 带宽探测用的
a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
// 视频朝向扩展头
// 参考 https://datatracker.ietf.org/doc/html/rfc6184
a=extmap:4 urn:3gpp:video-orientation
// transport-cc 扩展头,tcc 带宽探测用的
a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
// 扩展头的内容是对播放延迟限制的值
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
// 视频内容类型扩展头
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
// 这个扩展头用于传输每帧的时间信息
a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
// 视频的色域空间扩展头
a=extmap:12 http://www.webrtc.org/experiments/rtp-hdrext/color-space
// 传输视频 SDES 信息的扩展头
// 参考:https://datatracker.ietf.org/doc/html/draft-ietf-avtext-rid-06
a=extmap:13 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=sendrecv
a=rtcp-mux
// 支持 rtcp 压缩
// 参考 https://datatracker.ietf.org/doc/html/rfc5506#section-1
a=rtcp-rsize
// 指明 fmt=96 就是用来传输 H264 编码的视频的
a=rtpmap:96 H264/90000
// remb 反馈报文,gcc 带宽探测用的
a=rtcp-fb:96 goog-remb
a=rtcp-fb:96 transport-cc
// FIR(完整帧内请求)反馈报文
// 参考 https://datatracker.ietf.org/doc/html/rfc5104
a=rtcp-fb:96 ccm fir
a=rtcp-fb:96 nack
// PLI NACK 反馈报文
// 参考 https://datatracker.ietf.org/doc/html/rfc5104
a=rtcp-fb:96 nack pli
// 后面的是一些 H264 的参数
a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96;packetization-mode=1
a=rtpmap:101 red/90000
a=fmtp:101 packetization-mode=1
a=rtpmap:102 rtx/90000
a=fmtp:102 apt=101;packetization-mode=1
a=rtpmap:103 rsfec/90000
a=fmtp:103 packetization-mode=1
// ssrc-group:SIM 表示后面的这些 ssrc 是同一个流的 simulcast
a=ssrc-group:SIM 2955842370 1032318052
a=ssrc-group:FID 2955842370 521905126
a=ssrc-group:FID 1032318052 1492521545
a=ssrc:2955842370 cname:vqdagKn92E0lhuXn
a=ssrc:2955842370 msid:34b34ced3c5623ea4213vx3 34b34ced3c5623ea4213vx3v0
a=ssrc:2955842370 mslabel:34b34ced3c5623ea4213vx3
a=ssrc:2955842370 label:34b34ced3c5623ea4213vx3v0
a=ssrc:1032318052 cname:vqdagKn92E0lhuXn
a=ssrc:1032318052 msid:34b34ced3c5623ea4213vx3 34b34ced3c5623ea4213vx3
a=ssrc:1032318052 mslabel:34b34ced3c5623ea4213vx3
a=ssrc:1032318052 label:34b34ced3c5623ea4213vx3v0
a=ssrc:521905126 cname:vqdagKn92E0lhuXn
a=ssrc:521905126 msid:34b34ced3c5623ea4213vx3 34b34ced3c5623ea4213vx3v0
a=ssrc:521905126 mslabel:34b34ced3c5623ea4213vx3
a=ssrc:521905126 label:34b34ced3c5623ea4213vx3v0
a=ssrc:1492521545 cname:vqdagKn92E0lhuXn
a=ssrc:1492521545 msid:34b34ced3c5623ea4213vx3 34b34ced3c5623ea4213vx3v0
a=ssrc:1492521545 mslabel:34b34ced3c5623ea4213vx3
a=ssrc:1492521545 label:34b34ced3c5623ea4213vx3v0
// 使用的 rsfec 的版本
a=rsfec-version:1
五、实时传输协议——RTP协议
什么是RTP?
RTP定义:Real-time Transport Protocol,是由IETF的多媒体传输工作小组于1996年在RFC 1889中公布的。RTP为IP上的语音、图像等需要实时传输的多媒体数据提供端对端的传输服务,但本身无法保证服务质量(QoS),因此,需要配合实时传输控制协议(RTCP)一起使用。
RTCP定义:Real-time Transport Control Protocol,监控服务质量并传送会话参与者信息,服务器可利用RTCP数据包信息改变传输速率、负载数据类型。
RTP相关概念介绍
流媒体:使用流式传输技术的连续时基媒体。使用流式传输可以边下载边播放,无需等待音频或视频数据信息全部下载完成后再播放。
混频器(Mixer):一种中间系统,将一个或多个源的RTP数据包合成一个新的RTP数据包,然后转发出去。混频器可能会改变数据包的数据格式,并对各个流组合的新数据包生成一个新SSRC。
转换器(Translator):一种中间系统,转发RTP数据包但不改变数据包的同步源标识符,可用于通过IP多播无法直接到达的用户区,如在防火墙两端使用转换器,外侧转换器通过安全连接将数据传输到内侧转换器。
RTP利用混频器和转换器完成实时数据传输,混频器接收来自一个或多个发送方的RTP数据包,并把它们组合成一个新的RTP数据包继续转发。这个组合数据包使用新的SSRC标识,组合数据包将作为新的发送方加入到RTP传输中。混频器将不同的媒体流组合在一起,需要通过转换器来对单个媒体流进行操作,可进行编码转换或协议翻译。典型的RTP数据包传输流程如下图所示,其中S1、S2、S3、S4是数据源的发送端,R4是RTP数据包的接收端。
RTP工作原理
RTP和RTCP位于传输层,但运行在UDP协议之上。UDP协议实时性更好,可减少数据传输延时,另外,应用程序在UDP上运行RTP还可利用UDP的多路复用,校验和服务。
RTCP向RTP会话中的所有成员周期性的发送控制包,RTCP使用和RTP数据包相同的传输机制。RTP会话使用合法的偶数端口(2n),对应的RTCP包使用下一个奇数端口(2n+1)。
RTP报文分析
每帧RTP报文都是由头部(Header)和负载数据(Payload)两部分组成,头部前12个字节固定,存在于每一个RTP数据包中,最后的CSRC列表只在Mixer中使用。负载数据可以是音频数据或视频数据。
版本号(V):2比特,用来标志使用的RTP版本。
填充位(P):1比特,如果该位置位,则该RTP包的尾部就包含附加的填充字节。
扩展位(X): 1比特,如果该位置位的话,RTP固定头部后面就跟有一个扩展头部。
CSRC计数器(CC):4比特,含有固定头部后面跟着的CSRC的数目。
标记位(M): 1比特,该位的解释由配置文档(Profile)来承担.
载荷类型(PayloadType): 7比特,标识了RTP载荷的类型。
序列号(SN):16比特,每发送一个 RTP 数据包,序列号增加1。接收端可以据此检测丢包和重建包序列。
时间戳(Timestamp): 2比特,记录了该包中数据的第一个字节的采样时刻。在一次会话开始时,时间戳初始化成一个初始值。即使在没有信号发送时,时间戳的数值也要随时间而不断地增加(时间在流逝嘛)。时钟频率依赖于负载数据格式,并在描述文件(profile)中进行描述。
同步源标识符(SSRC):32比特,同步源就是指RTP包流的来源。在同一个RTP会话中不能有两个相同的SSRC值。该标识符是随机选取的 RFC1889推荐了MD5随机算法。
贡献源列表(CSRC List):0~15项,每项32比特,用来标志对一个RTP混合器产生的新包有贡献的所有RTP包的源。由混合器将这些有贡献的SSRC标识符插入表中。SSRC标识符都被列出来,以便接收端能正确指出交谈双方的身份。
RTCP报文分析
RTP只负责传输数据包,需要与RTCP配合使用,由RTCP来保证RTP数据包的服务质量。RTCP的主要功能:服务质量的监控和反馈、媒体设备间的同步以及多播组中的成员标识。在RTP会话期间,各参与者周期性传送RTCP数据包,RTCP数据包中包含已发送的数据包数量、丢失的数据包数量等信息,各参与者通过这些信息动态改变传输速率或传输的数据类型。
RTCP可划分成五种类型,如下表所示:
类型 | 名称 | 用途 |
---|---|---|
200 | SR | 发送报告 |
201 | RR | 接受报告 |
202 | SDES | 源描述 |
203 | BYE | 结束传输 |
204 | APP | 特定应用 |
RTP和RTCP协作传输多媒体数据的流程图如下图所示,在一个RTP会话中有发送端和接收端,发送端将数据封装到RTP中发送,同时以一定的时间间隔周期性发送RTCP中的发送报告(SR),也收到接收端发过来的接收报告(RR)或从其他发送端发送过来的发送报告(SR)。接收端获取到RTP数据包后解析数据,取得应用数据,获取其他参与者发送的发送报告(SR),接收数据的同时通过发送接收报告(RR)将RTCP反馈信息发送出去。