SIP Servlet 编程模型
页面: 1, 2

API

SSAPI基于servlet规范,因此大量沿用了Java EE标准。 这种沿用的最大优势之一就是对容器特性(如安全性、映射和环境资源)的声明式使用。 但对于API自身来说,其最重要的优势是在直观结构之后抽象了大量的复杂SIP任务。

Proxy接口就是很好的例子,它表示SIP中的代理功能。 RFC3261提供了对代理行为的详细解释。代理可以是有状态或无状态的;代理在接收到3xx类响应后能自动向响应中的Contact地址(可能是一个或多个)发送请求;代理能通过记录路由节点(Record-Route)确保后续请求按原路径发送;代理可以充当代理多重目的地的分流代理;代理可以并发执行或顺序执行。 所有这些行为都是Proxy对象上的简单属性。 容器管理代理服务器处理所有的低级细节,如查明目标集(基于Resquest-URI或Route头字段)、申请RFC规则(当上游或下游存在一个严格的路由器时)、创建多重客户端事务、关联响应以及选择最佳响应。

下一节将结合实例讨论SSAPI的一些其他重要接口和结构。

掌握API

本节将讨论在SSAPI中详细说明的各种接口。 对应用程序开发人员来说,SSAPI是一个相当简单的API。 SSAPI定义了访问SIP请求和响应消息的接口, SipServletRequest和SipServletResponse接口用于完成此目的。 SIP也利用了SIP URI和SIP地址;通过简易类(URI, SipURI, TelURL, Address)访问SIP头字段来实现这些接口。 由于SIP对话是会话的一种形式,因此还存在一个处理会话的对象类。 这个会话的概念符合API中的SipSession和SipApplicationSession对象。 本文下节将讨论SipServlet接口,后者是从容器到应用程序的入口点。

SipServlet对象

SipServlet类在servlet基本包中扩展GenericServlet类,服务方法向doRequest() 或doResponse()分派SIP消息,doRequest()或doResponse()又为请求(例如doInvite()和doSubscribe())调用一个doXXX()方法或为响应(例如doSuccessResponse()和doErrorResponse())调用一个doXXX()方法。

部署描述符中定义了一个servlet映射元素,利用该部署描述符,开发人员能够在调用servlet之前定义好规则。 JSR116中明确定义了映射规则的语法。下例中的映射元素确保只有当请求是INVITE并且Request-URI的主机部分包含字符串bea.com时才会调用servlet定义规则。

<pattern>
  <and>
    <equal>
      <var>request.method</var>
      <value>INVITE</value>
   </equal>
    <contains ignore-case="true">
      <var>request.from.uri.host</var>
      <value>bea.com</value>
    </contains>
  </and>
</pattern>

通常来说,并发的请求只能访问一个SipServlet对象,因此这显然不是特定于呼叫/会话的数据结构。 通常要使用doXXX()方法在呼叫中控制事务逻辑。 仔细考虑如下代码段:

1: package test;

2: import javax.servlet.sip.SipServlet;
3: import javax.servlet.sip.SipServletRequest;
4: import java.io.IOException;

5: public class SimpleUasServlet extends SipServlet {
6:   protected void doInvite(SipServletRequest req) 
7:      throws IOException {
8:     req.createResponse(180).send();
9:     req.createResponse(200).send();
10:  }
11:  protected void doBye(SipServletRequest req) throws IOException {
12:    req.createResponse(200).send();
13:    req.getApplicationSession().invalidate();
14:  }
15: }

这是一个简单的UAS servlet,在接收到一个传入INVITE请求时将被调用(由类似于以上定义的规则触发)。 容器通过调用doInvite()方法来调用应用程序。 应用程序选择发送一个180响应(第8行),之后再发送一个200响应(第9行)。注意到,应用程序对UAC发送过来的ACK消息并没有反应。 此时,容器在接收到ACK消息后即忽略它。 (若是有状态代理,应用程序会对其执行代理功能) 应用程序只做自己必须做的事。

SIP工厂

