JAIN SIP API 簡介

摘要

本文介紹如何在 Java SE 上使用會話發起協議 (SIP) 開發客戶端應用程序。文中展現 JAIN SIP API 這種強大的「SIP 堆棧」。首先介紹一個簡單的 IM 應用程序,而後對其進行剖析來了解該技術。html

關於 JAIN SIP API

集成網絡 Java API (JAIN) 是一個管理電信標準的 JCP 工做組。會話發起協議 (SIP) 是一種標準通訊協議,以前的文章中已對其進行了討論。將 Java 與 SIP 結合起來就獲得了 JAIN SIP API,這是一種強大的標準電信 API。這一想法始於 1999 年的 JSR 32。該參考實現是開源的、很是穩定且普遍應用。若是您使用 Java 編寫 SIP 應用程序,使用此 SIP 堆棧則很是合適。java

此 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 參考實現oracle

  • log4j-1.2.8.jar(包含在 jain-sip/lib 文件夾下的 jain-sip-1.2.jar 文件中):日誌記錄服務app

  • concurrent.jar(包含在 jain-sip/lib 文件夾下的 jain-sip-1.2.jar 文件中):併發性實用程序異步

本示例不須要其餘文件。請在您的項目中包括上面這些庫。tcp

有關 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 實現必須支持的屬性,還有一些是非標準屬性,這些屬性依賴於具體的實現。有關這些屬性的連接,請參見「參考資料」部分。

下一步是建立一對 ListeningPointSipProvider 對象。這兩個對象提供發送和接收消息的通訊功能。一組對象用於 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);
        ...

我使用在構造函數中建立的工廠(HeaderFactoryAddressFactory)來實例化這些元素。

下面咱們經過傳遞先前建立的全部元素來實例化實際的 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 不採用這種方法。不過我認爲有必要對這種方法有所瞭解。所以做爲折衷方案,本子部分介紹如何實現此方法。

在對話內發送消息須要建立 DialogTransaction 對象。在初始消息(即建立對話的消息)中,您將實例化一個 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() 方法。在對話和事務結束時分別自動調用這兩個方法。一般您可使用這些方法來進行清理(例如丟棄 DialogTransaction 實例)。因爲在 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 您須要的第一個接口,用於建立 ListeningPointSipProvider
ListeningPoint 此接口封裝一個傳輸/端口對(例如,UDP/5060)。
SipProvider 此接口用於發送 SIP 消息。還可使用此接口爲傳入 SIP 消息註冊監聽器。參見下面的 SipListener
SipListener 必須實現此接口才能接收傳入 SIP 消息。
RequestEvent / ResponseEvent 表示傳入 SIP 請求、響應。將傳遞到 SipListener 進行處理。分別包含 RequestResponse 對象。
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()                            
易讀的狀態碼說明的訪問器。
相關文章
相關標籤/搜索