本文仅用于作者学习,疏漏之处在所难免,出了虾米问题拒不负责。欢迎各位牛X踊跃拍砖赐教。
本文的理想读者是熟悉SIP协议但对SIP Servlet还不太了解。能够清楚SIP各个关键要素以及彼此间的逻辑联系和协议实现。如果你只是初学者建议把RFC反复看几遍,感觉就有了,不要老想取巧偷懒,以为看看博客上的介绍就对SIP了如指掌。我当年也是这样跌打滚爬过来的,坑爹的绕了不少弯路,最后还是老老实实看RFC,受益匪浅。废话少说,下面进入正题。
1.    SIP Servlet概念
JSR 289中自己给SIP Servlet下了定义:
A SIP servlet is a Java-based application component which is managed by a SIP servlet container and which performs SIP signaling. Like other Java-based components, servlets are platform independent Java classes that are compiled to platform neutral bytecode that can be loaded dynamically into and run by a Java-enabled SIP application server. Containers, sometimes called servlet engines, are server extensions that provide servlet functionality. Servlets interact with (SIP) clients by exchanging request and response messages through the servlet container.
如果你之前已经使用过Http Servlet,你会对这个定义感到很亲切。简单的理解,SIP Servlet作为一个中间件,能够将你的项目组从一大堆SIP底层协议实现必须的代码中解救出来(前提是你会使用它且能够用的好),使你能够专注于上层业务逻辑的实现。
但这并不意味着SIP Servlet能够让你在完全不懂SIP协议的前提下,自动屏蔽掉所有与SIP有关的实现细节,然后像你期望的那样跑起来。在我看来,它只能减少你搭建SIP协议环境的工作量,但你至少要清楚,它在底层替你做了些什么,又有什么没有为你做(或者说它无法做到),需要你来自行实现。SIP Servlet从设计到实现都为开发者留足了空间(或者说难听点,是给你留了很多协议的后门让你取巧),我在后边的讲解或许会提到一些,所以你必须有足够的实力来驾驭它,而不是被它驾驭。
在这里,需要稍微提及jain sip的概念。Jain sip是一个java的sip协议栈,它几乎为所有java sip的上层开发提供底层支撑。比较通俗的说,Jain sip提供了实现SIP协议最基本的原材料,比如SIP Requests和Response的基本模型、客户端事务和服务器端事务的基本模型等等(具体你可以去看jain sip的文档)。你可以很方便的使用jain sip获取SIP报文中的信息并处理它,但具体的SIP业务逻辑,jain并没有帮你实现,你需要按照自己的RFC 的理解来实现你所需要的SIP功能。所以直接使用协议栈来开发SIP是很辛苦的说。
2.    SIP Servlet入门
一、    基本概念
再次强调,此处需要你已经清晰理解SIP协议中的事务(transaction,唯一标识方法)、会话(Dialog,唯一标识方法)、UAC、UAS、Proxy(SipServlet在1.1版已经明确宣布不再支持无状态代理)、B2BUA、Request(INVITE、REGISTER、BYE、CANCEL、ACK、OPTIONS等)、Response(100-699状态码)等概念。
在我看来,可以分两个视角来将Sip Servlet提供的资源和模型分为两类。
一类是以开发者的视角:
Container(容器,Servlet基本概念,由Jboss、Tomcat等提供)->操纵->
->Applications(应用程序)->操纵->
->Servlets(每个应用程序内含多个Servlets) ->操纵->
->每个Servlet中描述业务逻辑
一类是以用户的角度:
用户操作软件(比如发起一个call)->创建->
->Application 实例 ->创建->
->Application Session ->创建->
->SipSessions(共享Application Session资源,一个SipSession约等于一个Dialog) ->创建和处理->
->Requests and Responses
而这两类在资源处理上又有交叠的部分,但总的来说一个应用程序(Application)根据实际运行情况产生多个Application Instances(应用程序实例)。同时,一个应用程序也包含多个Servlets,每个Servlets都能够操作到这些Application Instances。
下面,我们从开发者的角度讲解,应该如何设计一个SipServlet程序。

