会话发起协议(Session Initiation Protocol,SIP)是一种信号传输协议,用于建立、修改和终止两个端点之间的会话。SIP 可用于建立 两方呼叫、多方呼叫,或者甚至 Internet 呼叫、多媒体呼叫和多媒体分发的多播会话。JSR 116:SIP Servlet API 是一个服务器端接口,描 述了针对 SIP 组件及服务的容器。SIP servlet 是在 SIP 容器***诵械 servlet,与 HTTP Servlet 类似,但提供了对 SIP 协议的支持。 SIP 和 SIP servlet 是许多基于远程通信的流行应用程序的底层技术,这些应用程序提供了各种服务,比如 Voice-over-IP (VoIP)、即时通 信、在线和好友列表管理,以及网络会议。
SIP 和 SIP servlet 对于企业也很重要。与 Java EE 技术结合,SIP servlet 可用于向企业应用程序添加丰富的媒体交互功能。JSR 289: SIP Servlet v1.1 更新了 SIP Servlet API 并定义了一个标准的应用程序编程模型,用于将 SIP servlet 和 Java EE 组件集成到一起。SIP servlet 将在下一代远程通信服务中扮演更加重要的角色。
本技术文章涵盖了 SIP 和 SIP servlet 的一些基本底层概念。本文还提供了一个示例应用程序,该应用程序使用 SIP servlet 和 HTTP servlet 提供 VoIP 电话服务。
什么是 SIP?
介绍 SIP 的一种简单方法就是从应用场景入手。我们假设用户 A 想要与用户 B 建立一个呼叫。在远程通信设置中,用户 A 和 B 将通过 用户代理进行通信。用户代理的一个例子就是软件电话――用于在 Internet 上建立电话呼叫的软件程序。另一个例子就是 VoIP Phone――一 种使用 VoIP 的电话。下面列出了建立呼叫所需的步骤:
A 邀请 B 开始会话。作为邀请的一部分,A 会说明自己支持的媒体。
B 接收到邀请并向 A 发送一个及时响应,然后对邀请进行评估。
当 B 准备好接受邀请时,它会向 A 发送一个回执。作为回执的一部分,B 将说明自己支持的媒体。
A 分析从 B 收到的回执,并确定 B 和 A 支持的媒体是否相同。如果 A 和 B 支持相同的媒体,则它们之间将建立呼叫。邀请中指定的媒 体可以简化呼叫的建立。
图 1 演示了建立呼叫的步骤。
图 1. 建立呼叫的步骤
SIP 提供了一种标准的方式来执行这些步骤。它通过定义特定的请求方法、响应、响应代码,以及信号传输和呼叫控制的头部来完成这些步 骤。该协议已由 Internet Engineering Task Force (IETF) 根据 RFC3261 实现了标准化,现已被 第 3 代合作伙伴项目(3GPP) 采纳为标 准信号传输协议,还成为了 IP 多媒体子系统(IP Multimedia Subsystem,IMS) 架构中的永久元素。
SIP 与 HTTP 有何关系?
人们通常会问 SIP 是否使用 HTTP 作为底层协议。答案是否定的。SIP 是一种与 HTTP 在同一层(即应用层)运作的协议,它使用 TCP、 UDP 或 SCTP 作为底层协议。但是,SIP 与 HTTP 有很多相似之处。例如,与 HTTP 类似,SIP 基于文本而且是用户可读的。SIP 使用带有特 定方法、响应代码和头部的“请求响应”机制,这一点也与 HTTP 类似。HTTP 和 SIP 的一个显著不同是,SIP 中的“请求响应”机制是异步 的――请求不需要在后面紧跟相应的响应。实际上,一个 SIP 请求可能导致生成一个或多个请求。
SIP 是一种对等协议。这意味着用户代理既可以作为服务器,也可以作为客户机。这是 SIP 和 HTTP 的另一个不同之处,在 HTTP 中,客 户机始终是客户机,而服务器始终是服务器。
SIP 支持以下请求方法和响应代码:
请求方法:
REGISTER。客户机使用它向 SIP 服务器注册一个地址。
INVITE。指示用户和服务器被邀请参与一个会话。此消息的正文包括一个会话描述,用户或服务被邀请参与该会话。
ACK。确认客户端已经接收到 INVITE 请求的最终响应。此方法仅与 INVITE 请求一起使用。
CANCEL。用于取消挂起的请求。
BYE。由用户代理客户机发送,向服务器表明它希望终止呼叫。
OPTIONS。用于向服务器查询与它相关的功能。
响应代码:
1xx:临时用途。表明操作被成功接收、理解和接受的 ACK。
3xx:重定向。需要进一步操作来处理此请求。
4xx:客户机错误。请求包含错误的语法,不能在此服务器上进行处理。
5xx:服务器错误。服务器处理一个明显有效的请求失败。
6xx:全局失败。不能在任何服务器上处理该请求。
会话描述协议
会话描述协议(Session Description Protocol,SDP)是一种描述在多媒体会话中使用的媒体格式和类型的格式。SIP 使用 SDP 作为其消 息中的一个有效载荷,以方便各种用户代理之间的功能交换,例如,SDP 的内容可以指定用户代理支持的编解码器和要使用的协议,比如实时 传输协议(Real-time Transport Protocol,RTP)。
SIP 消息
图 2 展示了 SIP 消息的组成部分。SIP 消息主要包括三部分:
请求行。指定请求方法、地址和 SIP 版本。
头部。指定与要建立或终止的会话或呼叫相关的数据。
消息正文。提供有效载荷,也就是 SDP,描述用于会话的媒体。
图 2. SIP 消息的组成部分
SIP Servlet 模型
SIP servlet 编程模型基于 servlet 编程模型。它使 SIP 中的编程与 Java EE 更加接近。Servlet 是处理传入请求和将合适的响应发送 到客户机的服务器端对象。它们通常部署到 servlet 容器中,并且具有定义良好的生命周期。servlet 容器负责管理容器中 servlet 的生命 周期以及管理与 servlet 所使用技术(比如 JNDI 和 JDBC)相关的资源。servlet 容器还管理 servlet 的网络连接。
如前所述,SIP servlet 与 HTTP Servlet 类似,但前者处理的是 SIP 请求。SIP servlet 通过定义具体方法来处理各 SIP 请求方法。例 如,HTTP servlet 定义 doPost() 方法(该方法覆盖 service() 方法)来处理 POST 请求。比较而言,SIP servlet 定义 doInvite() 方法 (也覆盖了 service() 方法)来处理 INVITE 请求。
JSR116 定义了 SIP Servlet API 1.0。它指定了:
一个用于 SIP servlet 编程模型的 API。
SIP servlet 容器的职责。
SIP servlet 如何与 HTTP servlet 和 Java EE 组件交互。
最初的 SIP Servlet API 规范已被修订为 JSR 289: SIP Servlet v1.1。
SIP Servlet API -- 关键概念
SIP servlet 底层的关键概念与 HTTP servlet 类似。以下各节简短介绍其中的一些概念。
SipServletRequest 和 SipServletResponse
SIP 中的“请求响应”方法与 HTTP servlet 类似。请求在 SipServletRequest 对象中定义,而响应在 SipServletResponse 对象中定义 。但是,只有一个 ServletRequest 或 ServletResponse 对象是非空的。这是因为一个 SIP 请求不会导致对称的响应。还有一个公共的高级 接口,称为 SipServletMessage,SipServletRequest 和 SipServletResponse 对象都可以使用。SipServletMessage 接口定义 SipServletRequest 和 SipServletResponse 对象通用的方法。
图 3 演示了 SipServletRequest 和 SipServletResponse 对象的层次结构。
Servlet 上下文
servlet 规范中定义的 servlet 上下文也适用于 SIP servlet。servlet 规范定义了一些特定的上下文属***,用于存储和检索特定上下文 中的 SIP servlet 和接口信息。servlet 上下文可以与同一个规范中的 HTTP servlet 共享。这一点已在 Converged Applications 一节中详 细解释。
部署描述符
使用一种基于 XML 的部署描述符来描述 SIP servlets、调用它们的规则,以及应用程序中使用的资源和环境属***。这个描述符位于一个 sip.xml 文件中,并且与 HTTP servlet 中使用的文件类似。sip.xml 文件由一个 XMl 模式定义。
SIP 应用程序打包
SIP 应用程序具有与 Web 应用程序相同的打包结构。它们被打包为扩展名为 .sar(Sip 归档文件)或 .war(Web 归档文件)的 JAR 格式 。
融和上下文和融和应用程序
应用程序可以使用 SIP 和 HTTP servlet 创建服务。为了允许在一个应用程序中同时使用 HTTP 和 SIP servlet,SIP servlet 规范定义 了一个 ConvergedContext 对象。这个对象保存 HTTP 和 SIP servlet 共享的 servlet 上下文,并为 HTTP 和 SIP servlet 提供在 servlet 上下文属***、资源和 JNDI 名称空间方面相同的应用程序视图。
当应用程序同时包含 SIP 和 HTTP servlet 时,它就成为了一个融合应用程序(converged application)。这与仅包含 SIP 的应用程序 (称为 SIP 应用程序)是相对的。融合应用程序在结构上与 SIP 应用程序类似,但是除 sip.xml 文件之外,它还使用一个 web.xml 文件作 为部署描述符。在 SIP Servlet API 1.1 (JSR289) 中,融合应用程序概念被扩展为也包括企业应用程序。企业应用程序现在可以包含一个 SIP 应用程序或融合应用程序作为模块。这种类型的企业应用程序被称为融合企业应用程序。
SIP 会话
SIP servlet 规范定义 SipSession 对象来表示基于 SIP 的会话,这与使用 HttpSession 对象表示基于 HTTP 的会话相同。因为单个应用 程序(比如融合应用程序)可以包含基于 HTTP 和 SIP 的会话,所以规范还定义了一个 SipApplicationSession 对象,这是一个应用程序级 别的会话对象。SipApplicationSession 对象在应用程序中担当 HTTP 和 SIP 会话(也就是协议会话)的父会话。
注释
回想一下,SIP Servlet API 1.1 的目标是使 SIP servlet 与 Java EE 5 保持一致。结果,该规范在 SIP servlet 和侦听器中引入了 Java EE 5 定义的注释。它还定义了自定义注释来表示 SIP servlet 规范定义的接口。该规范引入了以下注释:
@SipServlet。用于指示特定类是一个 SipServlet
@SipApplication。用于定义 SIP 应用程序。这个注释拥有一组属***,其中一个是 "name" 属***,该属***用于定义应用程序的名称。 SipApplication 注释可用于为构成应用程序的 servlet 创建一个逻辑集合,而无需使用部署描述符。
@SipListener。允许将某个特定类注册为特定应用程序的 SipListener。SIP 应用程序的名称被定义为此注释的一个属***。
@SipApplicationKey。帮助定义 SIP 应用程序的 SipApplicationKey 的方法层。SipApplicationKey 用于将请求与现有的 SipApplicationSession 关联。
Project Sailfin - 开源的 SIP 应用服务器
SIP servlet 容器可以是独立的,即仅支持 SIP servlet,也可以是同时支持 HTTP 和 SIP servlet 的融合容器。但是,对于大多数企业 应用,SIP servlet 容器必须是应用服务器中的一个融合容器。 Project Sailfin 旨在使用 GlassFish 应用服务器生成 SIP servlet 容器的 开源实现。该项目由 java.net 开发,Sun 和 Ericsson 是主要的贡献者。Sailfin 是在 SailFin 项目中开发的 GlassFish 中的 SIP servlet 容器实现,它支持 SIP Servlet API 1.0 并计划在 SIP Servlet API 1.1 完成之后提供对该规范的支持。
CallSetup 示例应用程序
本文使用的示例应用程序名为 CallSetup,是 SailFin 下载的一部分。您可以从 下载 SailFin 版本 页面下载 SailFin。遵循 SailFin 项目 - 指令 来安装和配置 SailFin。CallSetup 应用程序的代码位于/samples/sipservlet/CallSetup 目录 ,其中是安装 SailFin 的目录。
CallSetup 应用程序使用 SIP servlet 和 HTTP servlet 来提供 VoIP 电话服务。该应用程序借助背靠背用户代理(Back-to-Back User Agent,B2BUA)SIP servlet 来建立 VoIP 呼叫。B2BUA 单独调用每个用户代理,然后将它们连接起来,从而在两个用户代理之间建立呼叫。
CallSetup 组件
CallSetup 包括以下组件:
Registration.java。一个表示注册用户的 Plain Old Java Object (POJO)。
RegistrarServlet。一个允许用户注册的 SIP servlet。该 servlet 还用于在与 SailFin 绑定的 Java DB 数据库中持久化用户数据。
RegistrationBrowserServlet.java。一个 HTTP Servlet,提供了一个接口用于选择呼叫的注册用户。
SipCallSetupServlet.java。一个将 INVITE 消息发送到第一个用户 (UserB) 的 HTTP Servlet。
B2BCallServlet.java。该 SIP Servlet 处理来自第一个用户的响应并与第二个用户(User A)建立呼叫。
web.xml。HTTP servlet 的部署描述符。
sip.xml。SIP servlet 的部署描述符。
sun-web.xml。一个特定于产品的部署描述符。
persistence.xml。定义持久单元。
图 4 展示了应用程序的执行顺序。
图 4. CallSetup 的执行顺序
让我们看一看构成 CallSetup 的组件中的一些代码。此处未显示应用程序的所有组件,也没显示每个组件中的所有代码。建议在 SailFin 下载中研究应用程序的完整代码。
RegistrarServlet.java
当用户代理发送 REGISTER 请求时,doRegister() 方法将被调用并存储注册数据。然后将一个带有状态码的响应发送给用户代理。
import com.ericsson.sip.Registration; @PersistenceContext(name = "persistence/LogicalName", unitName = "EricssonSipPU") public class RegistrarServlet extends SipServlet{ The PersistenceUnit annotation is used to annotate the EntityManagerFactory with the name of the PU to be used. @PersistenceUnit(unitName = "EricssonSipPU") private EntityManagerFactory emf; The Resource annotation is used to inject the UserTransaction @Resource private UserTransaction utx; protected void doRegister(SipServletRequest request) throws ServletException, IOException { SipServletResponse response = request.createResponse(200); try { The SipServletRequest object is parsed to get to address and request headers. The Contact header is obtained and stored in the database SipURI to = cleanURI((SipURI) request.getTo().getURI()); ListIterator<Address> li = request.getAddressHeaders("Contact"); while (li.hasNext()){ Address na = li.next(); SipURI contact = (SipURI) na.getURI(); logger.log(Level.FINE, "Contact = " + contact); An EntityManager object is created for storing the user data. EntityManager em = emf.createEntityManager(); try { utx.begin(); Registration registration = new Registration(); registration.setId(to.toString()); registration = em.merge(registration); em.remove(registration); utx.commit(); logger.log(Level.FINE, "Registration was successfully created."); } catch (Exception ex) { try { utx.rollback(); } catch (Exception e) { } } em.close(); If the registration is successful , a response code of 200 OK is sent response.send(); } catch(Exception e) { If the registration is not successful , a response code of 500 is sent response.setStatus(500); response.send(); } }
RegistrationBrowserServlet.java
这是一个 HTTP Servlet,它提供了一个接口,用于列出注册用户并在两个注册用户之间建立呼叫。
@PersistenceContext(name = "persistence/LogicalName", unitName = "EricssonSipPU") public class RegistrationBrowserServlet extends HttpServlet { @PersistenceUnit(unitName = "EricssonSipPU") private EntityManagerFactory emf; public Collection getRegistrations() { EntityManager em = emf.createEntityManager(); Query q = em.createQuery("select object(o) from Registration as o"); return q.getResultList(); } protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); This gets the list of registrations Collection registrations = getRegistrations(); Iterator iter = registrations.iterator(); out.println(""); Call to the HTTP servlet SipCallSetupServlet out.println(""); out.println(" "); out.println(""); out.println("SipFactoryInstance = "+sf.toString()); out.println(""); out.close(); }
SipCallSetupServlet.java
此 HTTP Servlet 通过 RegistrationBrowserServlet 调用,其行为类似于 B2BUA 在两个用户之间建立呼叫。
public class SipCallSetupServlet extends HttpServlet { SipFactory sf = null; TimerService ts = null; ServletContext ctx = null; public void init(ServletConfig config) throws ServletException { ctx = config.getServletContext(); Getting the SIpFactory object from the ServletContext sf = (SipFactory) ctx.getAttribute(SipServlet.SIP_FACTORY); } protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String callA = null; String callB = null; Getting the contacts from request parameters String[] contacts = request.getParameterValues("CONTACT"); if ( contacts.length < 2 ) { return; } callA = contacts[0]; callB = contacts[1]; Using the SipFactory object to create a SipApplicationSession SipApplicationSession as = sf.createApplicationSession(); Using the SipFactory object to create a "To" and "From" Address objects Address to = sf.createAddress(callB); Address from = sf.createAddress(callA); Creating a SipServletRequest using the SipFactory and the SipApplicationSessionObjects created. Note that INVITE is being sent as if UserA is inviting UserB. SipServletRequest sipReq = sf.createRequest(as, "INVITE", from, to); logger.log(Level.FINE, "SipCallSetupServlet sipRequest = " + sipReq.toString()); Set an attribute in SipServletRequest to indicate that this is an initial INVITE sipReq.setAttribute("CALL","INITIAL"); // set servlet to invoke by response Getting a SipSession from the Request created SipSession s = sipReq.getSession(); This is the key part. We set name of the servlet that would handler the response for the Request being sent. Here b2b is the name of the SIP Servlet that would handle the response to this request. s.setHandler("b2b"); // lets send invite to B ... Sending the request sipReq.send(); }
B2BCallServlet.java
这个 SIP Servlet 接收并处理针对 SipCallSetupServlet 发送的 INVITE 请求的响应。该 servlet 处理响应头部和正文,获取 SDP,并 向其他用户代理发送另一个 INVITE 请求和 SDP 元数据。接收到来自其他用户的带有成功响应代码的响应之后,该 servlet 在两个用户之间 建立呼叫。
public class B2BCallServlet extends SipServlet { SipFactory sf = null; ServletContext ctx = null; public void init(ServletConfig config) throws ServletException { super.init(config); ctx = config.getServletContext(); Get the SipFactory from the ServletContext sf = (SipFactory) ctx.getAttribute(SipServlet.SIP_FACTORY); ts = (TimerService) ctx.getAttribute(SipServlet.TIMER_SERVICE); } protected void doResponse(SipServletResponse resp) throws ServletException, IOException { get the SipApplicationSession and SipServletRequest from the response SipApplicationSession sas = resp.getApplicationSession(true); SipServletRequest origReq = resp.getRequest(); String alreadySent = (String) origReq.getAttribute("SENT_INVITE"); if( alreadySent == null && resp.getContentLength() > 0 && resp.getContentType().equalsIgnoreCase("application/sdp")) { String responseFrom = (String) origReq.getAttribute("CALL"); Check if this an response to INITIAL INVITE sent from the HTTP Servlet, and if there is an SDP sent in the response, create an INVITE to the other user if("INITIAL".equals(responseFrom)) { //Take the SDP and send to A Note that To address in the orginal request is the From address here and vice versa. This is what makes this servlet act like a B2BUA. SipServletRequest newReq = sf.createRequest(sas,"INVITE",origReq.getTo(),origReq.getFrom()); newReq.setContent(resp.getContent(),resp.getContentType()); SipSession ssA = newReq.getSession(true); SipSession ssB = resp.getSession(true); Set the SipSession object as a session attribute to each call leg ssA.setAttribute("OTHER_SESSION",ssB); ssB.setAttribute("OTHER_SESSION",ssA); //Test Set the b2b servlet as the handler for the response for the new request being sent. ssA.setHandler("b2b"); ssB.setHandler("b2b"); origReq.setAttribute("SENT_INVITE","SENT_INVITE"); send the request to the other user newReq.send(); //Send to A } else { If this is a response from User A then get the SDP from User A and set it i SipSession ssB = (SipSession) resp.getSession().getAttribute("OTHER_SESSION"); ssB.setAttribute("SDP",resp.getContent()); } } else { return; } // Count so that both sides sent 200. If response has a 200OK as the status code if( resp.getStatus() == 200 ) { Check if this is a response from the UserB ( first user) SipServletResponse first = (SipServletResponse) sas.getAttribute("GOT_FIRST_200"); if( first == null ) { // This is the first 200 sas.setAttribute("GOT_FIRST_200",resp); } else { //This is the second 200 sen both ACK This is a second response and now we send an ACK to both User A and UserB. This exchanges the SDP and sets up the call. sendAck(resp); sendAck(first); } } } This method sends the ACK with the SDP. private void sendAck( SipServletResponse resp ) throws IOException { SipServletRequest ack = resp.createAck(); //Check if pending SDP to include in ACK Object content = resp.getSession().getAttribute("SDP"); if( content != null ) { ack.setContent(content,"application/sdp"); } ack.send(); } }
sip.xml
sip.xml 文件定义 SIP servlet 并指定它们的映射。SIP servlet 的映射使用 equal、and、or 和 not 运算符来定义调用 servlet 的条 件。在本例中,与此条件匹配的请求方法为 REGISTER、INVITE、OPTIONS 或 MESSAGE。
SIP Registrar
SIP Registrar application
registrar
Registrar SIP servlet
com.ericsson.sip.RegistrarServlet
Registrar_Domain
ericsson.com
1
b2b
com.ericsson.sip.B2BCallServlet
1
registrar
request.uri.host
test.com
request.method
REGISTER
request.method
INVITE
request.method
OPTIONS
request.method
MESSAGE
persistence.xml
persistence.xml 文件定义持久单元 EricssonSipPU,后者用于在数据库中持久化注册数据。应用程序使用 Sailfin 中可用的默认 JDBC 资源 jdbc/__default。
oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider
jdbc/__default
运行示例应用程序
要运行 CallSetup 应用程序,请遵循 SailFin 项目 - 指令 页面上的步骤。如果成功执行了这些步骤,您应该能够在两个 softphone 客 户机之间建立一个呼叫。两个选定端点上的电话应该会振铃。
结束语