Java ME的SIP API簡介

本文將提供一個易於使用的方法來開發使用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編程

  有許多在線指南可供參考。請參見以下內容:

 作者簡介
  Emmanuel Proulx 是一位J2EE和Enterprise JavaBeans方面的專家,也是一位獲得認證的WebLogic Server 7.0工程師
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章