由于基于 APR 或 NIO API 來構(gòu)建連接器,Tomcat 能在通常的阻塞 IO 之上提供一些擴展,從而支持 Servlet API。
重要說明:這些特性需要使用 APR 或 NIO HTTP 連接器。經(jīng)典的 java.io HTTP 連接器 與 AJP 連接器并不支持它們。
Comet 支持能讓 Servlet 實現(xiàn):對 IO 的異步處理;當連接可以讀取數(shù)據(jù)時,接收事件(而不是總使用阻塞讀取);將數(shù)據(jù)異步地寫入連接(很可能是響應(yīng)其他一些源所產(chǎn)生的事件)。
根據(jù)發(fā)生的具體事件,實現(xiàn) org.apache.catalina.comet.CometProcessor 接口的 Servlet 將調(diào)用自己的事件方法,而非通常的服務(wù)方法。事件對象允許訪問常見的請求與響應(yīng)對象,使用方式與通常方式相同。主要的區(qū)別在于:在處理 BEGIN 事件到 END 或 ERROR 事件之間,這些事件對象能夠保持有效和完整的功能性。事件類型如下:
下面是一些事件子類別,通過它們可以對事件處理過程進行微調(diào)(注意:其中有些事件可能需要使用 org.apache.catalina.valves.CometConnectionManagerValve 值):
如上所述,Comet 請求的典型生命周期會包含一系列的事件:BEGIN -> READ -> READ -> READ -> ERROR/TIMEOUT。任何時候,Servlet 都能用事件的 close 方法來終止對請求的處理。
跟一般的過濾器一樣,當處理 Comet 事件時,就會調(diào)用一個過濾器隊列。這些過濾器應(yīng)該實現(xiàn) CometFilter 接口(和常用的過濾器接口一樣),在部署描述符文件中的聲明與映像也都和通常的過濾器一樣。當過濾器隊列在處理事件時,它將只含有那些跟所有通常映射規(guī)則相匹配的過濾器,并且這些過濾器要實現(xiàn) CometFilter 接口。
在下面的范例偽碼中,通過使用上文所述的 API,Servlet 實現(xiàn)了異步聊天功能。
public class ChatServlet
extends HttpServlet implements CometProcessor {
protected ArrayList connections =
new ArrayList();
protected MessageSender messageSender = null;
public void init() throws ServletException {
messageSender = new MessageSender();
Thread messageSenderThread =
new Thread(messageSender, "MessageSender[" + getServletContext().getContextPath() + "]");
messageSenderThread.setDaemon(true);
messageSenderThread.start();
}
public void destroy() {
connections.clear();
messageSender.stop();
messageSender = null;
}
/**
* Process the given Comet event.
*
* @param event The Comet event that will be processed
* @throws IOException
* @throws ServletException
*/
public void event(CometEvent event)
throws IOException, ServletException {
HttpServletRequest request = event.getHttpServletRequest();
HttpServletResponse response = event.getHttpServletResponse();
if (event.getEventType() == CometEvent.EventType.BEGIN) {
log("Begin for session: " + request.getSession(true).getId());
PrintWriter writer = response.getWriter();
writer.println("");
writer.println("JSP Chat
");
writer.flush();
synchronized(connections) {
connections.add(response);
}
} else if (event.getEventType() == CometEvent.EventType.ERROR) {
log("Error for session: " + request.getSession(true).getId());
synchronized(connections) {
connections.remove(response);
}
event.close();
} else if (event.getEventType() == CometEvent.EventType.END) {
log("End for session: " + request.getSession(true).getId());
synchronized(connections) {
connections.remove(response);
}
PrintWriter writer = response.getWriter();
writer.println("
");
event.close();
} else if (event.getEventType() == CometEvent.EventType.READ) {
InputStream is = request.getInputStream();
byte[] buf = new byte[512];
do {
int n = is.read(buf); //can throw an IOException
if (n > 0) {
log("Read " + n + " bytes: " + new String(buf, 0, n)
+ " for session: " + request.getSession(true).getId());
} else if (n < 0) {
error(event, request, response);
return;
}
} while (is.available() > 0);
}
}
public class MessageSender implements Runnable {
protected boolean running = true;
protected ArrayList messages = new ArrayList();
public MessageSender() {
}
public void stop() {
running = false;
}
/**
* Add message for sending.
*/
public void send(String user, String message) {
synchronized (messages) {
messages.add("[" + user + "]: " + message);
messages.notify();
}
}
public void run() {
while (running) {
if (messages.size() == 0) {
try {
synchronized (messages) {
messages.wait();
}
} catch (InterruptedException e) {
// Ignore
}
}
synchronized (connections) {
String[] pendingMessages = null;
synchronized (messages) {
pendingMessages = messages.toArray(new String[0]);
messages.clear();
}
// Send any pending message on all the open connections
for (int i = 0; i < connections.size(); i++) {
try {
PrintWriter writer = connections.get(i).getWriter();
for (int j = 0; j < pendingMessages.length; j++) {
writer.println(pendingMessages[j] + "
");
}
writer.flush();
} catch (IOException e) {
log("IOExeption sending message", e);
}
}
}
}
}
}
}
如果使用 NIO 連接器,你可以為不同的 comet 連接設(shè)置單獨的超時。只需設(shè)置一個如下所示的請求屬性即可設(shè)置超時:
CometEvent event.... event.setTimeout(30*1000);
或
event.getHttpServletRequest().setAttribute("org.apache.tomcat.comet.timeout", new Integer(30 * 1000));
超時被設(shè)置為 30 秒。重要說明:為了設(shè)置超時,必須完成 BEGIN 事件。默認值為 soTimeout。
如果使用 APR 連接器,所有的 Comet 連接將擁有統(tǒng)一的超時值:soTimeout*50。
當 APR 或 NIO 可用時,Tomcat 支持使用 sendfile 方式去發(fā)送大型靜態(tài)文件。只要系統(tǒng)負載一增加,就會異步地高效執(zhí)行寫操作。作為一種使用阻塞寫操作發(fā)送大型響應(yīng)的替代方式,有可能使用 sendfile 代碼來將內(nèi)容寫入靜態(tài)文件。緩存值將利用這一點將響應(yīng)數(shù)據(jù)緩存至文件而非存儲在內(nèi)存中。如果請求屬性 org.apache.tomcat.sendfile.support 設(shè)為 Boolean.TRUE,則表示支持 sendfile。
通過合適的請求屬性,任何 Servlet 都可以指示 Tomcat 執(zhí)行 sendfile 調(diào)用。正確地設(shè)置響應(yīng)長度也是很有必要的。在使用 sendfile 時,最好確定請求與響應(yīng)都沒有被包裝起來。因為稍后連接器本身將發(fā)送響應(yīng)主體,所以不能夠過濾響應(yīng)主體。除了設(shè)置 3 個所需的請求屬性之外,Servlet 不應(yīng)該發(fā)送任何響應(yīng)數(shù)據(jù),但能使用一些能夠修改響應(yīng)報頭的方法(比如設(shè)定 cookie)。
除了設(shè)置這些屬性,還有必要設(shè)置內(nèi)容長度報頭。不要指望 Tomcat 來處理,因為你可能已經(jīng)將數(shù)據(jù)寫入輸出流了。
注意,使用 sendfile 將禁止 Tomcat 可能在響應(yīng)中執(zhí)行的壓縮操作。