本文将提供一个易于使用的方法来开发使用SIP的Java ME应用程序。同时还将检查随Java
Wireless Toulkit发行的用于Java ME (JSR 180)的SIP API。本文还讨论了使用此技术的各种方法。您将看到能够运行于移动电话或仿真器的一个真正的SIP应用程序。
简介:SIP + Java =卓越
移动电话和可连接到Internet的PDA越来越受到人们的欢迎。我的所有朋友都使用它们,并且结合使用了大量新的应用程序,。其中许多程序可以“连网”,不论是客户端/服务器还是点对点设备。
开发可移动的网络应用程序时,需要选择通讯协议。开发者可打开套接字并创建一个完全私有的协议。可使用具有私有API的SOAP,也可使用完全基于标准的方法。鉴于以下原因,我建议使用后者:
- 在包含库的情况下更易进行开发。
- 可提供更多控制,例如:除了根据下载的KB数量外还可根据交互类型收费。
- 移动运营商可阻止非标准协议。
- 可与各种设备进行互操作。
这就是我建议使用SIP进行移动网络编程的原因。SIP是移动运营商使用的标准连接协议。此外,它所使用的库也易于查找和使用。有关SIP简介,请参见介绍性文章:
SIP简介,第1部分:SIP初探 (中文版)和
SIP简介,第2部分:SIP SERVLET(中文版)
使用Java ME为SIP编程非常简单。最新移动库构成了丰富的编程环境,这使得应用程序的开发变得轻而易举。
本文将介绍为移动电话构建简单的messenger移动电话客户端的方式。该方式使用SIP协议并使用Java
ME库进行构建。此应用程序可单独运行,也可将其配置为使用SIP注册器,如BEA WebLogic
SIP Server。
先决条件
要从本文中获取最大收益,必须在开发环境中安装以下工具:
此外,还必须了解一点Java ME知识。有关这些软件的帮助信息,请阅读附录。
MEssenger应用程序
我决定通过开发即时消息传递客户机应用程序来演示SIP在Java ME中的使用。此应用程序虽然简单,但可演示发送消息(REGISTER、MESSAGE)、处理响应和处理收到的消息等功能。
我将此应用程序命名为MEssenger(其发音为“mee-senger”,与Java ME中的“ME”一样,此处的“ME”表示Micro
Edition)。它具有很简单的GUI(两页)。主页可用于收发消息。第二个页面用于配置应用程序。本文没有包含其他有趣的功能。
MEssenger的导航界面如图1所示。输入目标SIP地址和消息并选择menu中的Send后即可发送消息。该应用程序最近的五个事件可显示在下半个屏幕中。配置页面中可输入注册信息。
图1. MEssenger导航
在深入研究应用程序的代码前,我们先看一下应用程序的设计。
设计
我们将要编写的应用程序由以下五种类和接口构成:
|
|
类 |
说明 |
MEssengerMIDlet |
该Java ME应用程序的“主”类。它可创建并显示MenuManager示例。 |
MenuManager |
此类包含页面和导航事件。它还可以实例化SipManager类。此类可实现MessageListener接口。 |
SipManager |
此类包含通信行为;它可收发消息、安排注册。 |
ErrorAlert |
显示错误的实用类。 |
MessageListener |
SipManager使用的接口,请求MenuManager显示消息。这将拆分两个类,可在不使用MenuManager的其他应用程序中重用SipManager。 |
正如我先前说过,本文的目的不是介绍Java ME,因此接下来我将重点介绍SipManager类。有关其他类的详细信息,请参阅文本包含的源代码。
关于Java ME的SIP API
使用Java ME进行SIP编程有些像套接字编程。它将显示打开和关闭客户机和服务器连接、数据流以及线程等概念。我将展示示例所需的所有不同类。首先我将列出此API的一些关键类:
|
|
类 |
说明 |
Connector |
创建各种连接对象的工厂。对于SIP连接,只需使用以“sip:”开头的地址,Connector就可创建SipClientConnection或SipServerConnection
对象。 |
SipClientConnection |
此类用于发送不会反复出现的SIP消息,如INVITE和MESSAGE。 |
SipClientConnectionListener |
此接口必须由需要处理SIP响应的类来执行。 |
SipServerConnectionListener |
此接口必须由计划接收SIP请求的类来执行。 |
SipServerConnection |
此类可读取收到的消息。 |
SipRefreshHelper |
该实用类管理反复发出的SIP消息(如REGISTER和SUBSCRIBE)。 |
SipRefreshListener |
实现该接口可处理反复发出的消息的响应。 |
使用这些类可以完成三种典型的“操作”。本文将依次介绍各操作:
在介绍这些操作前,需要做一些基础工作。我们先来看看如何创建SipManager。
创建SipManager
虽然不必用此方法设计应用程序,我决定将整个SIP消息封装到一个单独的类中,即SipManager。正如前面提到的一样,这是一个可重用的类,没有假定其执行环境。
在现有MIDlet项目中创建新类。称为SipManager。现在使用Java编辑器开始编码。SipManager将实现以下接口:
public class SipManager implements SipServerConnectionListener,
SipRefreshListener, SipClientConnectionListener {
在显示信息时,单个构造函数将保存调用方的引用。它还发起注册(如果此功能已开启)并开始侦听到来的消息。我们将这称为“连接”。稍后我们会讨论连接问题。
public SipManager(MessageListener messageListener) throws IOException {
this.messageListener = messageListener;
reconnect();
}
SipManager包含许多字段。由于时间原因这里并不介绍这些琐碎代码。在这些字段中,某些字段是可在MEssenger
configuration页面中进行修改的参数:
|
|
类 |
说明 |
Register |
布尔值,如果SIP客户机将自己注册为注册器,则为真。 |
Username |
字符串,用于客户端SIP地址的标识符。例如:此标识符与sip:username@10.0.0.3:5060中的username部分相对应。 |
Port |
整型,SIP客户端使用的端口。通常为5060,但如果在同一机器上运行多个SIP客户机和一个服务器,则需要使用不同地址。 |
Registrar |
字符串,注册器地址,包括端口。例如:如果SIP地址是sip:username@10.0.0.3:5060,则为10.0.0.3:5060。 |
Expires |
整型,注册持续的秒数。 |
当然,所有这类参数均有获取者和设置者。
SipManager还包含其他私有成员,如SipConnectionNotifier对象,它可接收消息、要使用的地址、以及反复发送的请求的标识符。稍后我们会讨论此问题。
发送一个请求
使用SIP可执行的最简单的操作是发送单个消息。图2说明了这一操作:
图2.发送一个请求
如图所示,发送消息的过程分为两部分。第一步是准备和发送消息。第二步是处理响应。我们来看一下执行此操作的代码。首先使用SipManager.sendMessage()方法执行第一步:
public void sendMessage(final String destination, final String message) {
Thread t = new Thread() {
public void run() {
SipClientConnection connection = null;
OutputStream output = null;
try {
connection = (SipClientConnection) Connector
.open(destination);
connection.setListener(SipManager.this);
connection.initRequest("MESSAGE", null);
connection.setHeader("From", registeredAddress);
connection.setHeader("To", destination);
connection.setHeader("Content-Type", "text/plain");
connection.setHeader("Content-Length", String
.valueOf(message.length()));
output = connection.openContentOutputStream();
output.write(message.getBytes());
output.close();
output = null;
} catch (Throwable e) {
messageListener.notifyMessage("Error sending to "
+ destination + ": " + e.getMessage());
e.printStackTrace();
try {
if (output != null) {
output.close();
}
if (connection != null) {
connection.close();
}
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
};
t.start();
}
您将注意到该方法开始了一个新线程。在此示例应用程序中的其他也会出现这种情况。为什么会这样呢?因为通讯需要耗费时间,而此时用户不希望GUI反应迟钝。此外,GUI线程在等待通信结束时会发生死锁,且通讯会触发GUI变更。
此示例代码相对简单。我将打开一个客户机连接,使用它接收响应,初始化请求类型并设置大量强制的标头。请求所需的大部分SIP标头会自动填充默认值。然后打开输出流并写入信息,最后关闭流。此时并没有关闭连接;还需等待响应到达。
值得注意的是:内容是可选的。请求可以为空。在此情况下,发送消息的方法是SipClientConnection.send(),而不只是关闭流。其他方法可用于自定义请求。包括:
- initCancel():创建CANCEL请求。代替initRequest()。
- initAck():创建ACK请求。代替initRequest()。
- setRequestUri():变更默认请求URI值。
- addHeader():用于插入重复的标头,例如:联系人。
- setCredentials():用于添加验证标头。
对于待处理的响应,SipManager必须实现SipClientConnectionListener。这包含一个方法,即notifyResponse()。响应到达后系统会自动调用此方法。实现将首先检查与响应相关的请求,然后显示消息:
- OK(在消息成功发送的情况下)。
- Error(在发送消息时出错的情况下)。
最后,关闭连接。
public void notifyResponse(SipClientConnection connection) {
try {
connection.receive(0);
String method = connection.getMethod();
if (method.equals("MESSAGE")) {
int status = connection.getStatusCode();
if (status == 200) {
messageListener.notifyMessage("Sent OK");
} else {
messageListener.notifyMessage("Error sending: " + status
+ " " + connection.getReasonPhrase());
}
return;
}
/* Registration code goes here. */
} catch (Throwable e) {
messageListener.notifyMessage("Error sending: " + e.getMessage());
} finally {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
此操作结束。它真的很简单。让我们接着查看下一个操作。
接收请求
处理传入的请求有两种方法。第一种方法是同步法,即阻截当前线程以等待要到达的请求。我认为这不是最好的方法,但如果用户知道接收请求的时间或大概的时间范围,则此方法就很有效。由于此方法使用有限,因此这里不准备介绍此技术。
第二种方法是打开“永久”服务器连接,在消息异步到达时收到通知。这是首选技术,并且我打算在此使用它。
图3显示了应用程序处理请求的方式:
图3.接收请求
与发送请求一样,接收请求也分为两步。第一步是在服务器连接中注册监听程序来监听到来的消息。第二步是收到请求到来的通知并发送响应。此代码片段将完成第一步:
public void reconnect() {
Thread t = new Thread() {
public void run() {
doClose();
try {
sipConnection = (SipConnectionNotifier) Connector
.open("sip:" + port);
} catch (Throwable e) {
e.printStackTrace();
}
try {
sipConnection.setListener(SipManager.this);
contactAddress = "sip:" + username + "@"
+ sipConnection.getLocalAddress() + ":"
+ sipConnection.getLocalPort();
} catch (Throwable e) {
e.printStackTrace();
}
/* Registration code goes here. */
};
t.start();
}
注意Connector.open()的参数使用语法的方式:
sip:port
不是应该使用sip:username@registraraddress:port吗?使用实际的SIP地址将创建客户机连接。在端口号后使用sip:或sips:将创建服务器连接。(创建服务器连接还有其他方法,但这些方法与了解MEssenger的工作方式无关。有关详细信息,请参阅SipConnection的Javadoc页面。)
接口SipConnectionNotifier很有趣。在此使用它来注册到来请求的监听程序。还可用它来检索设备地址。但是它并非有传言中的那样好,目前就我所知还没有实现的方法。(我也无法解释非SIP
API不能实现的原因。)通过其acceptAndOpen()方法,还可将其用于阻塞和等待到来的请求。
此服务器连接打开之后,其会自动通知SipManager有请求消息到来。然后读取消息,并使用SipServerConnection对象发送相应的响应。方式如下:
public void notifyRequest(SipConnectionNotifier notifier) {
SipServerConnection connection = null;
InputStream input = null;
try {
connection = notifier.acceptAndOpen(); //Shouldn't block
String size = connection.getHeader("Content-Length");
int length = Integer.parseInt(size);
if (length == 0) {
connection.initResponse(200);
connection.send();
return; //nothing else to do...
}
byte buffer[] = new byte[length];
int readSize;
input = connection.openContentInputStream();
readSize = input.read(buffer);
String from = connection.getHeader("From");
SipAddress sipAddress = new SipAddress(from);
from = sipAddress.getDisplayName();
if (from != null)
from = from.trim();
if ((from == null) || (from.equals("")))
from = sipAddress.getURI();
String message = "From " + from + ": ";
message += new String(buffer, 0, readSize);
messageListener.notifyMessage(message);
//All done, reply OK.
connection.initResponse(200);
connection.send();
} catch (Throwable e) {
e.printStackTrace();
} finally {
try {
if (input != null)
input.close();
if (connection != null)
connection.close();
} catch (Throwable e) {
e.printStackTrace();
}
}
}
参数SipConnectionNotifier是SipServerConnection对象的工厂。注意如何使用SipServerConnection接收请求和返回响应。方法SipServerConnection类似于SipClientConnection,包括获取和设置标头和内容的方法,当然被initResponse(int
statusCode)替换的init...()方法除外。此外,还可使用setReasonPhrase(String
reason)替换响应中状态代码旁的默认文本。
注意:关闭SipServerConnection不代表关闭了创建它的SipConnectionNotifier。这只表示当前操作结束。
现在我们来看一下最后一种操作。
发送重复请求
REGISTER和SUBSCRIBE之类的请求是在特定间隔时间反复发送的请求。使用用于Java
ME的SIP API中的刷新机制后,此作业可轻松完成。
重复请求包含的步骤如图4和图5所示。图4看起来类似于发送单个请求操作,但有一点不同。大家是否能发现不同之处?
图4.首次注册
不同之处在于调用方法SipClientConnection.enableRefresh()。此方法用于自动刷新请求和为刷新事件指定侦听程序。返回的标识符稍后可用于停止刷新任务。我将稍加讨论。首个REGISTER消息的响应会被发送到notifyResponse()方法。
图5.后续注册
SipRefreshHelper在请求到期前会使用某种计时器计划请求更新。后续请求的响应被发送到之前提供的RefreshListener。
我们来看一下与图5对应的代码。之前我对代码进行了几行注释,如下所示:
/* Registration code goes here. */
此注释标记了必须插入注册代码片段的位置。第一个片段从reconnect()方法内发送第一个REGISTER消息。我将其标为粗体,如下所示:
public void reconnect() {
Thread t = new Thread() {
public void run() {
// First half hidden for brevity
registeredAddress = "sip:" + username + "@" + registrar;
if (!register)
return;
try {
SipClientConnection registerConnection=createRegisterConnection();
registerConnection.setListener(SipManager.this);
refreshIdentifier = registerConnection
.enableRefresh(SipManager.this);
registerConnection.send();
registerConnection.close();
} catch (Throwable e) {
e.printStackTrace();
}
}
};
t.start();
}
代码本身一目了然。注意enableReferesh()方法的使用。作为方法notifyResponse()的一部分,下一段代码将作为第一个REGISTER消息的响应被调用:
public void notifyResponse(SipClientConnection connection) {
try {
// First half hidden for brevity
if (method.equals("REGISTER")) {
int status = connection.getStatusCode();
if (status == 200) {
messageListener.notifyMessage("Registration OK");
} else {
messageListener.notifyMessage("Error registering: "
+ status + " " + connection.getReasonPhrase());
}
return;
}
} catch (Throwable e) {
messageListener.notifyMessage("Error sending: " + e.getMessage());
} finally {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
注册代码的最后一个代码段实现RefreshListener接口。它由一个refreshEvent()方法组成:
public void refreshEvent(int refreshID, int statusCode, String reasonPhrase) {
if (statusCode == 200) { //OK
messageListener.notifyMessage("Re-registered OK.");
}
else { //ERROR!
messageListener.notifyMessage("Error registering: " + statusCode);
SipRefreshHelper.getInstance().stop(refreshIdentifier);
}
}
此代码只显示了有关注册状态的消息,并且在出错情况下,将停止刷新计时器。
清理代码
这个示例基本完成。惟一缺少的是执行清理操作的代码,它将关闭连接并停止刷新计时器。在应用程序关闭时可调用此代码。
public void close() {
Thread t = new Thread() {
public void run() {
doClose();
}
};
t.start();
}
protected void doClose() {
if (contactAddress == null)
return; //No need to unregister and close connection; there wasn't a connection.
try {
if (register)
SipRefreshHelper.getInstance().stop(refreshIdentifier);
} catch (Throwable e) {
e.printStackTrace();
}
try {
if (sipConnection != null) {
sipConnection.close();
sipConnection = null;
}
} catch (Throwable e) {
e.printStackTrace();
}
}
先停止刷新任务。这将发送未注册消息(Expires标头为0的REGISTER消息)。然后关闭服务器连接。在新线程中执行所有操作,以便不会中断GUI线程。
结束语
小但实用的Messenger现在已经完成。要查看其实际操作,可参见下面的图6:
图6. MEssenger实际操作
或者,还可直接在移动电话上运行此应用程序!
使用注册器
如果要使用注册,则需要使用注册器。BEA WebLogic SIP Server附带了注册器和代理。本节将介绍如何配置和使用它们。
在编写本文时,此代理还不能处理SIP MESSAGE消息。我必须配置此代理,以使其可以处理此类消息。只需编辑文件C:\bea\sipserver30\samples\sipserver\examples\src\registrar\WEB-INF\sip.xml(此文件夹是默认安装文件夹;可以使用选择的任何安装文件夹)并添加以下标签,即可完成配置:
<servlet-mapping>
<servlet-name>proxy</servlet-name>
<pattern>
<equal>
<var>request.method</var>
<value>MESSAGE</value>
</equal>
</pattern>
</servlet-mapping>
完成此操作后,使用以下步骤构建并部署注册器应用程序:
- 创建环境变量WL_HOME,指向SIP服务器文件夹。例如,此操作可通过在命令窗口中键入以下内容来完成:
set WL_HOME=c:\bea\sipserver30
(此文件夹是默认安装文件夹,可以使用安装SIP服务器时使用的文件夹。)
- 向类路径添加weblogic.jar。例如,此操作可通过在命令窗口中键入以下内容来完成:
set classpath=%CLASSPATH%;%WL_HOME%\server\lib\weblogic.jar
- 现在可以开始构建了。转到注册器源文件夹:
cd %WL_HOME%\samples\sipserver\examples\src\registrar
- 接着使用Ant进行构建:
ant build
- 创建WebLogic SIP Server域,以便在其中运行应用程序。
- 最后,将此应用程序部署到运行的服务器上:
ant deploy。
现在即可将MEssenger注册到SIP服务器了。
MEssenger还可与前面文章提到的ChatRoomServer
servlet兼容。
下载
-
MEssenger.zip (7 KB):访问本文介绍的应用程序的全部源代码。
总结
本文演示了使用Java ME进行SIP编程是多么简单。借助简单的MEssenger应用程序,还演示了许多有用的通信模式实现。简单的库与灵活的SIP相结合可开发出不计其数的应用程序。
现在,我朋友和我的移动电话都是启用IM的电话,我们经常使用它们聊天。让我们尽情享受移动电话带来的乐趣吧!
参考资料
-
JSR 180
-
SIP简介,第1部分:SIP初探(中文版)
-
SIP简介,第2部分:SIP SERVLET(中文版)
- 有关注册器示例文档,请参见默认路径C:\bea\sipserver30\samples\sipserver\examples\src\registrar\readme.html
附录
本注释介绍了安装和配置Java Wireless Toolkit和EclipseME的提示。旨在帮助大家在Java
ME平台下开发SIP应用程序做准备。
Java Wireless Toolkit的安装提示
Java Wireless Toolkit (WTK)是一套可用于开发用于移动电话和类似设备的应用程序的工具。它包含大量库(包括用于Java
ME的SIP API的实现)和一个仿真器,所有工具包装在一个易于安装的工具包中。这样便于在SIP开发环境下作出轻松选择。
有关最新Java Wireless Toolkit的下载,请参见Java ME下载页面:http://java.sun.com/javame/downloads
可导航到下载页面。在下载安装程序前,必须注册(免费)。在此还必须使用下载中心的用户名和密码登录。
Windows安装程序名类似于j2me_wireless_toolkit-x_y-windows.exe(其中,x和y分别表示主次版本号)。将文件下载到硬盘上。然后运行执行文件并按照屏幕上显示的步骤进行操作。Quick
Time Player选项是可选的,它可帮助用户在仿真器中播放媒体文件。如果未显示Quick
Time Player而用户又希望安装它,请转到此地址。
http://www.apple.com/quicktime/download/standalone.html
EclipseME的安装提示
Eclipse ME是用于Eclipse的插件,它有助于轻松开发Java ME应用程序。它与Java
Wireless Toolkit完全集成。设置Eclipse ME分两步进行。第一步是安装EclipseME。第二步是对其进行配置。该部分将介绍第一步。
获取EclipseME最简便的方法是使用Eclipse Software Updates。先启动Eclipse,然后转到菜单Help
> Software Updates > Find and Install。选择“Search
for new features to install”,然后单击Next。在下一页上单击按钮New
Remote Site。输入以下信息:
- Name: EclipseME
- URL: http://www.eclipseme.org/updates
单击Finish。现在Eclipse即可在更新站点查找EclipseME。在下一个对话框中选择EclipseME并继续操作直到完成安装。
单击Install继续操作。安装完EclipseME后,必须重新启动Eclipse。
EclipseME的配置提示
前面我说过EclipseME是与Java Wireless Toolkit集成的。虽然如此,但要让它们协调运作还必须执行一些配置操作。以下是需要执行的步骤:
- J2ME首选项:在Eclipse中,转到Preferences对话框(菜单Window
> Preferences)。导航到J2ME类别。必须在WTK Root字段中输入Java
Wireless Toolkit的位置。
- 导航到Device Management类别。设备列表为空。EclipseME可搜索所需设备。单击Import。再次进入WTK文件夹,并选择所有设备。单击Finish。各设备即被导入列表。将要使用的一个设备选作默认值。
- 调试设置:需要对某些设置进行调试,以使Java Wireless Toolkit能够在调试器中工作。必须在Java
> Debug 类别中设置以下选项:
- Suspend execution on uncaught exceptions:不选择。
- Suspend execution on compilation errors:不选择。
- Debugger timeout (ms): 15000(15秒)。
如果没有这些选项,调试则无法执行。
EclipseME配置提示
安装现在结束。我们要测试一下程序是否能正常运行。
- 创建J2ME项目:在Eclipse中,转到菜单File > New >
Project。选择类别J2ME > J2ME Midlet Suite。单击Next。输入项目名称。单击Next。选择部署文件(JAD文件)的名称。单击Finish。
- 创建包。
- 创建MIDlet:转到菜单File > New > Other。导航到J2ME
> J2ME Midlet。单击Next。输入类的名称。单击Finish。
- 完成操作后,查看生成的MIDlet代码。
了解Java ME编程
有许多在线指南可供参考。请参见以下内容: