摘要
会话发起协议(Session Initiation Protocol,SIP)是一个重要的信令协议,它正在迅速被电信业采用以构建下一代应用程序。Java是用于SIP开发的极好平台,尤其是在进行服务器端开发时。类似于HTTP servlet,SIP Servlet API使SIP服务的开发变得更轻松。本文将介绍SIP servlet技术,并提供一个带注释的例子。
简介
即时消息传递正在改变人们的生活。它是一个非常有用的工具,结合了电子邮件、Internet电话以及文件传输应用程序的优点。用户甚至可以看到谁在线、谁的状态为“忙碌”。当然了,人们可以用它来长时间地进行不创造任何效益的聊天。但是,员工也可以利用它在老板会见客户时向他发送极为重要的信息。
所以,市场上出现如此多的不同种类的即时消息传递应用程序也就不足为怪了。有这么多的选择应该是一件好事,可是如果员工使用的应用程序与老板使用的不同,那又会怎么样呢?这将是一个大问题,因为这些应用程序大多都使用专有的协议。
SIP为我们带来了福音。SIP很有可能会成为标准的即时消息传递协议。
在本文中,我将开发一个简单的SIP应用程序——一个允许SIP即时messenger (消息传递应用程序)彼此联系并互相传播消息的聊天室服务器端。
SIP SIMPLE
SIMPLE ,即SIP Instant Messaging and Presence Leveraging Extension(SIP即时消息和现场支持扩展)的缩写,是一个工作组以及一组SIP扩展。其中的一个扩展是MESSAGE消息。可以用它来发送包含文本和二进制内容的任意组合的即时消息。这种消息使用起来非常简单,这也是我决定使用它来开发第一个SIP应用程序的原因。
TextClient
为了测试我们的应用程序,我提供了一个小型SIP即时messenger应用程序(参见文章结尾处的“下载”部分)。该应用程序向其他messenger或服务器发送MESSAGE消息。用户界面中包含了客户端的地址、好友地址的输入字段、一个文本消息以及一个提交按钮。图1显示了正在运行的TextClient。
图1. 运行中的TextClient |
要启动TextClient,只需使用以下命令:
java -jar textclient.jar dev2dev.textclient.TextClient username port
该命令使用JAIN SIP API参考实现作为一个SIP协议栈。我们提供了该工具的源代码,如果您希望了解更多,我推荐您读一下源代码。
ChatRoomServer
下面是示例应用程序的需求
聊天室是一个虚拟空间,不同的即时messenger应用程序可以在其中进行交互。传入聊天室的消息将向聊天室中其他所有的人进行广播。换句话说,所有的消息都可以被所有用户看到。这意味着,当一个消息到达服务器端应用程序时,用户的地址将被添加到一个列表中。然后消息将被发送到该列表中的所有用户。
此外,还可以实现“命令”。命令以正斜杠(/)开头,它不被广播,而是由服务器自己处理,用于特定功能。我将实现的命令包括:
/join:默默地进入一个聊天室,不广播任何消息。
/who:打印一份该聊天室所有用户的列表。
/quit:离开聊天室,不再有消息传入。
SIP Servlet API
SIP Servlet API (JSR 116)是一个服务器端接口,它描述了一个SIP组件或服务的容器。这正适合用于开发ChatRoomServer。下载该规范,并解压缩。生成的文件夹包括一些库(servlet.jar、sipservlet.jar)以及文档。我无法获得运行示例SIP servlet的参考实现,所以我想您也不必费心去找它了。
SIP servlet最核心的概念是包含。SIP服务是部署或运行在在一个容器或SIP应用服务器上的打包SIP servlet。容器提供了可供应用程序使用的许多服务,比如自动重试、消息调度和排队、分流和归并,以及状态管理。应用程序中只需包含高级的消息处理和业务逻辑。这使SIP服务的开发成为一件轻而易举的事情。
本文的目的不是要提供对SIP Servlet API技术的全面介绍。因此我只简要概述了该API和示例代码,更多信息请参见文章结尾处的“参考资料”部分。
服务器端代码
如果您曾经开发过HTTP servlet,那么服务器端的代码会让您感到非常熟悉。如果您还不知道什么是servlet,您应该首先了解一下。SIP Servlet规范是HTTP Servlet规范的扩展。其语法、容器行为,甚至方法名都是相似的。
下面我将详细分析该例子。它主要由3个部分组成:
生命周期方法
这些方法在启动或关闭servlet时被容器调用:
public class ChatRoomServer extends SipServlet { /** Context attribute key to store user list. */ public static String THE_LIST="dev2dev.chatroomserver.userList"; /** Init parameter key to retrieve the chat room's address. */ public static String THE_NAME="dev2dev.chatroomserver.name"; /** This chat room server's address, retrieved from the init params. */ public String serverAddress; /** This is called by the container when starting up the service. */ public void init() throws ServletException { super.init(); getServletContext().setAttribute(THE_LIST,new ArrayList()); serverAddress = getServletConfig().getInitParameter(THE_NAME); } /** This is called by the container when shutting down the service. */ public void destroy() { try { sendToAll(serverAddress, "Server is shutting down -- goodbye!"); } catch (Throwable e) { //ignore all errors when shutting down. e.printStackTrace(); } super.destroy(); } ...
在初始化方法中,我创建了一个所有会话共享的全局属性。这是用户的列表。我还获得了该聊天室的地址(servlet参数)以备将来使用。
SIP servlet与HTTP servlet稍有不同。对于HTTP servlet,您处理传入的请求,并发送响应消息。而对于SIP servlet,可以发送和接收请求和响应。我将说明如何做到这一点。
当收到消息(请求或响应)时,容器将调用下面的方法。容器将按照下面图表的顺序调用这些方法,也可以重写这些方法来根据消息的类型处理消息:
void service(ServletRequest,ServletResponse) 如果对其进行重写,不要忘记调用super.service()。 其默认实现调用以下方法之一: | |
---|---|
void doRequest(SipServletRequest) 如果对其进行重写,不要忘记调用super.doRequest()。 其默认实现调用以下方法之一: |
void doResponse(SipServletResponse) 如果对其进行重写,不要忘记调用super.doResponse()。 其默认实现调用以下方法之一:: |
下列请求方法之一(自解释):
|
下列响应方法之一:
|
例如,MESSAGE可以调用以下方法:
- service(),传入一个SipServletRequest(必须进行类型转换)以及null
- doRequest()
- doMessage()
通常只重写最后一级的方法,除非使用了非标准的SIP消息,或者希望收集有关消息的统计信息。
下面是处理即时消息的代码:
/** This is called by the container when a MESSAGE message arrives. */ protected void doMessage(SipServletRequest request) throws ServletException, IOException { request.createResponse(SipServletResponse.SC_OK).send(); String message = request.getContent().toString(); String from = request.getFrom().toString(); //A user asked to quit. if(message.equalsIgnoreCase("/quit")) { sendToUser(from, "Bye"); removeUser(from); return; } //Add user to the list if(!containsUser(from)) { sendToUser(from, "Welcome to chatroom " + serverAddress + ". Type '/quit' to exit."); addUser(from); } //If the user is joining the chat room silently, no message //to broadcast, return. if(message.equalsIgnoreCase("/join")) { return; } //We could implement more IRC commands here, //see http://www.mirc.com/cmds.html sendToAll(from, message); } /** * This is called by the container when an error is received * regarding a sent message, including timeouts. */ protected void doErrorResponse(SipServletResponse response) throws ServletException, IOException { super.doErrorResponse(response); //The receiver of the message probably dropped off. Remove //the receiver from the list. String receiver = response.getTo().toString(); removeUser(receiver); } /** * This is called by the container when a 2xx-OK message is * received regarding a sent message. */ protected void doSuccessResponse(SipServletResponse response) throws ServletException, IOException { super.doSuccessResponse(response); //We created the app session, we have to destroy it too. response.getApplicationSession().invalidate(); }
第一个方法在收到一个MESSAGE消息时被调用。最初以一条200 OK消息响应,表明收到了消息。然后它处理服务器命令,比如/join。最后,它调用一个业务逻辑方法来广播传入的消息。
传入的错误响应消息表明上一个请求失败了。这可能意味着有一个用户被断开了。只需将该用户从列表中移除即可。
成功的响应消息表明上一个MESSAGE消息被即时messenger正确地接收了。因此不再需要该会话,可以将其删除了。通常,MESSAGE消息是以无状态的形式发送的,并不保存消息之间的连接信息。(对于INVITE消息来说,情况不是这样的,它打开一个有状态的会话直到发送BYE。)
其余的代码由helper方法组成。前两个方法向即时messenger发送消息。要发送消息,使用一个工厂创建以下两项:
- 一个SipApplicationSession(稍后将详细介绍)
- 一个请求消息
此时,可以随心所欲地修改消息。在我们的例子中,我们在有效负载中添加即时消息文本。最后,发送该消息。
private void sendToAll(String from, String message) throws ServletParseException, IOException { SipFactory factory = (SipFactory)getServletContext(). getAttribute("javax.servlet.sip.SipFactory"); List list = (List)getServletContext().getAttribute(THE_LIST); Iterator users = list.iterator(); while (users.hasNext()) { //Send this message to all on the list. String user = (String) users.next(); SipApplicationSession session = factory.createApplicationSession(); SipServletRequest request = factory.createRequest(session, "MESSAGE", serverAddress, user); String msg = from + " sent message: " + message; request.setContent(msg.getBytes(), "text/plain"); request.send(); } } private void sendToUser(String to, String message) throws ServletParseException, IOException { SipFactory factory = (SipFactory)getServletContext(). getAttribute("javax.servlet.sip.SipFactory"); SipApplicationSession session = factory.createApplicationSession(); SipServletRequest request = factory.createRequest(session, "MESSAGE", serverAddress, to); request.setContent(message.getBytes(), "text/plain"); request.send(); } private boolean containsUser(String from) { List list = (List)getServletContext().getAttribute(THE_LIST); return list.contains(from); } private void addUser(String from) { List list = (List)getServletContext().getAttribute(THE_LIST); list.add(from); } private void removeUser(String from) { List list = (List)getServletContext().getAttribute(THE_LIST); list.remove(from); } }
部署描述符
对于HTTP servlet,还必须编写web.xml部署描述符。而在SIP servlet中,对应的文件是sip.xml,我们在其中列出SIP servlet、初始化参数以及映射(哪个SIP servlet处理哪些SIP消息)。关于该文件语法的更多信息,请参见SIP Servlet规范中15.5节的DTD。其语法类似于web.xml,但
注意,该映射只用于初始请求;同一个会话/对话中的后续请求由处理初始请求的同一servlet处理。
下面是用于ChatRoomServer的XML代码:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE sip-app PUBLIC "-//Java Community Process//DTD SIP Application 1.0//EN" "http://www.jcp.org/dtd/sip-app_1_0.dtd"> <sip-app> <servlet> <servlet-name>ChatRoomServer</servlet-name> <servlet-class>dev2dev.chatroomserver.ChatRoomServer</servlet-class> <init-param> <param-name>dev2dev.chatroomserver.name</param-name> <!-- This will be replaced by the build script --> <param-value>sip:chatroomname</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>ChatRoomServer</servlet-name> <pattern> <and> <equal> <var>request.uri.user</var> <!-- This will be replaced by the build script --> <value>chatroomname</value> </equal> <equal> <var>request.method</var> <value>MESSAGE</value> </equal> </and> </pattern> </servlet-mapping> </sip-app>
代码看起来很复杂,其实并非如此。Servlet映射说明了:
如果请求URI的用户名部分等于chatroomname,则将传入的MESSAGE请求映射到ChatRoomServer Servlet。
该聊天室名称只是一个占位符。在编译过程中,会用实际的聊天室名称替换关键字“chatroomname”。
这么做有什么用呢?您可以将同样的服务部署多次,每次都使用其独有的聊天室名称,而消息可以自动发送到相应的servlet。
构建、打包、部署
需要对SIP servlet进行编译,并将其打包到SAR(Servlet ARchives)文件中。这些文件在功能上等效于WAR文件,结构也相同。参见图2:
图2. SAR文件结构 |
最后一步是部署,这根据SIP应用服务器的不同而不同。通常需要将SAR文件复制到一个部署文件夹中,然后部署应用程序。
下面的Ant脚本可以帮助部署:
<project name="ChatRoomServer" default="build" basedir="."> <!-- Change this to specify the name of the chat room. In order to send messages to this chat room, simply deploy just4fun.sar, and use the address sip:just4fun.0.2.5060:5060. --> <property name="chatroomname" value="just4fun" /> <!-- Change this to the address and port of the SIP server. --> <property name="serveraddress" value="10.0.2.69:5060" /> <!-- Change this to the location of the SAR deployment folder. --> <property name="sar.deployment" value="" /> <property name="src" value="/src" /> <property name="lib" value="/lib" /> <property name="tmp" value="/tmp" /> <path id="classpath"> <fileset dir=""/> </path> <target name="build"> <mkdir dir=""/> <mkdir dir="/WEB-INF"/> <mkdir dir="/WEB-INF/classes"/> <mkdir dir="/WEB-INF/lib"/> <javac debug="true" srcdir="" destdir="/WEB-INF/classes"> <classpath refid="classpath"/> </javac> <copy todir="/WEB-INF" file="/sip.xml"/> <replace file="/WEB-INF/sip.xml" token="chatroomname" value=""></replace> <replace file="/WEB-INF/sip.xml" token="serveraddress" value=""></replace> <zip destfile="/.sar"> <zipfileset dir=""/> </zip> <copy file="/.sar" todir=""/> </target> </project>
结果
聊天室应用程序运行之后,试着通过运行两个TextClient实例来访问它。要确保运行在同一机器上的SIP应用程序使用的是不同的端口。下面的例子显示了运行在同一机器上的3个应用程序:
- 运行在ChatRoomServer上的SIP应用服务器,地址是sip:just4fun@10.0.2.69:5060。
- 地址为sip:snoopy71@10.0.2.69:5061的Text client。
- 地址为sip:maria119@10.0.2.69:5062的Text client。
图3显示了结果。
图3. TextClient与ChatRoomServer交互 |
复杂应用程序
我知道本文中的例子相对于我们通常要构建的应用程序来说有点过于简单了,现实中的大多数SIP应用程序都由大量代码组成。
会话和状态:通常,SIP应用程序是一个状态机(state machine),其中呼叫或会话都是长时间保持的(有状态的),直到断开。对于SIP servlet,呼叫是由SipApplicationSession表示的,它可以带属性(状态)。在呼叫中,每个会话(呼叫的分支)由SipApplicationSession中的SipSession表示。(两人间的back-to-back会话要使用一个SipApplicationSession和两个SipSession。会议呼叫可能包含更多的SipSession。这些都可以带属性。容器会根据消息的上下文自动提供相应的会话对象。
分层设计:最糟糕的是将所有的代码放入单个的大型SIP servlet。应该按照相对独立的层来设计复杂的应用程序。一个明显的层就是包括连接池的数据库层。也可以包含一个与SIP信令分离的业务逻辑层。另一个方面是有效负载分析,它应该被构建为一个可重用的层。
其他技术:存在许多先进的SIP servlet技术,包括请求代理、重定向和循环、会话超时管理、身份验证、国际化、TCP支持、计时器、会话监听程序以及错误管理。很明显,本文并没有涵盖所有这些方面,但是您可以在SIP Servlet规范中找到相关内容。
例子:可以参见“参考资料”部分,其中有可以帮助您了解更复杂的SIP编程的例子。
结束语
标准促进了互操作性,从而促进了协作。而协作——不管它是用于朋友间的轻松聊天,还是用于重要的文件传输——都是一件好事。
SIP是一个大有前途的电信标准,而SIP Servlet API则是轻松快速地开发服务器端SIP应用程序的极佳方式。在本文中,我们通过一个简单的例子,概述了SIP servlet编程。希望通过本文,能够帮助您在协作的道路上迈出一大步。
本文简要介绍了SIP、它的使用场景,以及一些SIP语法。我们还了解了各种与SIP相关的Java技术。尽管本文不够详尽,我还是希望它能够激发您的兴趣,并促使您开始使用它。SIP的时代已经到来,现在使用它可以实现很多很酷的想法。
在本系列文章的第2部分中,我将说明如何使用SIP Servlet API编写一个聊天室应用程序。
参考资料
- SIP Forum——SIP工作组的社区站点
- SIP Center——另一个广受欢迎的社区站点
- RFC 3261——SIP规范
- SIP的相关RFC和草案
- 开源JAIN SIP堆栈
- 开源SIP软件电话