簡介: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。
先決條件
要從本文中獲取最大收益,必須在開發環境中安裝以下工具:
- 最新版的Eclipse
- Java Wireless Toulkit (JWT)
- 配置使用JWT的EclipseME插件
此外,還必須瞭解一點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編程
有許多在線指南可供參考。請參見以下內容:
- http://www.javaworld.com/javaworld/jw-05-2005/jw-0502-midlet.html
- http://today.java.net/pub/a/today/2005/02/09/j2me1.html
- http://today.java.net/pub/a/today/2005/05/03/midletUI.html
- http://developers.sun.com/techtopics/mobility/midp/articles/wtoolkit/
- http://developers.sun.com/techtopics/mobility/midp/articles/tutorial2/
作者簡介 | |
Emmanuel Proulx 是一位J2EE和Enterprise JavaBeans方面的專家,也是一位獲得認證的WebLogic Server 7.0工程師 |