摘要

本文介绍如何在 Java SE 上使用会话发起协议 (SIP) 开发客户端应用程序。文中展示 JAIN SIP API 这种强大的“SIP 堆栈”。首先介绍一个简单的 IM 应用程序,然后对其进行剖析来了解该技术。

关于 JAIN SIP API

集成网络 Java API (JAIN) 是一个管理电信标准的 JCP 工作组。会话发起协议 (SIP) 是一种标准通信协议,之前的文章中已对其进行了讨论。将 Java 与 SIP 结合起来就得到了 JAIN SIP API,这是一种强大的标准电信 API。这一想法始于 1999 年的 JSR 32。该参考实现是开源的、非常稳定且广泛应用。如果您使用 Java 编写 SIP 应用程序,使用此 SIP 堆栈则非常合适。

此 API 通常用于客户端应用程序开发。其他基于容器的技术,如 SIP Servlet API(例如,参见 BEA WebLogic SIP Server),更适合于服务器端开发。之前的一篇文章重点介绍了 SIP Servlet API。现在我们来看看客户端 API 堆栈。

先决条件

本文需要对 Java 有充分了解。同时,建议您熟悉一下 SIP,因为使用 JAIN SIP API 要求对此协议有充分了解。SIP 信令(尤其是消息和头)特别重要。有关相关信息的链接,请参见本文结尾处的“参考资料”部分。

要获得 JAIN SIP API 库,请访问 jain-sip 项目主页。单击此链接可转到下载页。您将需要获取以下文件:

  • JainSipApi1.2.jar:SIP 接口和主类
  • JainSipRi1.2.jar:SIP 参考实现
  • log4j-1.2.8.jar(包含在 jain-sip/lib 文件夹下的 jain-sip-1.2.jar 文件中):日志记录服务
  • concurrent.jar(包含在 jain-sip/lib 文件夹下的 jain-sip-1.2.jar 文件中):并发性实用程序

本示例不需要其他文件。请在您的项目中包括上面这些库。

有关 JAIN SIP API 中提供的各种类和接口的列表,建议您查看附录的第一部分。这些类和接口是标准的,免费提供,很快您就可以看到如何将其用作 SipLayer 示例类的一部分。

TextClient 示例应用程序

作为 JAIN SIP API 程序的一个示例,我们来剖析一个应用程序,如果您已阅读我先前所写的一篇有关 SIP Servlet 主题的文章,可能已经熟悉该应用程序。TextClient 是一个即时消息传递应用程序,可通过 SIP 协议发送和接收文本消息。该应用程序的一个实例可以将消息发送到另一个实例,但理论上可以使用此客户端将消息发送到其他类型的 SIP 即时消息传递客户端,甚至发送到 SIP 服务器应用程序。图 1 显示了一个屏幕截图。

TextClient 返回值 
图 1. TextClient 返回值

要运行该应用程序,首先需要下载所提供的源代码。其次,必须使用提供的 Ant 脚本构建该应用程序。这将产生一个 JAR 文件。最后,使用以下命令运行该应用程序:

                         
java -jar textclient.jar <username> <port>
                      

您可以运行此客户端的多个实例(确保它们使用不同的端口)并在彼此之间发送消息。本文的其余部分将研究此示例应用程序的代码。

TextClient 代码概述

整个 TextClient 代码由两个类和一个接口组成。下表介绍了这些类和这个接口:

类/接口说明
TextClient主类,包含应用程序小部件的 Swing 窗口。参见图 1。
SipLayer处理所有 SIP 通信的类。它由 TextClient 类实例化,并通过 MessageProcessor 接口回调。
MessageProcessor回调接口(观察器模式),用于将 SipLayer 与其容器相分离。

在下面的部分中,我将介绍 MessageProcessor,然后大部分时间用来介绍 SipLayer。我几乎不会谈到 TextClient 类,因为它只包含用户接口 Swing 代码,并且与本文主题无关。有关详细信息,请参见本文附带的源代码。

