會話發起協議(Session Initiation Protocol,SIP)是一種信號傳輸協議,用於建立、修改和終止兩個端點之間的會話。SIP 可用於建立 兩方呼叫、多方呼叫,或者甚至 Internet 呼叫、多媒體呼叫和多媒體分發的多播會話。JSR 116:SIP Servlet API 是一個服務器端接口,描 述了針對 SIP 組件及服務的容器。SIP servlet 是在 SIP 容器中運行的 servlet,與 HTTP Servlet 類似,但提供了對 SIP 協議的支持。 SIP 和 SIP servlet 是許多基於遠程通信的流行應用程序的底層技術,這些應用程序提供了各種服務,比如 Voice-over-IP (VoIP)、即時通 信、在線和好友列表管理,以及網絡會議。
SIP 和 SIP servlet 對於企業也很重要。與 Java EE 技術結合,SIP servlet 可用於向企業應用程序添加豐富的媒體交互功能。JSR 289: SIP Servlet v1.1 更新了 SIP Servlet API 並定義了一個標准的應用程序編程模型,用於將 SIP servlet 和 Java EE 組件集成到一起。SIP servlet 將在下一代遠程通信服務中扮演更加重要的角色。
本技術文章涵蓋了 SIP 和 SIP servlet 的一些基本底層概念。本文還提供了一個示例應用程序,該應用程序使用 SIP servlet 和 HTTP servlet 提供 VoIP 電話服務。
什麼是 SIP?
介紹 SIP 的一種簡單方法就是從應用場景入手。我們假設用戶 A 想要與用戶 B 建立一個呼叫。在遠程通信設置中,用戶 A 和 B 將通過 用戶代理進行通信。用戶代理的一個例子就是軟件電話——用於在 Internet 上建立電話呼叫的軟件程序。另一個例子就是 VoIP Phone——一 種使用 VoIP 的電話。下面列出了建立呼叫所需的步驟:
A 邀請 B 開始會話。作為邀請的一部分,A 會說明自己支持的媒體。
B 接收到邀請並向 A 發送一個及時響應,然後對邀請進行評估。
當 B 准備好接受邀請時,它會向 A 發送一個回執。作為回執的一部分,B 將說明自己支持的媒體。
A 分析從 B 收到的回執,並確定 B 和 A 支持的媒體是否相同。如果 A 和 B 支持相同的媒體,則它們之間將建立呼叫。邀請中指定的媒 體可以簡化呼叫的建立。
圖 1 演示了建立呼叫的步驟。
圖 1. 建立呼叫的步驟
SIP 提供了一種標准的方式來執行這些步驟。它通過定義特定的請求方法、響應、響應代碼,以及信號傳輸和呼叫控制的頭部來完成這些步 驟。該協議已由 Internet Engineering Task Force (IETF) 根據 RFC3261 實現了標准化,現已被 第 3 代合作伙伴項目(3GPP) 采納為標 准信號傳輸協議,還成為了 IP 多媒體子系統(IP Multimedia Subsystem,IMS) 架構中的永久元素。
SIP 與 HTTP 有何關系?
人們通常會問 SIP 是否使用 HTTP 作為底層協議。答案是否定的。SIP 是一種與 HTTP 在同一層(即應用層)運作的協議,它使用 TCP、 UDP 或 SCTP 作為底層協議。但是,SIP 與 HTTP 有很多相似之處。例如,與 HTTP 類似,SIP 基於文本而且是用戶可讀的。SIP 使用帶有特 定方法、響應代碼和頭部的“請求響應”機制,這一點也與 HTTP 類似。HTTP 和 SIP 的一個顯著不同是,SIP 中的“請求響應”機制是異步 的——請求不需要在後面緊跟相應的響應。實際上,一個 SIP 請求可能導致生成一個或多個請求。
SIP 是一種對等協議。這意味著用戶代理既可以作為服務器,也可以作為客戶機。這是 SIP 和 HTTP 的另一個不同之處,在 HTTP 中,客 戶機始終是客戶機,而服務器始終是服務器。
SIP 支持以下請求方法和響應代碼:
請求方法:
REGISTER。客戶機使用它向 SIP 服務器注冊一個地址。
INVITE。指示用戶和服務器被邀請參與一個會話。此消息的正文包括一個會話描述,用戶或服務被邀請參與該會話。
ACK。確認客戶端已經接收到 INVITE 請求的最終響應。此方法僅與 INVITE 請求一起使用。
CANCEL。用於取消掛起的請求。
BYE。由用戶代理客戶機發送,向服務器表明它希望終止呼叫。
OPTIONS。用於向服務器查詢與它相關的功能。
響應代碼:
1xx:臨時用途。表明操作被成功接收、理解和接受的 ACK。
3xx:重定向。需要進一步操作來處理此請求。
4xx:客戶機錯誤。請求包含錯誤的語法,不能在此服務器上進行處理。
5xx:服務器錯誤。服務器處理一個明顯有效的請求失敗。
6xx:全局失敗。不能在任何服務器上處理該請求。
會話描述協議
會話描述協議(Session Description Protocol,SDP)是一種描述在多媒體會話中使用的媒體格式和類型的格式。SIP 使用 SDP 作為其消 息中的一個有效載荷,以方便各種用戶代理之間的功能交換,例如,SDP 的內容可以指定用戶代理支持的編解碼器和要使用的協議,比如實時 傳輸協議(Real-time Transport Protocol,RTP)。
SIP 消息
圖 2 展示了 SIP 消息的組成部分。SIP 消息主要包括三部分:
請求行。指定請求方法、地址和 SIP 版本。
頭部。指定與要建立或終止的會話或呼叫相關的數據。
消息正文。提供有效載荷,也就是 SDP,描述用於會話的媒體。
圖 2. SIP 消息的組成部分
SIP Servlet 模型
SIP servlet 編程模型基於 servlet 編程模型。它使 SIP 中的編程與 Java EE 更加接近。Servlet 是處理傳入請求和將合適的響應發送 到客戶機的服務器端對象。它們通常部署到 servlet 容器中,並且具有定義良好的生命周期。servlet 容器負責管理容器中 servlet 的生命 周期以及管理與 servlet 所使用技術(比如 JNDI 和 JDBC)相關的資源。servlet 容器還管理 servlet 的網絡連接。
如前所述,SIP servlet 與 HTTP Servlet 類似,但前者處理的是 SIP 請求。SIP servlet 通過定義具體方法來處理各 SIP 請求方法。例 如,HTTP servlet 定義 doPost() 方法(該方法覆蓋 service() 方法)來處理 POST 請求。比較而言,SIP servlet 定義 doInvite() 方法 (也覆蓋了 service() 方法)來處理 INVITE 請求。
JSR116 定義了 SIP Servlet API 1.0。它指定了:
一個用於 SIP servlet 編程模型的 API。
SIP servlet 容器的職責。
SIP servlet 如何與 HTTP servlet 和 Java EE 組件交互。
最初的 SIP Servlet API 規范已被修訂為 JSR 289: SIP Servlet v1.1。
SIP Servlet API -- 關鍵概念
SIP servlet 底層的關鍵概念與 HTTP servlet 類似。以下各節簡短介紹其中的一些概念。
SipServletRequest 和 SipServletResponse
SIP 中的“請求響應”方法與 HTTP servlet 類似。請求在 SipServletRequest 對象中定義,而響應在 SipServletResponse 對象中定義 。但是,只有一個 ServletRequest 或 ServletResponse 對象是非空的。這是因為一個 SIP 請求不會導致對稱的響應。還有一個公共的高級 接口,稱為 SipServletMessage,SipServletRequest 和 SipServletResponse 對象都可以使用。SipServletMessage 接口定義 SipServletRequest 和 SipServletResponse 對象通用的方法。
圖 3 演示了 SipServletRequest 和 SipServletResponse 對象的層次結構。
Servlet 上下文
servlet 規范中定義的 servlet 上下文也適用於 SIP servlet。servlet 規范定義了一些特定的上下文屬性,用於存儲和檢索特定上下文 中的 SIP servlet 和接口信息。servlet 上下文可以與同一個規范中的 HTTP servlet 共享。這一點已在 Converged Applications 一節中詳 細解釋。
部署描述符
使用一種基於 XML 的部署描述符來描述 SIP servlets、調用它們的規則,以及應用程序中使用的資源和環境屬性。這個描述符位於一個 sip.xml 文件中,並且與 HTTP servlet 中使用的文件類似。sip.xml 文件由一個 XMl 模式定義。
SIP 應用程序打包
SIP 應用程序具有與 Web 應用程序相同的打包結構。它們被打包為擴展名為 .sar(Sip 歸檔文件)或 .war(Web 歸檔文件)的 JAR 格式 。
融和上下文和融和應用程序
應用程序可以使用 SIP 和 HTTP servlet 創建服務。為了允許在一個應用程序中同時使用 HTTP 和 SIP servlet,SIP servlet 規范定義 了一個 ConvergedContext 對象。這個對象保存 HTTP 和 SIP servlet 共享的 servlet 上下文,並為 HTTP 和 SIP servlet 提供在 servlet 上下文屬性、資源和 JNDI 名稱空間方面相同的應用程序視圖。
當應用程序同時包含 SIP 和 HTTP servlet 時,它就成為了一個融合應用程序(converged application)。這與僅包含 SIP 的應用程序 (稱為 SIP 應用程序)是相對的。融合應用程序在結構上與 SIP 應用程序類似,但是除 sip.xml 文件之外,它還使用一個 web.xml 文件作 為部署描述符。在 SIP Servlet API 1.1 (JSR289) 中,融合應用程序概念被擴展為也包括企業應用程序。企業應用程序現在可以包含一個 SIP 應用程序或融合應用程序作為模塊。這種類型的企業應用程序被稱為融合企業應用程序。
SIP 會話
SIP servlet 規范定義 SipSession 對象來表示基於 SIP 的會話,這與使用 HttpSession 對象表示基於 HTTP 的會話相同。因為單個應用 程序(比如融合應用程序)可以包含基於 HTTP 和 SIP 的會話,所以規范還定義了一個 SipApplicationSession 對象,這是一個應用程序級 別的會話對象。SipApplicationSession 對象在應用程序中擔當 HTTP 和 SIP 會話(也就是協議會話)的父會話。
注釋
回想一下,SIP Servlet API 1.1 的目標是使 SIP servlet 與 Java EE 5 保持一致。結果,該規范在 SIP servlet 和偵聽器中引入了 Java EE 5 定義的注釋。它還定義了自定義注釋來表示 SIP servlet 規范定義的接口。該規范引入了以下注釋:
@SipServlet。用於指示特定類是一個 SipServlet
@SipApplication。用於定義 SIP 應用程序。這個注釋擁有一組屬性,其中一個是 "name" 屬性,該屬性用於定義應用程序的名稱。 SipApplication 注釋可用於為構成應用程序的 servlet 創建一個邏輯集合,而無需使用部署描述符。
@SipListener。允許將某個特定類注冊為特定應用程序的 SipListener。SIP 應用程序的名稱被定義為此注釋的一個屬性。
@SipApplicationKey。幫助定義 SIP 應用程序的 SipApplicationKey 的方法層。SipApplicationKey 用於將請求與現有的 SipApplicationSession 關聯。
Project Sailfin - 開源的 SIP 應用服務器
SIP servlet 容器可以是獨立的,即僅支持 SIP servlet,也可以是同時支持 HTTP 和 SIP servlet 的融合容器。但是,對於大多數企業 應用,SIP servlet 容器必須是應用服務器中的一個融合容器。 Project Sailfin 旨在使用 GlassFish 應用服務器生成 SIP servlet 容器的 開源實現。該項目由 java.net 開發,Sun 和 Ericsson 是主要的貢獻者。Sailfin 是在 SailFin 項目中開發的 GlassFish 中的 SIP servlet 容器實現,它支持 SIP Servlet API 1.0 並計劃在 SIP Servlet API 1.1 完成之後提供對該規范的支持。
CallSetup 示例應用程序
本文使用的示例應用程序名為 CallSetup,是 SailFin 下載的一部分。您可以從 下載 SailFin 版本 頁面下載 SailFin。遵循 SailFin 項目 - 指令 來安裝和配置 SailFin。CallSetup 應用程序的代碼位於 <sailfin-install-home>/samples/sipservlet/CallSetup 目錄 ,其中 <sailfin-install-home> 是安裝 SailFin 的目錄。
CallSetup 應用程序使用 SIP servlet 和 HTTP servlet 來提供 VoIP 電話服務。該應用程序借助背靠背用戶代理(Back-to-Back User Agent,B2BUA)SIP servlet 來建立 VoIP 呼叫。B2BUA 單獨調用每個用戶代理,然後將它們連接起來,從而在兩個用戶代理之間建立呼叫。
CallSetup 組件
CallSetup 包括以下組件:
Registration.java。一個表示注冊用戶的 Plain Old Java Object (POJO)。
RegistrarServlet。一個允許用戶注冊的 SIP servlet。該 servlet 還用於在與 SailFin 綁定的 Java DB 數據庫中持久化用戶數據。
RegistrationBrowserServlet.java。一個 HTTP Servlet,提供了一個接口用於選擇呼叫的注冊用戶。
SipCallSetupServlet.java。一個將 INVITE 消息發送到第一個用戶 (UserB) 的 HTTP Servlet。
B2BCallServlet.java。該 SIP Servlet 處理來自第一個用戶的響應並與第二個用戶(User A)建立呼叫。
web.xml。HTTP servlet 的部署描述符。
sip.xml。SIP servlet 的部署描述符。
sun-web.xml。一個特定於產品的部署描述符。
persistence.xml。定義持久單元。
圖 4 展示了應用程序的執行順序。
圖 4. CallSetup 的執行順序
讓我們看一看構成 CallSetup 的組件中的一些代碼。此處未顯示應用程序的所有組件,也沒顯示每個組件中的所有代碼。建議在 SailFin 下載中研究應用程序的完整代碼。
RegistrarServlet.java
當用戶代理發送 REGISTER 請求時,doRegister() 方法將被調用並存儲注冊數據。然後將一個帶有狀態碼的響應發送給用戶代理。
import com.ericsson.sip.Registration;
@PersistenceContext(name = "persistence/LogicalName", unitName = "EricssonSipPU")
public class RegistrarServlet extends SipServlet{
The PersistenceUnit annotation is used to annotate the EntityManagerFactory with the name of the PU to be used.
@PersistenceUnit(unitName = "EricssonSipPU")
private EntityManagerFactory emf;
The Resource annotation is used to inject the UserTransaction
@Resource
private UserTransaction utx;
protected void doRegister(SipServletRequest request) throws ServletException, IOException {
SipServletResponse response = request.createResponse(200);
try {
The SipServletRequest object is parsed to get to address and request headers. The Contact header is obtained and stored in the database
SipURI to = cleanURI((SipURI) request.getTo().getURI());
ListIterator<Address> li = request.getAddressHeaders("Contact");
while (li.hasNext()){
Address na = li.next();
SipURI contact = (SipURI) na.getURI();
logger.log(Level.FINE, "Contact = " + contact);
An EntityManager object is created for storing the user data.
EntityManager em = emf.createEntityManager();
try {
utx.begin();
Registration registration = new Registration();
registration.setId(to.toString());
registration = em.merge(registration);
em.remove(registration);
utx.commit();
logger.log(Level.FINE, "Registration was successfully created.");
} catch (Exception ex) {
try {
utx.rollback();
} catch (Exception e) {
}
}
em.close();
If the registration is successful , a response code of 200 OK is sent
response.send();
} catch(Exception e) {
If the registration is not successful , a response code of 500 is sent
response.setStatus(500);
response.send();
}
}
RegistrationBrowserServlet.java
這是一個 HTTP Servlet,它提供了一個接口,用於列出注冊用戶並在兩個注冊用戶之間建立呼叫。
@PersistenceContext(name = "persistence/LogicalName", unitName = "EricssonSipPU")
public class RegistrationBrowserServlet extends HttpServlet {
@PersistenceUnit(unitName = "EricssonSipPU") private EntityManagerFactory emf;
public Collection getRegistrations() {
EntityManager em = emf.createEntityManager();
Query q = em.createQuery("select object(o) from Registration as o");
return q.getResultList();
}
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
This gets the list of registrations
Collection registrations = getRegistrations();
Iterator iter = registrations.iterator();
out.println("<html><body>");
Call to the HTTP servlet SipCallSetupServlet
out.println("<FORM ACTION = "/CallSetup/SipCallsetupServlet" METHOD = POST>");
out.println("<INPUT TYPE=SUBMIT NAME=Submit VALUE="Submit">");
out.println("</FORM>");
out.println("SipFactoryInstance = "+sf.toString());
out.println("</body></html>");
out.close();
}
SipCallSetupServlet.java
此 HTTP Servlet 通過 RegistrationBrowserServlet 調用,其行為類似於 B2BUA 在兩個用戶之間建立呼叫。
public class SipCallSetupServlet extends HttpServlet {
SipFactory sf = null;
TimerService ts = null;
ServletContext ctx = null;
public void init(ServletConfig config) throws ServletException {
ctx = config.getServletContext();
Getting the SIpFactory object from the ServletContext
sf = (SipFactory) ctx.getAttribute(SipServlet.SIP_FACTORY);
}
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String callA = null;
String callB = null;
Getting the contacts from request parameters
String[] contacts = request.getParameterValues("CONTACT");
if ( contacts.length < 2 ) {
return;
}
callA = contacts[0];
callB = contacts[1];
Using the SipFactory object to create a SipApplicationSession
SipApplicationSession as = sf.createApplicationSession();
Using the SipFactory object to create a "To" and "From" Address objects
Address to = sf.createAddress(callB);
Address from = sf.createAddress(callA);
Creating a SipServletRequest using the SipFactory and the SipApplicationSessionObjects created. Note that
INVITE is being sent as if UserA is inviting UserB.
SipServletRequest sipReq = sf.createRequest(as, "INVITE", from, to);
logger.log(Level.FINE, "SipCallSetupServlet sipRequest = " + sipReq.toString());
Set an attribute in SipServletRequest to indicate that this is an initial INVITE
sipReq.setAttribute("CALL","INITIAL");
// set servlet to invoke by response
Getting a SipSession from the Request created
SipSession s = sipReq.getSession();
This is the key part. We set name of the servlet that would handler the response for the
Request being sent. Here b2b is the name of the SIP Servlet that would handle the response
to this request.
s.setHandler("b2b");
// lets send invite to B ...
Sending the request
sipReq.send();
}
B2BCallServlet.java
這個 SIP Servlet 接收並處理針對 SipCallSetupServlet 發送的 INVITE 請求的響應。該 servlet 處理響應頭部和正文,獲取 SDP,並 向其他用戶代理發送另一個 INVITE 請求和 SDP 元數據。接收到來自其他用戶的帶有成功響應代碼的響應之後,該 servlet 在兩個用戶之間 建立呼叫。
public class B2BCallServlet extends SipServlet {
SipFactory sf = null;
ServletContext ctx = null;
public void init(ServletConfig config) throws ServletException {
super.init(config);
ctx = config.getServletContext();
Get the SipFactory from the ServletContext
sf = (SipFactory) ctx.getAttribute(SipServlet.SIP_FACTORY);
ts = (TimerService) ctx.getAttribute(SipServlet.TIMER_SERVICE);
}
protected void doResponse(SipServletResponse resp) throws ServletException, IOException {
get the SipApplicationSession and SipServletRequest from the response
SipApplicationSession sas = resp.getApplicationSession(true);
SipServletRequest origReq = resp.getRequest();
String alreadySent = (String) origReq.getAttribute("SENT_INVITE");
if( alreadySent == null && resp.getContentLength() > 0 && resp.getContentType ().equalsIgnoreCase("application/sdp")) {
String responseFrom = (String) origReq.getAttribute("CALL");
Check if this an response to INITIAL INVITE sent from the HTTP Servlet, and if there is
an SDP sent in the response, create an INVITE to the other user
if("INITIAL".equals(responseFrom)) {
//Take the SDP and send to A
Note that To address in the orginal request is the From address here and vice versa. This is what makes this servlet act like
a B2BUA.
SipServletRequest newReq = sf.createRequest(sas,"INVITE",origReq.getTo(),origReq.getFrom());
newReq.setContent(resp.getContent(),resp.getContentType());
SipSession ssA = newReq.getSession(true);
SipSession ssB = resp.getSession(true);
Set the SipSession object as a session attribute to each call leg
ssA.setAttribute("OTHER_SESSION",ssB);
ssB.setAttribute("OTHER_SESSION",ssA);
//Test
Set the b2b servlet as the handler for the response for the new request being sent.
ssA.setHandler("b2b");
ssB.setHandler("b2b");
origReq.setAttribute("SENT_INVITE","SENT_INVITE");
send the request to the other user
newReq.send(); //Send to A
} else {
If this is a response from User A then get the SDP from User A and set it i
SipSession ssB = (SipSession) resp.getSession().getAttribute("OTHER_SESSION");
ssB.setAttribute("SDP",resp.getContent());
}
} else {
return;
}
// Count so that both sides sent 200.
If response has a 200OK as the status code
if( resp.getStatus() == 200 ) {
Check if this is a response from the UserB ( first user)
SipServletResponse first = (SipServletResponse) sas.getAttribute("GOT_FIRST_200");
if( first == null ) { // This is the first 200
sas.setAttribute("GOT_FIRST_200",resp);
}
else { //This is the second 200 sen both ACK
This is a second response and now we send an ACK to both
User A and UserB. This exchanges the SDP and sets up the call.
sendAck(resp);
sendAck(first);
}
}
}
This method sends the ACK with the SDP.
private void sendAck( SipServletResponse resp ) throws IOException {
SipServletRequest ack = resp.createAck();
//Check if pending SDP to include in ACK
Object content = resp.getSession().getAttribute("SDP");
if( content != null ) {
ack.setContent(content,"application/sdp");
}
ack.send();
}
}
sip.xml
sip.xml 文件定義 SIP servlet 並指定它們的映射。SIP servlet 的映射使用 equal、and、or 和 not 運算符來定義調用 servlet 的條 件。在本例中,與此條件匹配的請求方法為 REGISTER、INVITE、OPTIONS 或 MESSAGE。
<sip-app>
<display-name>SIP Registrar</display-name>
<description>SIP Registrar application</description>
<servlet>
<servlet-name>registrar</servlet-name>
<description>Registrar SIP servlet</description>
<servlet-class>com.ericsson.sip.RegistrarServlet</servlet-class>
<init-param>
<param-name>Registrar_Domain</param-name>
<param-value>ericsson.com</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet>
<servlet-name>b2b</servlet-name>
<servlet-class>com.ericsson.sip.B2BCallServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>registrar</servlet-name>
<pattern>
<and>
<equal>
<var ignore-case="false">request.uri.host</var>
<value>test.com</value>
</equal>
<or>
<equal>
<var ignore-case="false">request.method</var>
<value>REGISTER</value>
</equal>
<equal>
<var ignore-case="false">request.method</var>
<value>INVITE</value>
</equal>
<equal>
<var ignore-case="false">request.method</var>
<value>OPTIONS</value>
</equal>
<equal>
<var ignore-case="false">request.method</var>
<value>MESSAGE</value>
</equal>
</or>
</and>
</pattern>
</servlet-mapping>
</sip-app>
persistence.xml
persistence.xml 文件定義持久單元 EricssonSipPU,後者用於在數據庫中持久化注冊數據。應用程序使用 Sailfin 中可用的默認 JDBC 資源 jdbc/__default。
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema- instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
<persistence-unit name="EricssonSipPU" transaction-type="JTA">
<provider>oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider</provider>
<jta-data-source>jdbc/__default</jta-data-source>
<properties>
<property name="toplink.ddl-generation" value="drop-and-create-tables"/>
</properties>
</persistence-unit>
</persistence>
運行示例應用程序
要運行 CallSetup 應用程序,請遵循 SailFin 項目 - 指令 頁面上的步驟。如果成功執行了這些步驟,您應該能夠在兩個 softphone 客 戶機之間建立一個呼叫。兩個選定端點上的電話應該會振鈴。
結束語
本文涵蓋了 SIP 和 SIP servlet 內部的一些基本概念。文章還提供一個示例應用程序,該應用程序使用 SIP servlet 和 HTTP servlet 來提供 VoIP 電話服務;並介紹了 SailFin 項目,該項目正在使用 GlassFish 應用服務器構建 SIP servlet 容器的開源實現。