从名字可以看出,这个类用于创建各种SSAPI对象,如 Request、SipApplicationSession,和Address对象。 担当UA的应用程序使用这个类创建新请求,通过工厂创建的请求拥有一个新Call-ID(除了B2BUA特定的方法,在此方法中应用程序可以在上游段选择重用Call-ID),并且To头字段中不含有标签。 通过ServletContext的javax.servlet.sip.SipFactory属性能够检索工厂对象。

SIP消息

存在两个SIP消息类: SipSevletRequest和SipServletResponse,分别代表SIP请求(例如INVITE, ACK, INFO)和SIP响应(例如1xx, 2xx,)。 这些消息通过SipServlet类中定义的各种doXXX()方法传送到应用程序。 SIP是一种异步协议,因此当doRequest()被调用时,应用程序不一定要响应请求。 由于对原始请求对象有访问权,应用程序可以稍后再响应请求。

SipServletRequest和SipServletResponse对象派生自基本SipServletMessag对象,后者提供了一些通用附件/增件方法,例如getHeader(), getContent()和setContent()。 SipServletRequest为请求处理定义了一些有用的方法。 SipServletRequest.createResponse()方法创建一个SipServletResponse类的实例,代表对创建它的请求的响应。

类似地,SipServletRequest.createCancel()对先前发送的请求创建一个CANCEL请求。 请注意,当UAC决定不再继续进行呼叫并且没有接收到对原始请求的响应时,将发送CANCEL请求。接收到一个200响应或未接收到一个100响应时就发送CANCEL请求是不正确的。不过幸运的是,SSAPI将会做出补救。UAC应用程序能创建和发送CANCEL请求。 容器将确保只有当接收到1xx类响应并且没有接收到任何>200的响应时才发送CANCEL请求。

SipServletRequest.getProxy()用于获得相关Proxy对象,使应用程序执行各种代理操作。 SipServletRequest.pushRoute(SipURI)方法允许UAC或代理通过SipURI标识的服务器路由请求。 这个方法的作用是在Route头字段列表的顶部为请求添加一个Route头字段。

另一个有用的方法是SipServletRequest.isInitial()。 应用程序对初始和后续请求的处理不一样,因此理解这些概念很重要。 例如,当应用程序接收到一个Re-INVITE请求时,也会把它传送给servlet的doInvite()方法,但isInitial()将会返回false。

初始请求通常是对话以外的请求,容器没有关于它的任何信息。 当容器接收到一个初始请求时,将通过其创建的机制确定使用哪个应用程序进行调用。 这可能会涉及到查找Servlet映射规则。 我们知道,一些请求用于创建对话——因此在创建对话之后接收到的任何请求都属于后续请求。

与SIP中的对话结构紧密相关的是SSAPI中的SipSession对象,这点我们将在下节讨论。 createAck()是SipServletResponse类中的一个具有特定用途的方法,它为接收到的2xx响应(INVITE事务产生)创建ACK请求。容器本身将为INVITE事务中的非2xx响应创建ACK请求。

SipSession

SipSession大致可对应于一个SIP对话。对于UA,它维护在RFC中指定的对话状态,以在对话中正确创建一个后续请求。 如果应用程序担当UA(UAC或B2BUA)并且在处理完初始请求之后需要在对话中发送一个后续请求(如Re-INVITE或BYE),则应使用SipSession.createRequest()方法而不是某个SipFactory方法,否则将会把请求创建在对话之外。

应用程序在SipSession中存放了容器维护的状态,除此之外还存放了需要维护的特定于会话的状态。 应用程序能够在SipSession对象上设定/取消属性;并能根据不同的调用访问这些属性。 SipSession也提供了SipSession.setHandler(String nameOfAServlet),用于指派应用程序中特定的servlet为SipSession获取后续请求。

SipApplicationSession

逻辑上,SipApplicationSession是应用程序的一个实例。 应用程序可能拥有一个或多个与之有关的协议会话。 在JSR 116中,这些协议会话可以是SipSession或HttpSession。应用程序存储在应用程序级有效的数据作为SipApplicationSession的属性。还需特别注意,SipApplicationSession或相关SipSession上设定的任何属性只针对特定应用程序可见。 SSAPI定义的机制允许同一呼叫调用多个应用程序,该属性称为应用合成。 SipApplicationSession提供的getSessions()方法返回与这个应用会话相关的协议会话。 图3显示了SSAPI中不同会话的容器分层结构。

