UML软件工程组织

 

 

Java ME的SIP API简介
 
作者:Emmanuel Proulx 出处:dev2dev.bea.com
 

本文将提供一个易于使用的方法来开发使用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后即可发送消息。该应用程序最近的五个事件可显示在下半个屏幕中。配置页面中可输入注册信息。

  Java ME的SIP API简介 图-1

  图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说明了这一操作:

  Java ME的SIP API简介 图-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显示了应用程序处理请求的方式:

  Java ME的SIP API简介 图-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看起来类似于发送单个请求操作,但有一点不同。大家是否能发现不同之处?

  Java ME的SIP API简介 图-4

  图4.首次注册

不同之处在于调用方法SipClientConnection.enableRefresh()。此方法用于自动刷新请求和为刷新事件指定侦听程序。返回的标识符稍后可用于停止刷新任务。我将稍加讨论。首个REGISTER消息的响应会被发送到notifyResponse()方法。

  Java ME的SIP API简介 图-5

  图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:

  Java ME的SIP API简介 图-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>

完成此操作后,使用以下步骤构建并部署注册器应用程序:

  1. 创建环境变量WL_HOME,指向SIP服务器文件夹。例如,此操作可通过在命令窗口中键入以下内容来完成:

    set WL_HOME=c:\bea\sipserver30

    (此文件夹是默认安装文件夹,可以使用安装SIP服务器时使用的文件夹。)

  2. 向类路径添加weblogic.jar。例如,此操作可通过在命令窗口中键入以下内容来完成:

    set classpath=%CLASSPATH%;%WL_HOME%\server\lib\weblogic.jar

  3. 现在可以开始构建了。转到注册器源文件夹:

    cd %WL_HOME%\samples\sipserver\examples\src\registrar

  4. 接着使用Ant进行构建:

    ant build

  5. 创建WebLogic SIP Server域,以便在其中运行应用程序。
  6. 最后,将此应用程序部署到运行的服务器上:

    ant deploy。

现在即可将MEssenger注册到SIP服务器了。

MEssenger还可与前面文章提到的ChatRoomServer servlet兼容。

下载

  1. MEssenger.zip (7 KB):访问本文介绍的应用程序的全部源代码。

总结

本文演示了使用Java ME进行SIP编程是多么简单。借助简单的MEssenger应用程序,还演示了许多有用的通信模式实现。简单的库与灵活的SIP相结合可开发出不计其数的应用程序。

现在,我朋友和我的移动电话都是启用IM的电话,我们经常使用它们聊天。让我们尽情享受移动电话带来的乐趣吧!

参考资料

  1. JSR 180
  2. SIP简介,第1部分:SIP初探(中文版)
  3. SIP简介,第2部分:SIP SERVLET(中文版)
  4. 有关注册器示例文档,请参见默认路径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。输入以下信息:

  1. Name: EclipseME
  2. URL: http://www.eclipseme.org/updates

单击Finish。现在Eclipse即可在更新站点查找EclipseME。在下一个对话框中选择EclipseME并继续操作直到完成安装。

单击Install继续操作。安装完EclipseME后,必须重新启动Eclipse。

EclipseME的配置提示

前面我说过EclipseME是与Java Wireless Toolkit集成的。虽然如此,但要让它们协调运作还必须执行一些配置操作。以下是需要执行的步骤:

  1. J2ME首选项:在Eclipse中,转到Preferences对话框(菜单Window > Preferences)。导航到J2ME类别。必须在WTK Root字段中输入Java Wireless Toolkit的位置。
  2. 导航到Device Management类别。设备列表为空。EclipseME可搜索所需设备。单击Import。再次进入WTK文件夹,并选择所有设备。单击Finish。各设备即被导入列表。将要使用的一个设备选作默认值。
  3. 调试设置:需要对某些设置进行调试,以使Java Wireless Toolkit能够在调试器中工作。必须在Java > Debug 类别中设置以下选项:
  4. Suspend execution on uncaught exceptions:不选择
  5. Suspend execution on compilation errors:不选择
  6. Debugger timeout (ms): 15000(15秒)。

如果没有这些选项,调试则无法执行。

EclipseME配置提示

安装现在结束。我们要测试一下程序是否能正常运行。

  1. 创建J2ME项目:在Eclipse中,转到菜单File > New > Project。选择类别J2ME > J2ME Midlet Suite。单击Next。输入项目名称。单击Next。选择部署文件(JAD文件)的名称。单击Finish。
  2. 创建包。
  3. 创建MIDlet:转到菜单File > New > Other。导航到J2ME > J2ME Midlet。单击Next。输入类的名称。单击Finish。
  4. 完成操作后,查看生成的MIDlet代码。

了解Java ME编程

有许多在线指南可供参考。请参见以下内容:

 

组织简介 | 联系我们 |   Copyright 2002 ®  UML软件工程组织 京ICP备10020922号

京公海网安备110108001071号