上面描述中涉及的关键概念将在下一篇中详细介绍。
先贴一个简单的例子代码,了解一下Servlet的基本结构。是根据mobicents sip servlet的ClickToCall改写的,添加了部分注释,应该理解起来问题不大。

 

   
   
   
   
  1. package org.mobicents.servlet.sip.example; 
  2.  
  3. import java.io.IOException; 
  4. import java.util.HashMap; 
  5. import java.util.Properties; 
  6.  
  7. import javax.naming.Context; 
  8. import javax.naming.InitialContext; 
  9. import javax.naming.NamingException; 
  10. import javax.servlet.Servlet; 
  11. import javax.servlet.ServletConfig; 
  12. import javax.servlet.ServletException; 
  13. import javax.servlet.sip.Address; 
  14. import javax.servlet.sip.SipErrorEvent; 
  15. import javax.servlet.sip.SipErrorListener; 
  16. import javax.servlet.sip.SipFactory; 
  17. import javax.servlet.sip.SipServlet; 
  18. import javax.servlet.sip.SipServletRequest; 
  19. import javax.servlet.sip.SipServletResponse; 
  20. import javax.servlet.sip.SipSession; 
  21. import javax.servlet.sip.annotation.SipListener; 
  22.  
  23. import org.apache.log4j.Logger; 
  24.  
  25. @javax.servlet.sip.annotation.SipServlet(loadOnStartup = 1, applicationName = "ClickToCallAsyncApplication"
  26. @SipListener 
  27. // From和To的所有交互,都通过此应用程序交互 
  28. // 它截取报文,改写头部后转发 
  29. public class SimpleSipServlet extends SipServlet implements SipErrorListener, 
  30.         Servlet { 
  31.  
  32.     private static final long serialVersionUID = 1L; 
  33.     private static Logger logger = Logger.getLogger(SimpleSipServlet.class); 
  34.     private static final String CONTACT_HEADER = "Contact"
  35.     private SipFactory sipFactory; 
  36.  
  37.     public SimpleSipServlet() { 
  38.     } 
  39.  
  40.     @Override 
  41.     public void init(ServletConfig servletConfig) throws ServletException { 
  42.         super.init(servletConfig); 
  43.         logger.info("the simple sip servlet has been started"); 
  44.         try { 
  45.             // Getting the Sip factory from the JNDI Context 
  46.             Properties jndiProps = new Properties(); 
  47.             Context initCtx = new InitialContext(jndiProps); 
  48.             Context envCtx = (Context) initCtx.lookup("java:comp/env"); 
  49.             sipFactory = (SipFactory) envCtx.lookup("sip/org.mobicents.servlet.sip.example.SimpleApplication/SipFactory"); 
  50.             logger.info("Sip Factory ref from JNDI : " + sipFactory); 
  51.  
  52.  
  53.             //将测试机的注册信息添加进入 
  54.             HashMap users = (HashMap) getServletContext().getAttribute("registeredUsersMap"); 
  55.             if (users == null) { 
  56.                 users = new HashMap(); 
  57.             } 
  58.  
  59.             String fromURI = "sip:[email protected]:8889"
  60.             String ContactURI = "sip:[email protected]:8889"
  61.             users.put(fromURI, ContactURI); 
  62.  
  63.             fromURI = "sip:[email protected]:8888"
  64.             ContactURI = "sip:[email protected]:8888"
  65.             users.put(fromURI, ContactURI); 
  66.  
  67.  
  68.             fromURI = "sip:[email protected]:7777"
  69.             ContactURI = "sip:[email protected]:7777"
  70.             users.put(fromURI, ContactURI); 
  71.  
  72.  
  73.             fromURI = "sip:[email protected]:6666"
  74.             ContactURI = "sip:[email protected]:6666"
  75.             users.put(fromURI, ContactURI); 
  76.  
  77.             getServletContext().setAttribute("registeredUsersMap", users); 
  78.  
  79.         } catch (NamingException e) { 
  80.             throw new ServletException("Uh oh -- JNDI problem !", e); 
  81.         } 
  82.     } 
  83.  
  84.     @Override 
  85.     protected void doInvite(SipServletRequest req) throws ServletException, 
  86.             IOException { 
  87.         logger.info("Click2Dial don't handle INVITE by CYX. Here's the one we got :  " + req.toString()); 
  88.  
  89.     } 
  90.  
  91.     @Override 
  92.     protected void doOptions(SipServletRequest req) throws ServletException, 
  93.             IOException { 
  94.         logger.info("Got :  " + req.toString()); 
  95.         req.createResponse(SipServletResponse.SC_OK).send(); 
  96.     } 
  97.  
  98.     @Override 
  99.     // 当服务器收到从To Addr处发回的 2XX响应 
  100.     // 但此时From Addr实际上并没有发出过INVITE,是由Web Servlet处代为发送的 
  101.     // To的Dialog先发送,From的Dialog后发送 
  102.     // Session中的inviteSent attrabute设置 
  103.     // ToDialog收到对应2XX后会调用doSucce***esponse(),顺利处理完成后会将inviteSent set True 
  104.     // FromDialog收到对应2XX后,设置好content SDP后,将两个ACK同时发现两端 
  105.     protected void doSucce***esponse(SipServletResponse resp) 
  106.             throws ServletException, IOException { 
  107.         logger.info("Got OK  by CYX\n"); 
  108.         SipSession session = resp.getSession(); 
  109.  
  110.         if (resp.getStatus() == SipServletResponse.SC_OK) { 
  111.  
  112.             Boolean inviteSent = (Boolean) session.getAttribute("InviteSent"); 
  113.             // 说明后续要发送给From的INVITE已经被发送 
  114.             if (inviteSent != null && inviteSent.booleanValue()) { 
  115.                 return
  116.             } 
  117.             // 获取在WebServlet处填入的FromAddr 
  118.             Address secondPartyAddress = (Address) resp.getSession().getAttribute("SecondPartyAddress"); 
  119.             // FromDialog没有设置secondPartyAddress 
  120.             // 说明收到的是ToDialog发送的2XX 
  121.             if (secondPartyAddress != null) { 
  122.                 // getRemoteParty()获取ToAddr 
  123.                 // 由SipServlet向FromAddr处发起邀请 
  124.                 // 但实际并没有INVITE从to地址对应处发出,而是从服务器对应出口ip处发向from地址 
  125.                 // 此方法同时创建一个新的SipSession通过sipFactory 
  126.                 // The returned request object exists in a new SipSession which 
  127.                 // belongs to the specified SipApplicationSession. 
  128.                 SipServletRequest invite = sipFactory.createRequest(resp.getApplicationSession(), "INVITE", session.getRemoteParty(), secondPartyAddress); 
  129.  
  130.                 logger.info("Found second party -- sending INVITE to " 
  131.                         + secondPartyAddress); 
  132.  
  133.                 // 如果contentType为SDP 
  134.                 // 将response(ToAddr)中得到的content发送给FromAddr 
  135.                 // 因为SDP协商可以发生在(1)INVITE-response 
  136.                 // 也可以发生在(2)response-ACK 
  137.                 // Dialog(From)由于INVITE(WebServlet发送)中没有写入SDP,故采用方式(2) 
  138.                 // Dialog(To)由于收到了From的response,所以采用方式(1) 
  139.  
  140.                 String contentType = resp.getContentType(); 
  141.                 if (contentType.trim().equals("application/sdp")) { 
  142.                     invite.setContent(resp.getContent(), "application/sdp"); 
  143.                 } 
  144.  
  145.                 // 将From的Dialog与To的Dialog关联起来 
  146.                 session.setAttribute("LinkedSession", invite.getSession()); 
  147.                 invite.getSession().setAttribute("LinkedSession", session); 
  148.  
  149.                 // 为To的Dialog创建ACK响应 
  150.                 // 并将ACK响应保存在From的Dialog中 
  151.                 SipServletRequest ack = resp.createAck(); 
  152.                 invite.getSession().setAttribute("FirstPartyAck", ack); 
  153.                 invite.getSession().setAttribute("FirstPartyContent", resp.getContent()); 
  154.  
  155.                 Call call = (Call) session.getAttribute("call"); 
  156.  
  157.                 // The call links the two sessions, add the new session to the call 
  158.                 // call关联了这两个Dialog 
  159.                 call.addSession(invite.getSession()); 
  160.                 // 为From 的Dialog关联call 
  161.                 invite.getSession().setAttribute("call", call); 
  162.  
  163.                 invite.send(); 
  164.  
  165.                 //FromDialog的"InviteSent"属性被置为true 
  166.                 session.setAttribute("InviteSent", Boolean.TRUE); 
  167.             } // 说明收到的是FromDialog收到的2XX 
  168.             else { 
  169.                 String cSeqValue = resp.getHeader("CSeq"); 
  170.  
  171.                 // Returns the index within this string of the first occurrence of the specified substring. 
  172.                 // 即indexOf返回"INVITE"的索引,-1为没有查找到(说明方法不匹配) 
  173.                 if (cSeqValue.indexOf("INVITE") != -1) { 
  174.                     logger.info("Got OK from second party -- sending ACK"); 
  175.  
  176.                     SipServletRequest secondPartyAck = resp.createAck(); 
  177.                     SipServletRequest firstPartyAck = (SipServletRequest) resp.getSession().getAttribute("FirstPartyAck"); 
  178.  
  179. //                  if (resp.getContentType() != null && resp.getContentType().equals("application/sdp")) { 
  180.                     firstPartyAck.setContent(resp.getContent(), 
  181.                             "application/sdp"); 
  182.                     secondPartyAck.setContent(resp.getSession().getAttribute("FirstPartyContent"), 
  183.                             "application/sdp"); 
  184. //                  } 
  185.  
  186.                     firstPartyAck.send(); 
  187.                     secondPartyAck.send(); 
  188.                 } 
  189.             } 
  190.         } 
  191.     } 
  192.  
  193.     @Override 
  194.     protected void doErrorResponse(SipServletResponse resp) throws ServletException, 
  195.             IOException { 
  196.         // If someone rejects it remove the call from the table 
  197.         CallStatusContainer calls = (CallStatusContainer) getServletContext().getAttribute("activeCalls"); 
  198.         calls.removeCall(resp.getFrom().getURI().toString(), resp.getTo().getURI().toString()); 
  199.         calls.removeCall(resp.getTo().getURI().toString(), resp.getFrom().getURI().toString()); 
  200.  
  201.     } 
  202.  
  203.     @Override 
  204.     protected void doBye(SipServletRequest request) throws ServletException, 
  205.             IOException { 
  206.         logger.info("Got bye"); 
  207.         SipSession session = request.getSession(); 
  208.         SipSession linkedSession = (SipSession) session.getAttribute("LinkedSession"); 
  209.         if (linkedSession != null) { 
  210.             SipServletRequest bye = linkedSession.createRequest("BYE"); 
  211.             logger.info("Sending bye to " + linkedSession.getRemoteParty()); 
  212.             bye.send(); 
  213.         } 
  214.         CallStatusContainer calls = (CallStatusContainer) getServletContext().getAttribute("activeCalls"); 
  215.         calls.removeCall(request.getFrom().getURI().toString(), request.getTo().getURI().toString()); 
  216.         calls.removeCall(request.getTo().getURI().toString(), request.getFrom().getURI().toString()); 
  217.         SipServletResponse ok = request.createResponse(SipServletResponse.SC_OK); 
  218.         ok.send(); 
  219.         //notifyStatus("Received Bye request"); 
  220.     } 
  221.  
  222.     /** 
  223.      * {@inheritDoc} 
  224.      */ 
  225.     @Override 
  226.     protected void doResponse(SipServletResponse response) 
  227.             throws ServletException, IOException { 
  228.  
  229.         logger.info("SimpleProxyServlet: Got response:\n" + response); 
  230.         super.doResponse(response); 
  231.     } 
  232.  
  233.     /** 
  234.      * {@inheritDoc} 
  235.      */ 
  236.     public void noAckReceived(SipErrorEvent ee) { 
  237.         logger.info("SimpleProxyServlet: Error: noAckReceived."); 
  238.     } 
  239.  
  240.     /** 
  241.      * {@inheritDoc} 
  242.      */ 
  243.     public void noPrackReceived(SipErrorEvent ee) { 
  244.         logger.info("SimpleProxyServlet: Error: noPrackReceived."); 
  245.     } 
  246.  
  247.     protected void doRegister(SipServletRequest req) throws ServletException, IOException { 
  248.         logger.info("Received register request  by CYX: " + req.getTo()); 
  249.         int response = SipServletResponse.SC_OK; 
  250.         SipServletResponse resp = req.createResponse(response); 
  251.  
  252.         HashMap users = (HashMap) getServletContext().getAttribute("registeredUsersMap"); 
  253.         if (users == null) { 
  254.             users = new HashMap(); 
  255.         } 
  256.  
  257.         getServletContext().setAttribute("registeredUsersMap", users); 
  258.  
  259.         Address address = req.getAddressHeader(CONTACT_HEADER); 
  260.         String fromURI = req.getFrom().getURI().toString(); 
  261.  
  262.         int expires = address.getExpires(); 
  263.         if (expires < 0) { 
  264.             expires = req.getExpires(); 
  265.         } 
  266.         if (expires == 0) { 
  267.             users.remove(fromURI); 
  268.             logger.info("User " + fromURI + " unregistered by CYX\n"); 
  269.         } else { 
  270.             resp.setAddressHeader(CONTACT_HEADER, address); 
  271.             users.put(fromURI, address.getURI().toString()); 
  272.  
  273.             logger.info("User " + fromURI 
  274.                     + " registered with an Expire time of " + expires + "by CYX\n"); 
  275.         } 
  276.  
  277.         //notifyStatus("Received register event"); 
  278.  
  279.         resp.send(); 
  280.     } 
  281. //    /* 
  282. //     * Any SIP Servlets method that wish to trigger an update to the client's page will use this method. 
  283. //     * For example doRegister use it to update the page every time a new user registers. 
  284. //     */ 
  285. //    protected void notifyStatus(String event) { 
  286. //        BlockingQueue eventsQueue = (LinkedBlockingQueue) getServletContext().getAttribute("eventsQueue"); 
  287. ////        if (eventsQueue == null) eventsQueue = new LinkedBlockingQueue(); 
  288. ////        getServletContext().setAttribute("eventsQueue", eventsQueue); 
  289. // 
  290. //        try { 
  291. //            eventsQueue.put(event); 
  292. //        } catch (InterruptedException e) { 
  293. //            e.printStackTrace(); 
  294. //        } 
  295. //    }