消息处理器

在开始介绍 SipLayer 类之前,我要先简单介绍一下 MessageProcessor 接口。为使 SIP 层与 GUI 层分离,您需要使用回调接口,该接口允许从前者发送信息,而无需知道后者的签名。该接口如下所示:

                         

public interface MessageProcessor

{

    public void processMessage(String sender, String message);

    public void processError(String errorMessage);

    public void processInfo(String infoMessage);

}
                      

SipLayer 构造函数将接受此接口的一个实现(即 TextClient 对象)作为参数并保存在该对象上。稍后您就可以使用此对象将信息发送回 GUI。

SIP 堆栈准备

我们来开始编写 SipLayer 类。TextClient 必须能够接收从其他 SIP 端点传来的异步消息。为此使用了观察器模式:SipLayer 类实现 SipListener 接口以处理传入消息:

                         

public class SipLayer

        implements SipListener {


                      

此接口具有以下方法:

                         
   void processRequest(RequestEvent evt);

    void processResponse(ResponseEvent evt);

    void processTimeout(TimeoutEvent evt);

    void processIOException(IOExceptionEvent evt);

    void processTransactionTerminated(TransactionTerminatedEvent evt);

    void processDialogTerminated(DialogTerminatedEvent evt);


                      

在本示例中,最重要的方法显然是用于处理传入消息的 processRequest() 和 processResponse()。稍后我再来看这两个方法。

接下来是两个用来存储稍后所需对象的字段。这两个字段与 SIP API 没有直接关系,但本例中需要它们。第一个是前面所讨论过的 MessageProcessor 对象。您还需要准备好用户名。这两个字段均有 getter 和 setter,为简单起见,本文不介绍它们。

                         

private MessageProcessor messageProcessor;

private String username;


                      

接下来是构造函数。启动 JAIN SIP API 应用程序的典型方式(TextClient 也遵循这种模式)是创建一系列稍后将用到的对象。我将谈谈一些工厂,以及一个已初始化的 SIP 堆栈。

                         
private SipStack sipStack;

private SipFactory sipFactory;

private AddressFactory addressFactory;

private HeaderFactory headerFactory;

private MessageFactory messageFactory;

private SipProvider sipProvider;



public SipLayer(String username, String ip, int port) throws

        PeerUnavailableException, TransportNotSupportedException,

        InvalidArgumentException, ObjectInUseException,

        TooManyListenersException {

  setUsername(username);

  sipFactory = SipFactory.getInstance();

  sipFactory.setPathName("gov.nist");

  Properties properties = new Properties();

  properties.setProperty("javax.sip.STACK_NAME",

          "TextClient");

  properties.setProperty("javax.sip.IP_ADDRESS",

          ip);





  sipStack = sipFactory.createSipStack(properties);

  headerFactory = sipFactory.createHeaderFactory();

  addressFactory = sipFactory.createAddressFactory();

  messageFactory = sipFactory.createMessageFactory();

  ...


                      

SIP 工厂用于实例化 SipStack 实现,但由于可能存在多个实现,您必须通过 setPathName() 方法指定所需的一个实现。名称“gov.nist”表示所获取的 SIP 堆栈。

SipStack 对象接受一些属性。至少必须设置堆栈名称。其他属性是可选的。下面我将设置堆栈所使用的 IP 地址,针对一台计算机具有多个 IP 地址的情况。注意,有些是标准属性,即所有 SIP API 实现必须支持的属性,还有一些是非标准属性,这些属性依赖于具体的实现。有关这些属性的链接,请参见“参考资料”部分。

下一步是创建一对 ListeningPoint 和 SipProvider 对象。这两个对象提供发送和接收消息的通信功能。一组对象用于 TCP,一组对象用于 UDP。在此还要选择 SipLayer (this) 作为传入 SIP 消息的监听器:

                         

...

  ListeningPoint tcp = sipStack.createListeningPoint(port, "tcp");

  ListeningPoint udp = sipStack.createListeningPoint(port, "udp");



  sipProvider = sipStack.createSipProvider(tcp);

  sipProvider.addSipListener(this);

  sipProvider = sipStack.createSipProvider(udp);

  sipProvider.addSipListener(this);

}
                      

至此,构造函数完成。您已经使用 JAIN SIP API 创建了一个 SipStack 实例、一组工厂、两个 ListeningPoint,还有一个 SipProvider。后面的有些方法将需要使用这些对象来发送和接收消息。

 

发送 SIP 请求

现有我们来写一个方法,它使用 JAIN SIP API 发送一条 SIP 消息。

在先决条件中,我曾建议您必须对 SIP 具备相当了解才可以开始使用 SIP API。现在您就会明白我那么说的原因了!SIP API 是相当底层的抽象,大多数情况下,它不使用默认值或隐藏头、请求 URI、或 SIP 消息的内容。这种设计的优点在于您可以完全控制 SIP 消息所包含的内容。

以下方法有点冗长。它准备并发送一个 SIP 请求。该方法大致可以分为四个子部分:

  • 创建主要元素
  • 创建消息
  • 完成消息
  • 发送消息

使用 JAIN SIP API 构造消息至少需要以下主要 SIP 元素:

  • 请求 URI
  • 方法
  • Call-ID 头
  • CSeq 头
  • From 头
  • Via 头数组
  • Max-forwards 头

有关这些元素的信息,请参见“SIP 简介,第 1 部分”(Dev2Dev,2006 年)。以下代码段创建所有这些元素:


                         
public void sendMessage(String to, String message) throws
            ParseException, InvalidArgumentException, SipException {

        SipURI from = addressFactory.createSipURI(getUsername(),
                getHost() + ":" + getPort());
    Address fromNameAddress = addressFactory.createAddress(from);
        fromNameAddress.setDisplayName(getUsername());
        FromHeader fromHeader =
                headerFactory.createFromHeader(fromNameAddress,
                        "textclientv1.0");

        String username = to.substring(to.indexOf(":")+1, to.indexOf("@"));
        String address = to.substring(to.indexOf("@")+1);

        SipURI toAddress =
                addressFactory.createSipURI(username, address);
        Address toNameAddress = addressFactory.createAddress(toAddress);
        toNameAddress.setDisplayName(username);
        ToHeader toHeader =
                headerFactory.createToHeader(toNameAddress, null);

        SipURI requestURI =
                addressFactory.createSipURI(username, address);
        requestURI.setTransportParam("udp");

        ArrayList viaHeaders = new ArrayList();
        ViaHeader viaHeader =
                headerFactory.createViaHeader(
                        getHost(),
                        getPort(),
                        "udp",
                        null);
        viaHeaders.add(viaHeader);

        CallIdHeader callIdHeader = sipProvider.getNewCallId();

        CSeqHeader cSeqHeader =
                headerFactory.createCSeqHeader(1, Request.MESSAGE);

        MaxForwardsHeader maxForwards =
                headerFactory.createMaxForwardsHeader(70);
        ...
                      

我使用在构造函数中创建的工厂(HeaderFactory 和 AddressFactory)来实例化这些元素。

下面我们通过传递先前创建的所有元素来实例化实际的 SIP 消息本身。

      Request request =  messageFactory.createRequest(
        requestURI, Request.MESSAGE, callIdHeader, cSeqHeader,
        fromHeader, toHeader, viaHeaders,       maxForwards);
...

                      

注意在此步骤中使用了 MessageFactory

然后向消息中添加其他元素:Contact 头和消息的内容(负载)。此时也可以添加自定义头。

    SipURI contactURI = addressFactory.createSipURI(getUsername(),
                getHost());
        contactURI.setPort(getPort());
        Address contactAddress = addressFactory.createAddress(contactURI);
        contactAddress.setDisplayName(getUsername());
        ContactHeader contactHeader =
                headerFactory.createContactHeader(contactAddress);
        request.addHeader(contactHeader);

        ContentTypeHeader contentTypeHeader =
                headerFactory.createContentTypeHeader("text", "plain");
        request.setContent(message, contentTypeHeader);
        ...
                      

有关如何进一步处理请求的信息,可参见附录中对 Request 接口的描述。

    sipProvider.sendRequest(request);
}
                      

在对话内发送消息

现在您是在对话外部发送消息。这意味着消息彼此不相关。这非常适用于像 TextClient 这样的简单即时消息传递应用程序。

也可以使用 INVITE 消息创建一个对话(有时称为会话),然后在此对话中发送消息。TextClient 不采用这种方法。不过我认为有必要对这种方法有所了解。因此作为折衷方案,本子部分介绍如何实现此方法。

在对话内发送消息需要创建 Dialog 和 Transaction 对象。在初始消息(即创建对话的消息)中,您将实例化一个 Transaction,然后从其获取 Dialog,而不是使用提供程序发送消息。保留 Dialog 引用以便稍后使用。然后使用 Transaction 发送消息:

ClientTransaction trans = sipProvider.getNewClientTransaction(invite);
dialog = trans.getDialog();
trans.sendRequest();

                      

稍后,当您希望在同一对话内发送一条新消息时,可使用前面的 Dialog 对象创建一个新请求。然后您可以对请求进行处理,最后使用 Transaction 发送消息。

request = dialog.createRequest(Request.MESSAGE);

request.setHeader(contactHeader);
request.setContent(message, contentTypeHeader);

ClientTransaction trans = sipProvider.getNewClientTransaction(request);
trans.sendRequest();

                      

从本质上说,当您在现有对话内发送消息时,将跳过“创建主要元素”这一步骤。当您使用 INVITE 创建对话时,不要忘记在结束时发送对话内 BYE 消息来清除该对话。此方法还可用于刷新注册和订阅。

前面您已经见到 SipListener 接口,它包含 processDialogTerminated() 和 processTransactionTerminated() 方法。在对话和事务结束时分别自动调用这两个方法。通常您可以使用这些方法来进行清理(例如丢弃 Dialog 和 Transaction 实例)。由于在 TextClient 中不需要这两个方法,因此将其保留为空。

 

接收响应

之前您注册了一个监听传入消息的监听器。监听器接口 SipListener 包含 processResponse() 方法,在 SIP 响应消息到达时由 SIP 堆栈调用该方法。processResponse() 接受类型为 ResponseEvent 的单个参数,该参数封装了一个 Response 对象。现在我们来实现这一方法。

                         

public void processResponse(ResponseEvent evt) {
        Response response = evt.getResponse();
        int status = response.getStatusCode();

        if( (status >= 200) && (status < 300) ) { //Success!
                messageProcessor.processInfo("--Sent");
                return;
        }

        messageProcessor.processError("Previous message not sent: " +
                        status);
}
                      

在该方法中,您将检查先前的 MESSAGE 消息的响应是表示成功(2xx 范围的状态码)还是错误(在该范围之外)。然后通过回调接口将此信息重新转发给用户。

通常在 processResponse() 方法中只读取 Response 对象。唯一的例外是对 INVITE 消息的成功响应;在此情况下,您必须直接回发 ACK 请求,如下所示:

                         

Dialog dialog = evt.getClientTransaction().getDialog()
Request ack =  dialog.createAck()
dialog.sendAck( ack );
                      

有关 Response 接口的描述,请参见附录

接收请求

接收 SIP 请求消息与接收响应一样轻松。只需实现 SipListener 接口的另一个方法 processRequest(),SIP 堆栈就会自动调用该方法。此方法只有一个参数,即 RequestEvent 对象,该对象包含(您猜对了)一个 Request 对象。这与您先前看到的类型相同,并具有相同的方法。但是,不要在传入请求上设置任何字段,因为这样没有意义。

processRequest() 的典型实现将分析请求,然后创建相应的响应并将其发送回去。下面演示现在应如何操作:

                         

public void processRequest(RequestEvent evt) {
        Request req = evt.getRequest();

        String method = req.getMethod();
        if( ! method.equals("MESSAGE")) { //bad request type.
                messageProcessor.processError("Bad request type: " + method);
                return;
        }

        FromHeader from = (FromHeader)req.getHeader("From");
        messageProcessor.processMessage(
                        from.getAddress().toString(),
                        new String(req.getRawContent()));
        Response response=null;
        try { //Reply with OK
                response = messageFactory.createResponse(200, req);
                ToHeader toHeader = (ToHeader)response.getHeader(ToHeader.NAME);
                toHeader.setTag("888"); //Identifier, specific to your application
                ServerTransaction st = sipProvider.getNewServerTransaction(req);
                st.sendResponse(response);
        } catch (Throwable e) {
                e.printStackTrace();
                messageProcessor.processError("Can't send OK reply.");
        }
}
                      

在此情况下,您始终使用成功响应 (200) 进行回复,但还可以传回任何错误响应(通常为 4xx 范围)。本文给出了 SIP 状态码的有用列表。

处理错误情况

SipListener 接口中还有一些您尚未实现的其他方法。由于某些原因无法发送请求时 SIP 堆栈将调用这些方法。例如,当接受消息的端点未能即时应答时,将调用 processTimeout()。这是一个没有响应的特殊情况,因此 Response 对象不可用。除其他内容外,TimeoutEvent 参数还包含了超时请求的 ClientTransaction,您可以根据需要使用它重新链接到原始请求。在本实现中,您只是使用回调接口通知用户:

                         

public void processTimeout(TimeoutEvent evt) {
        messageProcessor.processError("Previous message not sent: " +
                        "timeout");
}
                      

类似地,使用以下方法处理输入/输出 (IO) 错误:

                         
public void processIOException(IOExceptionEvent evt) {
        messageProcessor.processError("Previous message not sent: " +
                        "I/O Exception");
}
                      

点对点与客户端/服务器

SIP 客户端应用程序可以单独使用(点对点),也可以与服务器一起使用以提供代理或呼叫路由等额外功能。

建议您阅读我关于 SIP Servlet 的文章。这篇文章中包含一个简洁的 SIP 服务器应用程序,可与 TextClient 结合使用来提供聊天室类型的服务。该文介绍如何将 TextClient 与 BEA WebLogic SIP Server 结合使用,使其作用倍增。

下载

在此下载 TextClient 源代码

总结

本文概述了 JAIN SIP API,并介绍了如何编写简单的应用程序来使用此技术。现在,您应较好地了解了可用的 API,并了解了如何使用 SIP 编写自己的 IM 客户端。

不过,何必止步于此?我可以向此应用程序添加更多功能。如前所述,如果客户端与服务器应用程序交谈,可令作用倍增。如果需要建议,请考虑以下内容:

  • 自动文本应答器、存储和转发(例如,“John is offline right now, but he will receive your messages as soon as he logs back in”)
  • 简洁的联网检查器视频游戏
  • 针对笔记本电脑的基于位置的服务
  • 媒体共享的客户端
  • 类似 RSS 的客户端

可能性几乎是无限的。

参考资料

附录

本部分是 JAIN SIP API 中可用的各种类和接口的参考。

API 概述

以下概述了 JAIN SIP API 参考实现中的主要类和接口。

类/接口说明
SipFactory / AddressFactory / HeaderFactory / MessageFactory创建系统的各种对象的工厂类。这些类将返回实现标准接口的对象。
SipStack您需要的第一个接口,用于创建 ListeningPoint 和 SipProvider
ListeningPoint此接口封装一个传输/端口对(例如,UDP/5060)。
SipProvider此接口用于发送 SIP 消息。还可以使用此接口为传入 SIP 消息注册监听器。参见下面的 SipListener
SipListener必须实现此接口才能接收传入 SIP 消息。
RequestEvent / ResponseEvent表示传入 SIP 请求、响应。将传递到 SipListener 进行处理。分别包含 Request或 Response 对象。
TimeoutEvent表示传出请求未得到答复的故障情况。将传递到 SipListener 进行处理。
IOExceptionEvent表示一种故障情况,此时发送传出请求出现输入/输出问题。将传递到 SipListener进行处理。
Request / Response表示 SIP 请求、响应。两者都是 Message 接口的子接口。可通过它们访问 SIP 消息的头、内容及其他部分。
Dialog此接口的对象封装了 SIP 对话。(提醒:在一个对话中,所有消息与同一呼叫相关联;对话通常起始于 INVITE,结束于 BYE。)
ClientTransaction / ServerTransaction封装 SIP 事务。(提醒:所有事务起始于请求,结束于最终响应。事务通常生存于对话中。)

Message 接口

Message 接口是 SIP 消息的基接口。下面概述了可用的方法,供您参考:

方法说明
void addHeader(Header) 
void setHeader(Header)
将头字段设置为 SIP 消息。第一个方法可用于可重复或者可以有多个值的头,如 Contact 头。第二个方法删除此类型的现有头,然后添加单个头值。
void removeHeader(Header)删除此类型的现有头。
ListIterator getHeaderNames()返回所有头名称。
ListIterator getUnrecognizedHeaders()返回非标准头类型的头名称。
Header getHeader(String) 
ListIterator getHeaders(String)
特定头的 getter。第二种形式返回可重复头(或具有多个值的头,如 Contact 头)的所有值。
void setContent(Object, ContentTypeHeader)设置消息的负载以及 Content-Type 头。如果类型为字符串,还将设置 Content-Length,否则使用 void setContentLength(ContentLengthHeader)
byte [] getRawContent() 
Object getContent()
检索消息的负载。
void removeContent()清空负载。
void setContentLength(ContentLengthHeader) 
ContentLengthHeader getContentLength() 
void setContentLanguage(ContentLanguageHeader) 
ContentLanguageHeader getContentLanguage() 
void setContentEncoding(ContentEncodingHeader) 
ContentEncodingHeader getContentEncoding() 
void setContentDisposition(ContentDispositionHeader)
ContentDispositionHeader getContentDisposition()
特殊的与负载有关的头访问器。极少使用。
void setExpires(ExpiresHeader) 
ExpiresHeader getExpires()
管理 Expires 头。
void setSipVersion(String) 
String getSipVersion()
SIP 版本元素的访问器。极少使用,默认为 SIP/2.0。
Object clone()创建消息的副本。极少使用。

Request 接口

现在来大致了解 Request 接口(上面 Message 的子接口):

方法说明
String getMethod()
void setMethod(String)
方法元素的访问器。可以为任何 SIP 方法,包括 Request 接口常数中的方法:ACK、BYE、CANCEL、INVITE、OPTIONS、REGISTER、NOTIFY、SUBSCRIBE、MESSAGE、REFER、INFO、PRACK 和 UPDATE。
URI getRequestURI() 
void setRequestURI(URI)
请求 URI 的访问器,即 SIP 请求的第一行。通常,这是 SipURI 的实例。

Response 接口

Response 接口也扩展了 Message 接口:

方法说明
void setStatusCode() 
int getStatusCode()
状态码的访问器。这可以是任何 SIP 状态码,包括 Response 接口的常数成员中的状态码。以下是其中几种:RINGING (180)、OK (200)、BAD_REQUEST (400) 等等。
void setReasonPhrase(String)
String getReasonPhrase()
易读的状态码说明的访问器。

Emmanuel Proulx 是 J2EE 和 SIP 方面的专家。他是经过认证的 WebLogic Server 工程师。

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