Tomcat 高級(jí) IO 機(jī)制

2022-03-03 14:08 更新

簡(jiǎn)介

由于基于 APR 或 NIO API 來(lái)構(gòu)建連接器,Tomcat 能在通常的阻塞 IO 之上提供一些擴(kuò)展,從而支持 Servlet API。

重要說(shuō)明:這些特性需要使用 APR 或 NIO HTTP 連接器。經(jīng)典的 java.io HTTP 連接器 與 AJP 連接器并不支持它們。

Comet 支持

Comet 支持能讓 Servlet 實(shí)現(xiàn):對(duì) IO 的異步處理;當(dāng)連接可以讀取數(shù)據(jù)時(shí),接收事件(而不是總使用阻塞讀取);將數(shù)據(jù)異步地寫(xiě)入連接(很可能是響應(yīng)其他一些源所產(chǎn)生的事件)。

1. Comet 事件

根據(jù)發(fā)生的具體事件,實(shí)現(xiàn) org.apache.catalina.comet.CometProcessor 接口的 Servlet 將調(diào)用自己的事件方法,而非通常的服務(wù)方法。事件對(duì)象允許訪問(wèn)常見(jiàn)的請(qǐng)求與響應(yīng)對(duì)象,使用方式與通常方式相同。主要的區(qū)別在于:在處理 BEGIN 事件到 END 或 ERROR 事件之間,這些事件對(duì)象能夠保持有效和完整的功能性。事件類(lèi)型如下:

  • EventType.BEGIN 在連接處理開(kāi)始時(shí)被調(diào)用,用來(lái)初始化使用了請(qǐng)求和響應(yīng)對(duì)象的相關(guān)字段。從處理完該事件后直到 END 或 ERROR 事件開(kāi)始處理時(shí)的這段時(shí)間內(nèi),有可能使用響應(yīng)對(duì)象在開(kāi)放連接中寫(xiě)入數(shù)據(jù)。注意,響應(yīng)對(duì)象以及所依賴(lài)的 OutputStream 和 Writer 仍不能同步,因此在通過(guò)多個(gè)線程訪問(wèn)它們時(shí),需要進(jìn)行強(qiáng)制實(shí)現(xiàn)同步操作。處理完初始化事件后,就可以提交請(qǐng)求對(duì)象了。

  • EventType.READ 該事件表明可以使用輸入數(shù)據(jù),讀取過(guò)程不會(huì)阻塞??梢允褂?InputStream 或 Reader 的 available 和 ready 方法來(lái)確定是否存在阻塞危險(xiǎn):當(dāng)數(shù)據(jù)被報(bào)告可讀時(shí),Servlet 應(yīng)該進(jìn)行讀取。當(dāng)讀取遇到錯(cuò)誤時(shí),Servlet 可以通過(guò)正確傳播 Exception 屬性來(lái)報(bào)告這一情況。拋出異常會(huì)導(dǎo)致 ERROR 事件的調(diào)用,連接就會(huì)關(guān)閉。另外,也有可能捕獲一個(gè)異常,在 Servlet 可能使用的數(shù)據(jù)結(jié)構(gòu)上進(jìn)行清理,然后使用事件的 close 方法。不允許從 Servlet 對(duì)象執(zhí)行方法外部去讀取數(shù)據(jù)。

    在一些平臺(tái)(比如 Windows)上,利用 READ 事件來(lái)表示客戶(hù)端斷開(kāi)連接。從流中讀取的結(jié)果可能是 -1、IOException 異?;?EOFException 異常。一定要正確處理這些情況。如果你沒(méi)有捕捉到 IOException 異常,那么當(dāng) Tomcat 捕獲到異常時(shí),它會(huì)立刻調(diào)用你的事件隊(duì)列生成一個(gè) ERROR 事件來(lái)存儲(chǔ)這些錯(cuò)誤,并且你會(huì)馬上收到這個(gè)消息。

  • EventType.END 請(qǐng)求處理完畢時(shí),就會(huì)調(diào)用 END 方法。Begin 方法初始化的字段也將被重置。在處理完這一事件后,請(qǐng)求和響應(yīng)對(duì)象,以及它們所依賴(lài)的對(duì)象,都將被回收,以便再去處理其他請(qǐng)求。當(dāng)數(shù)據(jù)可讀取時(shí),以及到達(dá)請(qǐng)求輸入的文件末尾時(shí)(這通常表明客戶(hù)端通過(guò)管線提交請(qǐng)求),也會(huì)調(diào)用 END。

  • EventType.ERROR:當(dāng)連接上出現(xiàn) IO 異常或類(lèi)似的不可回收的錯(cuò)誤時(shí),容器就會(huì)調(diào)用 ERROR。在開(kāi)始時(shí)候被初始化的字段在這時(shí)候被重置。在處理完這一事件后,請(qǐng)求和響應(yīng)對(duì)象,以及它們所依賴(lài)的對(duì)象,都將被回收,以便再去處理其他請(qǐng)求。