SIP Servlet 编程模型图-3

图3。 SSAPI中的会话。

encodeUri(URI)是SipServletApplication接口中最有趣的方法之一。 该方法用于把SipApplication标识符编码到URI的参数中去。 如果容器接收到含有该URI编码的新请求,即使是不同的呼叫,也会将SipApplicationSession与该请求关联。 这一看似索然无味的方法具备将两个迥然相异的呼叫连接起来的能力,您可以采用很多别出心裁的方式来使用它。 SipApplicationSession也与应用会话计时器相关,这方面的话题在下一节讨论。

Proxy接口

SIP RFC 3261详细说明了代理行为。 简要地说,代理用于向目标转发请求。 然而,代理有不同的性质。 根据是否需要维护SIP事务状态,代理可以是有状态或无状态的。 代理可以决定关键路由节点(record-route),这样代理就能为对话接收后续请求。否则,在决定初始路由后,代理将放弃正在进行的对话。 代理能并行(同时)地或顺序(一个接着一个)地将请求发向多个目的地。 所有代理行为的复杂细节都很好地隐藏在了一个易用的proxy接口之后。 因此路由代理非常简单,就像如下代码段一样:

protected void doInvite(SipServletRequest req)
   throws ServletException, IOException {
      Proxy p = req.getProxy();
      SipURI uri = (SipURI) req.getRequestURI().clone();
      uri.setPort(5081);
        p.proxyTo(uri);
}

本例中,servlet代理用户将请求发送到相同URI,但是所用端口不同。

应用程序计时器

应用程序可以使用SSAPI提供的计时器服务。 TimerService接口可从ServletContext检索,并作为属性使用。 TimerService定义了一个creatTimer(SipApplicationSession appSession, long delay, boolean isPersistent, java.io.Serializable)方法,用于启动应用级计时器。 可以看到,SipApplicationSession与计时器相关联。 当计时器触发应用程序时,定义好的TimerListener被调用,ServletTimer对象放弃计时。通过这些便可以取回SipApplicationSession。 这为计时器终止提供了正确的上下文。

示例

接下来为大家演示一些示例代码,展示如何应用前面学习过的结构。 首先是一个融合应用程序。 如果您还记得,融合应用程序涉及到多个协议(本例使用SIP和HTTP)。

1: <html>
2: <body>
3: <%
4:   if (request.getMethod().equals("POST")) {
5:    javax.servlet.sip.SipFactory factory = 
6:      (javax.servlet.sip.SipFactory)
   application.getAttribute(javax.servlet.sip.SipServlet.SIP_FACTORY);
      
7:    javax.servlet.sip.SipApplicationSession appSession =
8:       factory.createApplicationSession();
   
9:    javax.servlet.sip.Address to = 
10:      factory.createAddress("sip:localhost:5080");
11:   javax.servlet.sip.Address from = 
12:      factory.createAddress("sip:localhost:5060");
   
13:   javax.servlet.sip.SipServletRequest invite =
14:       factory.createRequest(appSession, "INVITE", from, to);
   
15:   javax.servlet.sip.SipSession sess = invite.getSession(true);
16:   sess.setHandler("sipClickToDial");
17:   //invite.setContent(content, contentType);
18:   invite.send();
19:   }
20: %>
   
21: <p>
22: Message sent ...
23: </body>
24: </html>

这是一个JSP页面的例子,可以通过HTTP URL访问它。 本例中的JSP代码需要像SIP servlet一样调用sipClickToDial()。 这个HTTP servlet在一个较高的级别上通过工厂创建了一个SIP请求,并将其发送至一个SIP URI。 这是一个点击拨号(click-to-dial)应用程序的构架,在Web页面上单击鼠标即可发起依次SIP呼叫。

当发送到这个HTTP servlet的是HTTP POST请求时,将会调用SipFactory(第5-6行)。接着创建一个应用会话(第7-8行)。应用程序所有未来的SIP和HTTP的交互都将这个应用会话为中心件。 其目的是发送一个SIP请求(第13-14 行),但在此之前需创建From和To头字段用于形成该INVITE请求。 16行为SipSession(与刚创建的INVITE请求相关)分配了一个处理器,这样可以确保接收到这个INVITE请求的UAS发送的响应会被分派到SIP servlet,以便进行处理。