下面是一些事件子類(lèi)別,通過(guò)它們可以對(duì)事件處理過(guò)程進(jìn)行微調(diào)(注意:其中有些事件可能需要使用 org.apache.catalina.valves.CometConnectionManagerValve 值):

  • EventSubType.TIMEOUT: 連接超時(shí)(ERROR 的子類(lèi)別)。注意,這個(gè) ERROR 子類(lèi)型并不是必須的。除非 servlet 使用該事件的 close 方法,否則連接將不會(huì)關(guān)閉。
  • EventSubType.CLIENT_DISCONNECT:客戶(hù)端連接被關(guān)閉(ERROR 的子類(lèi)別)。
  • EventSubType.IOEXCEPTION:表示發(fā)生了 IO 異常(比如無(wú)效內(nèi)容),例如無(wú)效的塊阻塞(ERROR 的子類(lèi)別)。
  • EventSubType.WEBAPP_RELOAD:重新加載 Web 應(yīng)用(END 的子類(lèi)別)。
  • EventSubType.SESSION_END:Servlet 終止了會(huì)話(END 的子類(lèi)別)。

如上所述,Comet 請(qǐng)求的典型生命周期會(huì)包含一系列的事件:BEGIN -> READ -> READ -> READ -> ERROR/TIMEOUT。任何時(shí)候,Servlet 都能用事件的 close 方法來(lái)終止對(duì)請(qǐng)求的處理。

2. Comet 過(guò)濾器

跟一般的過(guò)濾器一樣,當(dāng)處理 Comet 事件時(shí),就會(huì)調(diào)用一個(gè)過(guò)濾器隊(duì)列。這些過(guò)濾器應(yīng)該實(shí)現(xiàn) CometFilter 接口(和常用的過(guò)濾器接口一樣),在部署描述符文件中的聲明與映像也都和通常的過(guò)濾器一樣。當(dāng)過(guò)濾器隊(duì)列在處理事件時(shí),它將只含有那些跟所有通常映射規(guī)則相匹配的過(guò)濾器,并且這些過(guò)濾器要實(shí)現(xiàn) CometFilter 接口。

3. 范例代碼

在下面的范例偽碼中,通過(guò)使用上文所述的 API,Servlet 實(shí)現(xiàn)了異步聊天功能。

public class ChatServlet
    extends HttpServlet implements CometProcessor {

    protected ArrayList<HttpServletResponse> connections =
        new ArrayList<HttpServletResponse>();
    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("<!DOCTYPE html>");
            writer.println("<head><title>JSP Chat</title></head><body>");
            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("</body></html>");
            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<String> messages = new ArrayList<String>();

        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] + "<br>");
                            }
                            writer.flush();
                        } catch (IOException e) {
                            log("IOExeption sending message", e);
                        }
                    }
                }

            }

        }

    }

}

4. Comet 超時(shí)

如果使用 NIO 連接器,你可以為不同的 comet 連接設(shè)置單獨(dú)的超時(shí)。只需設(shè)置一個(gè)如下所示的請(qǐng)求屬性即可設(shè)置超時(shí):

CometEvent event.... event.setTimeout(30*1000);

event.getHttpServletRequest().setAttribute("org.apache.tomcat.comet.timeout", new Integer(30 * 1000));

超時(shí)被設(shè)置為 30 秒。重要說(shuō)明:為了設(shè)置超時(shí),必須完成 BEGIN 事件。默認(rèn)值為 soTimeout

如果使用 APR 連接器,所有的 Comet 連接將擁有統(tǒng)一的超時(shí)值:soTimeout*50。

異步寫(xiě)操作

當(dāng) APR 或 NIO 可用時(shí),Tomcat 支持使用 sendfile 方式去發(fā)送大型靜態(tài)文件。只要系統(tǒng)負(fù)載一增加,就會(huì)異步地高效執(zhí)行寫(xiě)操作。作為一種使用阻塞寫(xiě)操作發(fā)送大型響應(yīng)的替代方式,有可能使用 sendfile 代碼來(lái)將內(nèi)容寫(xiě)入靜態(tài)文件。緩存值將利用這一點(diǎn)將響應(yīng)數(shù)據(jù)緩存至文件而非存儲(chǔ)在內(nèi)存中。如果請(qǐng)求屬性 org.apache.tomcat.sendfile.support 設(shè)為 Boolean.TRUE,則表示支持 sendfile。

通過(guò)合適的請(qǐng)求屬性,任何 Servlet 都可以指示 Tomcat 執(zhí)行 sendfile 調(diào)用。正確地設(shè)置響應(yīng)長(zhǎng)度也是很有必要的。在使用 sendfile 時(shí),最好確定請(qǐng)求與響應(yīng)都沒(méi)有被包裝起來(lái)。因?yàn)樯院筮B接器本身將發(fā)送響應(yīng)主體,所以不能夠過(guò)濾響應(yīng)主體。除了設(shè)置 3 個(gè)所需的請(qǐng)求屬性之外,Servlet 不應(yīng)該發(fā)送任何響應(yīng)數(shù)據(jù),但能使用一些能夠修改響應(yīng)報(bào)頭的方法(比如設(shè)定 cookie)。

  • org.apache.tomcat.sendfile.filename 作為字符串發(fā)送的標(biāo)準(zhǔn)文件名。
  • org.apache.tomcat.sendfile.start開(kāi)始位置偏移值,長(zhǎng)整型值。
  • org.apache.tomcat.sendfile.end 結(jié)束位置偏移值,長(zhǎng)整型值。

除了設(shè)置這些屬性,還有必要設(shè)置內(nèi)容長(zhǎng)度報(bào)頭。不要指望 Tomcat 來(lái)處理,因?yàn)槟憧赡芤呀?jīng)將數(shù)據(jù)寫(xiě)入輸出流了。

注意,使用 sendfile 將禁止 Tomcat 可能在響應(yīng)中執(zhí)行的壓縮操作。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)