然而,至本文撰写之日止,一旦控制从创建SipApplicationSession 和相关SipSession的HTTP servlet返回,就没有任何机制能从HTTP的领域内访问SipApplicationSession。 简单地说,在相同HTTP会话的上下文中,任何后续HTTP请求都无权访问SipApplicationSesstion。

我将用另一个简单的例子说明。 在这个例子中,应用程序接收到一个SUBSCRIBE请求,并发出一个NOTIFY请求。 随后应用程序等待NOTIFY接收者的响应。如果在三秒钟之内没有接收到成功响应(2xx级响应),应用程序将采取一些行动(如更新数据库,记录尝试丢失)。

1:public class Sample_TimerServlet extends SipServlet
2:  implements TimerListener {
  
3:  private TimerService timerService;
4:  private static String TIMER_ID = "NOTIFY_TIMEOUT_TIMER";
  
5:  public void init() throws ServletException {
6:    try {
7:      timerService = 
8:        (TimerService)getServletContext().getAttribute
9:          ("javax.servlet.sip.TimerService");
10:    }   
11:    catch(Exception e) {
12:     log ("Exception initializing the servlet "+ e);
13:    }
14:  }
  
15:  protected void doSubscribe(SipServletRequest req)
16:       throws ServletException, IOException {
17:    req.createResponse(200).send();
18:    req.getSession().createRequest("NOTIFY").send();
19:    ServletTimer notifyTimeoutTimer = 
20:      timerService.createTimer(req.getApplicationSession(), 3000, 
21:               false, null);
22:    req.getApplicationSession().setAttribute(TIMER_ID, 
23:             notifyTimeoutTimer);
24:  }
  
25:  protected void doSuccessResponse(SipServletResponse res) 
26:       throws javax.servlet.ServletException, java.io.IOException {
27:    if (res.getMethod().equals("NOTIFY")) {
28:      ServletTimer notifyTimeoutTimer =      
29:        (ServletTimer)(res.getApplicationSession().
             getAttribute(TIMER_ID));
30:      if (notifyTimeoutTimer != null) {
31:        notifyTimeoutTimer.cancel();
32:        res.getApplicationSession().removeAttribute(TIMER_ID);
33:      }
34:    }
35:  }
  
  
36:  public void timeout(ServletTimer timer) {
37:    // This indicates that the timer has fired because a 200 to
38:    // NOTIFY was not received. Here you can take any timeout 
39:    // action. 
40:    // .........
41:    timer.getApplicationSession().removeAttribute
         ("NOTIFY_TIMEOUT_TIMER");
42:  }   
43:}

上例中,servlet自已实现了TimerListener,所以超时将被通报给servlet。 首先应从ServletContext获得TimerService(第7至第9行),然后把获取SUBSCRIBE请求的计时器设定为3秒钟(第20行)。计时器可随时设定。 请注意,可以为计时器附加一个对象,此后便可将计时器作为标识符或可调用消息使用。 本例把定时器标识为字面值。 发送完NOTIFY之后,应用程序创建定时器,并在SipApplicationSession中保存其引用以供以后使用(22行)。此时如果接收到响应NOTIFY的200响应,便可调用定时器引用并取消定时器(第25行);如果未在3秒钟之内接收到200响应,定时器就会触发,容器将调用timeout()回调。 (第36行)。

结束语

SIP是一种构建VoIP网络的新兴协议。 SIP已经得到3GPP (3rd Generation Partnership Project)的最终认定,成为IMS架构的基本协议。SIP servlet编程模型技术越来越多地应用于应用服务器,原因主要在于其易用性和强大的力量。 此外,SIP servlet规范更加贴近Java EE标准,也是创建融合应用程序的首选。 希望本文能够在SIP servlet编程模型这个方面为您打下坚实的基础,并帮助您开始动手使用这些技术。

参考资料

Nasir Khan 是BEA Weblogic SIP Server产品的架构工程师。他在设计和构建可升级企业Java应用程序方面有着十二年的经验。