來源:《How Tomcat Works》
Servlet容器的工作原理:
1、創建一個request對象並填充那些有可能被所引用的servlet使用的信息,比如參數、頭部、cookies、查詢字符串、URL等。而一個request對象是javax.servlet.ServletRequest或javax.servlet.http.ServletRequest接口的一個實例對象。
2、創建一個response對象,所引用的servlet使用它給客戶端發送響應。一個response對象javax.servlet.ServletResponse或javax.servlet.http.ServletResponse接口的一個實例。
3、調用servlet的service方法,並傳入request和response對象,而servlet會從request對象中取值,給response對象寫值。
Catalina:是tomcat所使用的Servlet的容器。由兩個主要模塊組成,連接器(connector)和容器(container),連接器用來連接容器里面的請求,工作是接收到每一個http請求,構造一個resquest對象和response對象。然后傳遞給容器。容器接收到對象之后,調用servlet的service方法用於響應。
一個簡單的web服務器:超文本傳輸協議服務器(http),一個基於java的web服務器使用的是兩個基本的類,java.net.Socket和java.net.ServerSOcket.
Http協議:在Http中,始終是客戶端通過建立連接和發送一個http請求從而開啟一個事務。web服務器不需要聯系客戶端或者對客戶端做一個回調連接。無論客戶端或者服務器都可以提前終止連接。
Socket類:在計算機網絡中,socket是網絡連接的端點,使得一個應用可以從網絡中讀取和寫入數據。放在兩個不同計算機上的應用可以通過連接發送和接收字節流。在java中,套接字是一個類。
new Socket("ip地址",port);
Socket類是一個客戶端類,創建了socket類之后,可以使用它來向服務器發送和接收字節流。首先要調用Socket類中的getOutputStream方法來獲取java.io.OutputStream對象。要發送的對象是文本,需要構造一個printWrite對象。而要想從遠程端接收對象,可以使用Socket類中的getInputStream,來返回對象。
本質上來說,socket發送的是OutputStream對象,接收的是InputStream。

package test; import java.io.*; import java.net.Socket; public class MySocket { public static void main(String[] args) { try{ //向本機的4700端口發出客戶請求 Socket socket=new Socket("127.0.0.1",4700); //由系統標准輸入設備構造BufferedReader對象 BufferedReader sin=new BufferedReader(new InputStreamReader(System.in)); //由Socket對象得到輸出流,並構造PrintWriter對象 PrintWriter os=new PrintWriter(socket.getOutputStream()); //由Socket對象得到輸入流,並構造相應的BufferedReader對象 BufferedReader is=new BufferedReader(new InputStreamReader(socket.getInputStream())); String readline; readline=sin.readLine(); //從系統標准輸入讀入一字符串 //若從標准輸入讀入的字符串為 "bye"則停止循環 while(!readline.equals("bye")){ //將從系統標准輸入讀入的字符串輸出到Server os.println(readline); //刷新輸出流,使Server馬上收到該字符串 os.flush(); //在系統標准輸出上打印讀入的字符串 System.out.println("Client:"+readline); //從Server讀入一字符串,並打印到標准輸出上 System.out.println("Server:"+is.readLine()); readline=sin.readLine(); //從系統標准輸入讀入一字符串 } os.close(); //關閉Socket輸出流 is.close(); //關閉Socket輸入流 socket.close(); //關閉Socket }catch(Exception e){ e.printStackTrace();//出錯,打印出錯信息 } } }
ServerSocket類:是一個服務器類,綁定了端口號,等待連接,其中accept方法可以接收一個socket對象,然后和客戶端交互。

package test; import java.io.*; import java.net.ServerSocket; import java.net.Socket; public class MyServerSocket { public static void main(String args[]) { try{ ServerSocket server=null; try{ //創建一個ServerSocket在端口4700監聽客戶請求 server=new ServerSocket(4700); }catch(Exception e){ e.printStackTrace();//出錯,打印出錯信息 } Socket socket=null; try{ //使用accept()阻塞等待客戶請求,有客戶 socket=server.accept();//請求到來則產生一個Socket對象,並繼續執行 }catch(Exception e){ e.printStackTrace();//出錯,打印出錯信息 } String line; //由Socket對象得到輸入流,並構造相應的BufferedReader對象 BufferedReader is=new BufferedReader(new InputStreamReader(socket.getInputStream())); //由Socket對象得到輸出流,並構造PrintWriter對象 PrintWriter os=new PrintWriter(socket.getOutputStream()); //由系統標准輸入設備構造BufferedReader對象 BufferedReader sin=new BufferedReader(new InputStreamReader(System.in)); //在標准輸出上打印從客戶端讀入的字符串 System.out.println("Client:"+is.readLine()); //從標准輸入讀入一字符串 line=sin.readLine(); //如果該字符串為 "bye",則停止循環 while(!line.equals("bye")){ //向客戶端輸出該字符串 os.println(line); //刷新輸出流,使Client馬上收到該字符串 os.flush(); //在系統標准輸出上打印讀入的字符串 System.out.println("Server:"+line); //從Client讀入一字符串,並打印到標准輸出上 System.out.println("Client:"+is.readLine()); //從系統標准輸入讀入一字符串 line=sin.readLine(); } os.close(); //關閉Socket輸出流 is.close(); //關閉Socket輸入流 socket.close(); //關閉Socket server.close(); //關閉ServerSocket }catch(Exception e){ e.printStackTrace();//出錯,打印出錯信息 } } }
下面定義一個應用程序,定義一個web服務器。來處理http請求。即HttpServer。
一個HttpServer類代表着一個web服務器。
這里實現一個簡易的從服務器申請靜態資源的服務器。

package test.pyrmont; import java.net.Socket; import java.net.ServerSocket; import java.net.InetAddress; import java.io.InputStream; import java.io.OutputStream; import java.io.IOException; import java.io.File; public class HttpServer { /** WEB_ROOT is the directory where our HTML and other files reside. * For this package, WEB_ROOT is the "webroot" directory under the working * directory. * The working directory is the location in the file system * from where the java command was invoked. */ public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot"; // shutdown command private static final String SHUTDOWN_COMMAND = "/SHUTDOWN"; // the shutdown command received private boolean shutdown = false; public static void main(String[] args) { HttpServer server = new HttpServer(); server.await(); } public void await() { ServerSocket serverSocket = null; int port = 8080; try { serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1")); } catch (IOException e) { e.printStackTrace(); System.exit(1); } // Loop waiting for a request while (!shutdown) { Socket socket = null; InputStream input = null; OutputStream output = null; try { socket = serverSocket.accept(); input = socket.getInputStream(); output = socket.getOutputStream(); // create Request object and parse Request request = new Request(input); request.parse(); // create Response object Response response = new Response(output); response.setRequest(request); response.sendStaticResource(); // Close the socket socket.close(); //check if the previous URI is a shutdown command shutdown = request.getUri().equals(SHUTDOWN_COMMAND); System.out.println(request.getUri()); } catch (Exception e) { e.printStackTrace(); continue; } } } }
該服務器提供了一個靜態的final變量WEB_ROOT所在的目錄和它下面所有的子目錄的靜態資源。其目錄是當前服務器的目錄路徑下的webroot目錄。在該目錄下的靜態資源均可以被訪問到。
同時定義了一個停止服務器的命令行,使用uri來停止服務器(這是比較危險的操作)。
創建一個ServerSocket,然后綁定端口,並使用accept來接收瀏覽器傳來的input。使用request對象的函數對input進行解析,其實就是從瀏覽器的地址欄里拼接出靜態資源的訪問地址。然后,將request傳給reponse,response根據傳過來的地址,去尋找靜態資源。找到后,利用socket的output,將文件響應給瀏覽器(這里chrome瀏覽器無法解析字節bytes數組,需要使用其他瀏覽器)

package test.pyrmont; import java.io.InputStream; import java.io.IOException; public class Request { private InputStream input; private String uri; public Request(InputStream input) { this.input = input; } public void parse() { // Read a set of characters from the socket StringBuffer request = new StringBuffer(2048); int i; byte[] buffer = new byte[2048]; try { i = input.read(buffer); } catch (IOException e) { e.printStackTrace(); i = -1; } for (int j=0; j<i; j++) { request.append((char) buffer[j]); } uri = parseUri(request.toString()); } private String parseUri(String requestString) { int index1, index2; index1 = requestString.indexOf(' '); if (index1 != -1) { index2 = requestString.indexOf(' ', index1 + 1); if (index2 > index1) return requestString.substring(index1 + 1, index2); } return null; } public String getUri() { return uri; } }

package test.pyrmont; import com.sun.org.apache.xpath.internal.SourceTree; import java.io.OutputStream; import java.io.IOException; import java.io.FileInputStream; import java.io.File; import java.util.Arrays; /* HTTP Response = Status-Line *(( general-header | response-header | entity-header ) CRLF) CRLF [ message-body ] Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF */ public class Response { private static final int BUFFER_SIZE = 1024; Request request; OutputStream output; public Response(OutputStream output) { this.output = output; } public void setRequest(Request request) { this.request = request; } public void sendStaticResource() throws IOException { byte[] bytes = new byte[BUFFER_SIZE]; FileInputStream fis = null; try { File file = new File(HttpServer.WEB_ROOT, request.getUri()); if (file.exists()) { fis = new FileInputStream(file); int ch = fis.read(bytes, 0, BUFFER_SIZE); while (ch!=-1) { output.write(bytes, 0, ch); ch = fis.read(bytes, 0, BUFFER_SIZE); } } else { // file not found String errorMessage = "HTTP/1.1 404 File Not Found\r\n" + "Content-Type: text/html\r\n" + "Content-Length: 23\r\n" + "\r\n" + "<h1>File Not Found</h1>"; output.write(errorMessage.getBytes()); } } catch (Exception e) { // thrown if cannot instantiate a File object System.out.println(e.toString() ); } finally { if (fis!=null) fis.close(); } } }
以上我們就實現了一個建議的web服務器,其功能是根據uri來請求服務器上的靜態資源。
一個簡單的Servlet容器
Servlet接口:
void init(ServletConfig var1) throws ServletException; ServletConfig getServletConfig(); void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException; String getServletInfo(); void destroy(); }
其中init()、service()、destroy()三個方法是servlet的生命周期方法。
在servlet類被初始化之后,init()方法被servlet所調用,且只調用一次,表明,servlet已經被加載進服務了。因此,init方法必須在servlet可以接受任何請求之前成功運行完畢。程序員可以在init方法里覆蓋的寫一些只運行一次的代碼。或者留空。
servlet容器調用service方法來處理servlet請求。servlet容器會傳遞一個javax.servlet.ServletRequest對象和javax.servlet.ServletResponse對象。Request對象包括客戶端的HTTP請求信息,而response對象封裝servlet的響應。service方法會被調用很多次。
servlet容器調用destroy方法來移除一個servlet實例。如果servlet容器正在被關閉或者servlet容器需要空閑內存,當所有servlet線程的service方法退出了,該函數才會被調用。
實現一個簡易的servlet類

package test.servlet; import javax.servlet.*; import java.io.IOException; import java.io.PrintWriter; public class PrimitiveServlet implements Servlet { public void init(ServletConfig servletConfig) throws ServletException { System.out.println("init"); } public ServletConfig getServletConfig() { return null; } public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { System.out.println("from service"); PrintWriter out = servletResponse.getWriter(); out.println("Hello. Roses are red."); out.print("Violets are blue."); } public String getServletInfo() { return null; } public void destroy() { System.out.println("destroy"); } }
這是一個很簡單的servlet的實現類,其中service方法只向瀏覽器輸出一句話。
然后實現一個簡單的servlet容器。HttpServer1

package test.servlet; import java.net.Socket; import java.net.ServerSocket; import java.net.InetAddress; import java.io.InputStream; import java.io.OutputStream; import java.io.IOException; public class HttpServer1 { /** WEB_ROOT is the directory where our HTML and other files reside. * For this package, WEB_ROOT is the "webroot" directory under the working * directory. * The working directory is the location in the file system * from where the java command was invoked. */ // shutdown command private static final String SHUTDOWN_COMMAND = "/SHUTDOWN"; // the shutdown command received private boolean shutdown = false; public static void main(String[] args) { HttpServer1 server = new HttpServer1(); server.await(); } public void await() { ServerSocket serverSocket = null; int port = 8080; try { serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1")); } catch (IOException e) { e.printStackTrace(); System.exit(1); } // Loop waiting for a request while (!shutdown) { Socket socket = null; InputStream input = null; OutputStream output = null; try { socket = serverSocket.accept(); input = socket.getInputStream(); output = socket.getOutputStream(); // create Request object and parse Request request = new Request(input); request.parse(); // create Response object Response response = new Response(output); response.setRequest(request); // check if this is a request for a servlet or a static resource // a request for a servlet begins with "/servlet/" if (request.getUri().startsWith("/servlet/")) { ServletProcessor1 processor = new ServletProcessor1(); processor.process(request, response); } else { StaticResourceProcessor processor = new StaticResourceProcessor(); processor.process(request, response); } // Close the socket socket.close(); //check if the previous URI is a shutdown command shutdown = request.getUri().equals(SHUTDOWN_COMMAND); } catch (Exception e) { e.printStackTrace(); System.exit(1); } } } }
和之前寫的服務器代碼一樣,之前寫的服務器可以申請靜態資源,而該服務器可以申請servlet資源。
if (request.getUri().startsWith("/servlet/")) { ServletProcessor1 processor = new ServletProcessor1(); processor.process(request, response); } else { StaticResourceProcessor processor = new StaticResourceProcessor(); processor.process(request, response); }
上面的代碼就是,當申請地址有servlet目錄的時候,我們需要申請一個servlet資源,如果沒有,就申請靜態資源。

package test.servlet; import java.io.File; import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; import java.net.URLStreamHandler; import javax.servlet.Servlet; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; public class ServletProcessor1 { public void process(Request request, Response response) { String uri = request.getUri(); String servletName = uri.substring(uri.lastIndexOf("/") + 1); URLClassLoader loader = null; try { // create a URLClassLoader URL[] urls = new URL[1]; URLStreamHandler streamHandler = null; File classPath = new File(Constants.WEB_ROOT); // the forming of repository is taken from the createClassLoader method in // org.apache.catalina.startup.ClassLoaderFactory String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)) .toString(); // the code for forming the URL is taken from the addRepository method in // org.apache.catalina.loader.StandardClassLoader class. urls[0] = new URL(null, repository, streamHandler); loader = new URLClassLoader(urls); } catch (IOException e) { System.out.println(e.toString()); } Class myClass = null; try { myClass = loader.loadClass(servletName); } catch (ClassNotFoundException e) { System.out.println(e.toString()); } Servlet servlet = null; try { servlet = (Servlet) myClass.newInstance(); servlet.service((ServletRequest) request, (ServletResponse) response); } catch (Exception e) { System.out.println(e.toString()); } catch (Throwable e) { System.out.println(e.toString()); } } }
上面代碼是封裝了一個servlet資源申請的類,首先需要得到請求的地址,以及servlet的名稱。然后創建一個類加載器,類加載器里面需要一個URL對象的數組,URL對象指向了加載類時的查找位置。而URL被認為是一個要被下載的jar包。
而URL的構建,需要一個表示資源的字符串,最后,調用類加載器的loadClass方法來得到該Servlet類,然后使用newInstance方法新建一個Servlet對象,然后調用其service方法。
但上面的ServletProcessor1中有一個問題,我們傳request和response對象的時候,可以將Servletrequest對象往下轉成Request對象,這樣,在處理servlet的類中也可以處理訪問靜態資源的方法,會造成一定的危害。因此,我們可以創建額外的RequestFacade對象和ResponseFacade對象,然后繼承servlet來處理servlet。

package test.servlet2; import java.io.IOException; import java.io.BufferedReader; import java.io.UnsupportedEncodingException; import java.util.Enumeration; import java.util.Locale; import java.util.Map; import javax.servlet.RequestDispatcher; import javax.servlet.ServletInputStream; import javax.servlet.ServletRequest; public class RequestFacade implements ServletRequest { private ServletRequest request = null; public RequestFacade(Request request) { this.request = request; } /* implementation of the ServletRequest*/ public Object getAttribute(String attribute) { return request.getAttribute(attribute); } public Enumeration getAttributeNames() { return request.getAttributeNames(); } public String getRealPath(String path) { return request.getRealPath(path); } public int getRemotePort() { return request.getRemotePort(); } public String getLocalName() { return request.getLocalName(); } public String getLocalAddr() { return request.getLocalAddr(); } public int getLocalPort() { return request.getLocalPort(); } public RequestDispatcher getRequestDispatcher(String path) { return request.getRequestDispatcher(path); } public boolean isSecure() { return request.isSecure(); } public String getCharacterEncoding() { return request.getCharacterEncoding(); } public int getContentLength() { return request.getContentLength(); } public String getContentType() { return request.getContentType(); } public ServletInputStream getInputStream() throws IOException { return request.getInputStream(); } public Locale getLocale() { return request.getLocale(); } public Enumeration getLocales() { return request.getLocales(); } public String getParameter(String name) { return request.getParameter(name); } public Map getParameterMap() { return request.getParameterMap(); } public Enumeration getParameterNames() { return request.getParameterNames(); } public String[] getParameterValues(String parameter) { return request.getParameterValues(parameter); } public String getProtocol() { return request.getProtocol(); } public BufferedReader getReader() throws IOException { return request.getReader(); } public String getRemoteAddr() { return request.getRemoteAddr(); } public String getRemoteHost() { return request.getRemoteHost(); } public String getScheme() { return request.getScheme(); } public String getServerName() { return request.getServerName(); } public int getServerPort() { return request.getServerPort(); } public void removeAttribute(String attribute) { request.removeAttribute(attribute); } public void setAttribute(String key, Object value) { request.setAttribute(key, value); } public void setCharacterEncoding(String encoding) throws UnsupportedEncodingException { request.setCharacterEncoding(encoding); } }

package test.servlet2; import java.io.IOException; import java.io.PrintWriter; import java.util.Locale; import javax.servlet.ServletOutputStream; import javax.servlet.ServletResponse; public class ResponseFacade implements ServletResponse { private ServletResponse response; public ResponseFacade(Response response) { this.response = response; } public void flushBuffer() throws IOException { response.flushBuffer(); } public int getBufferSize() { return response.getBufferSize(); } public String getCharacterEncoding() { return response.getCharacterEncoding(); } public String getContentType() { return response.getContentType(); } public Locale getLocale() { return response.getLocale(); } public ServletOutputStream getOutputStream() throws IOException { return response.getOutputStream(); } public PrintWriter getWriter() throws IOException { return response.getWriter(); } public void setCharacterEncoding(String s) { } public boolean isCommitted() { return response.isCommitted(); } public void reset() { response.reset(); } public void resetBuffer() { response.resetBuffer(); } public void setBufferSize(int size) { response.setBufferSize(size); } public void setContentLength(int length) { response.setContentLength(length); } public void setContentType(String type) { response.setContentType(type); } public void setLocale(Locale locale) { response.setLocale(locale); } }

package test.servlet2; import java.io.File; import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; import java.net.URLStreamHandler; import javax.servlet.Servlet; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; public class ServletProcessor2 { public void process(Request request, Response response) { String uri = request.getUri(); String servletName = uri.substring(uri.lastIndexOf("/") + 1); URLClassLoader loader = null; try { // create a URLClassLoader URL[] urls = new URL[1]; URLStreamHandler streamHandler = null; File classPath = new File(Constants.WEB_ROOT); // the forming of repository is taken from the createClassLoader method in // org.apache.catalina.startup.ClassLoaderFactory String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)) .toString(); // the code for forming the URL is taken from the addRepository method in // org.apache.catalina.loader.StandardClassLoader class. urls[0] = new URL(null, repository, streamHandler); loader = new URLClassLoader(urls); } catch (IOException e) { System.out.println(e.toString()); } Class myClass = null; try { myClass = loader.loadClass(servletName); } catch (ClassNotFoundException e) { System.out.println(e.toString()); } Servlet servlet = null; RequestFacade requestFacade = new RequestFacade(request); ResponseFacade responseFacade = new ResponseFacade(response); try { servlet = (Servlet) myClass.newInstance(); servlet.service((ServletRequest) requestFacade, (ServletResponse) responseFacade); } catch (Exception e) { System.out.println(e.toString()); } catch (Throwable e) { System.out.println(e.toString()); } } }
到目前為止,可以總結一下:一個簡單的Web服務器,需要有申請靜態資源和動態資源的功能,我們在瀏覽器上發出請求,然后服務器響應。服務器響應主要用到ServerSocket,通過接收intput,響應output來處理數據。我們通過封裝request和response對象,將input和out封裝起來,最后響應回去。對於request來說,它接收到了瀏覽器傳過來的input字節流,需要將字節流轉換為字節數組,然后轉換為字符串。得到請求的地址。然后我們根據這個地址字符串來判斷該請求是請求靜態資源還是servlet,如果請求靜態資源,那么直接調用response的發送資源函數即可,該函數將通過地址來尋找文件,找到后將其轉換為out字節流傳回瀏覽器,瀏覽器會自行渲染。如果請求的是servlet,那么服務器會得到該servlet的地址以及名字,然后創建一個類加載器,根據地址來尋找該servlet的字節碼文件,找到后加載為一個類,然后根據newInstance方法來創建一個servlet對象,該servlet對象的類型是我們自己定義的servlet類的對象,然后調用servlet對象的service方法來執行。
連接器:
三個模塊:
startup模塊,Bootstrap,用來啟動應用。
connector模塊:連接器和其支撐類(HttpConnector和HttpProcessor)、指代HTTP請求的類(HttpRequest)和它的輔助類、指代HTTP響應的類(HttpResponse)和它的輔助類、Facade類(HttpRequestFacade和HttpResponseFacade)、
Constant類
core模塊:ServletProcessor和StaticResourceProcessor。
將HttpServer類分為HttpConnector和HttpProcessor。其中HttpConnector類用來等待http請求,而HttpProcessor用來創建請求和響應對象。
而且Http請求對象實現了javax.servlet.http.HttpServletRequest接口。一個HttpRequest對象會被轉換為HttpServletRequest實例,然后傳遞給被調用的servlet的service方法。HttpRequest對象,包括URI,查詢字符串,參數,cookies和其他
的頭部等等。連接器不會先去解析request的所有字段,而是等servlet需要的時候才會去解析。
啟動應用程序:Bootstrap類。其實就是開啟連接器,因為連接器等待http請求。
package test.servlet3.startup; import test.servlet3.connector.http.HttpConnector; public final class Bootstrap { public static void main(String[] args) { HttpConnector connector = new HttpConnector(); connector.start(); } }
這個啟動類很簡單,HttpConnector繼承了Runnable接口,然后自己開了一個線程,啟動類開啟此線程即可。
接下來看 HttpConnector類,此類負責等待客戶端請求,拿到socket。

package test.servlet3.connector.http; import java.io.IOException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; public class HttpConnector implements Runnable { boolean stopped; private String scheme = "http"; public void run() { ServerSocket serverSocket = null; int port = 8080; try { serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1")); } catch (IOException e) { e.printStackTrace(); System.exit(1); } while (!stopped) { // Accept the next incoming connection from the server socket Socket socket = null; try { socket = serverSocket.accept(); } catch (Exception e) { continue; } //這里實際上就是將服務器接收到的套接字交給配套的函數,讓其去創建request和response對象。 HttpProcessor processor = new HttpProcessor(this); processor.process(socket); } } public void start() { Thread thread = new Thread(this); thread.start(); } }
其中重要的是
HttpProcessor processor = new HttpProcessor(this); processor.process(socket);
這個一個處理類,將socket傳過去去處理,主要是創建Request對象和Response對象。

package test.servlet3.connector.http; import org.apache.catalina.util.RequestUtil; import org.apache.catalina.util.StringManager; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import java.io.IOException; import java.io.OutputStream; import java.net.Socket; /* this class used to be called HttpServer */ public class HttpProcessor { public HttpProcessor(HttpConnector connector) { this.connector = connector; } /** * The HttpConnector with which this processor is associated. */ private HttpConnector connector = null; private HttpRequest request; private HttpRequestLine requestLine = new HttpRequestLine(); //請求行使用默認 private HttpResponse response; protected String method = null; protected String queryString = null; /** * The string manager for this package. */ protected StringManager sm = StringManager.getManager("test.servlet3.connector.http"); public void process(Socket socket) { SocketInputStream input = null; //自己定義了一個繼承InputStream的流處理類 OutputStream output = null; try { input = new SocketInputStream(socket.getInputStream(), 2048); //初始化input,得到請求傳過來的數據。 output = socket.getOutputStream();//為響應做准備 // create HttpRequest object and parse request = new HttpRequest(input); //根據input來創建一個request對象。 // create HttpResponse object response = new HttpResponse(output);//根據output來創建一個response對象。 //設置response對象。 response.setRequest(request); //將請求傳入 response.setHeader("Server", "Pyrmont Servlet Container"); //設置響應頭 //響應里面很多都留空。 parseRequest(input, output); //解析請求 parseHeaders(input); //check if this is a request for a servlet or a static resource //a request for a servlet begins with "/servlet/" if (request.getRequestURI().startsWith("/servlet/")) { ServletProcessor processor = new ServletProcessor(); processor.process(request, response); } else { StaticResourceProcessor processor = new StaticResourceProcessor(); processor.process(request, response); } // Close the socket socket.close(); // no shutdown for this application } catch (Exception e) { e.printStackTrace(); } } /** * This method is the simplified version of the similar method in * org.apache.catalina.connector.http.HttpProcessor. * However, this method only parses some "easy" headers, such as * "cookie", "content-length", and "content-type", and ignore other headers. * @param input The input stream connected to our socket * * @exception IOException if an input/output error occurs * @exception ServletException if a parsing error occurs */ private void parseHeaders(SocketInputStream input) throws IOException, ServletException { while (true) { HttpHeader header = new HttpHeader();; // Read the next header input.readHeader(header); if (header.nameEnd == 0) { if (header.valueEnd == 0) { return; } else { throw new ServletException (sm.getString("httpProcessor.parseHeaders.colon")); } } String name = new String(header.name, 0, header.nameEnd); String value = new String(header.value, 0, header.valueEnd); request.addHeader(name, value); // do something for some headers, ignore others. if (name.equals("cookie")) { Cookie cookies[] = RequestUtil.parseCookieHeader(value); for (int i = 0; i < cookies.length; i++) { if (cookies[i].getName().equals("jsessionid")) { // Override anything requested in the URL if (!request.isRequestedSessionIdFromCookie()) { // Accept only the first session id cookie request.setRequestedSessionId(cookies[i].getValue()); request.setRequestedSessionCookie(true); request.setRequestedSessionURL(false); } } request.addCookie(cookies[i]); } } else if (name.equals("content-length")) { int n = -1; try { n = Integer.parseInt(value); } catch (Exception e) { throw new ServletException(sm.getString("httpProcessor.parseHeaders.contentLength")); } request.setContentLength(n); } else if (name.equals("content-type")) { request.setContentType(value); } } //end while } //解析請求 private void parseRequest(SocketInputStream input, OutputStream output) throws IOException, ServletException { // Parse the incoming request line input.readRequestLine(requestLine); //閱讀請求行 String method = new String(requestLine.method, 0, requestLine.methodEnd); //得到請求行的方法,get還是post String uri = null; String protocol = new String(requestLine.protocol, 0, requestLine.protocolEnd);//得到請求行的協議。 // Validate the incoming request line if (method.length() < 1) { throw new ServletException("Missing HTTP request method"); } else if (requestLine.uriEnd < 1) { throw new ServletException("Missing HTTP request URI"); } // Parse any query parameters out of the request URI int question = requestLine.indexOf("?"); if (question >= 0) { request.setQueryString(new String(requestLine.uri, question + 1, requestLine.uriEnd - question - 1)); //給request設置查詢字符串。 uri = new String(requestLine.uri, 0, question); //得到uri } else { request.setQueryString(null); uri = new String(requestLine.uri, 0, requestLine.uriEnd); } // Checking for an absolute URI (with the HTTP protocol) 查看是不是一個絕對路徑 if (!uri.startsWith("/")) { int pos = uri.indexOf("://"); // Parsing out protocol and host name if (pos != -1) { pos = uri.indexOf('/', pos + 3); if (pos == -1) { uri = ""; } else { uri = uri.substring(pos); } } } // Parse any requested session ID out of the request URI 從uri中解析會話id String match = ";jsessionid="; int semicolon = uri.indexOf(match); if (semicolon >= 0) { String rest = uri.substring(semicolon + match.length()); int semicolon2 = rest.indexOf(';'); if (semicolon2 >= 0) { request.setRequestedSessionId(rest.substring(0, semicolon2)); rest = rest.substring(semicolon2); } else { request.setRequestedSessionId(rest); rest = ""; } request.setRequestedSessionURL(true); uri = uri.substring(0, semicolon) + rest; } else { request.setRequestedSessionId(null); request.setRequestedSessionURL(false); //最終給request設置了一個會話id } // Normalize URI (using String operations at the moment) String normalizedUri = normalize(uri); // Set the corresponding request properties ((HttpRequest) request).setMethod(method); request.setProtocol(protocol); //給request設置method和協議。 if (normalizedUri != null) { ((HttpRequest) request).setRequestURI(normalizedUri); } else { ((HttpRequest) request).setRequestURI(uri);//給request設置uri } if (normalizedUri == null) { throw new ServletException("Invalid URI: " + uri + "'"); } } /** * Return a context-relative path, beginning with a "/", that represents * the canonical version of the specified path after ".." and "." elements * are resolved out. If the specified path attempts to go outside the * boundaries of the current context (i.e. too many ".." path elements * are present), return <code>null</code> instead. * * @param path Path to be normalized */ protected String normalize(String path) { if (path == null) return null; // Create a place for the normalized path String normalized = path; // Normalize "/%7E" and "/%7e" at the beginning to "/~" if (normalized.startsWith("/%7E") || normalized.startsWith("/%7e")) normalized = "/~" + normalized.substring(4); // Prevent encoding '%', '/', '.' and '\', which are special reserved // characters if ((normalized.indexOf("%25") >= 0) || (normalized.indexOf("%2F") >= 0) || (normalized.indexOf("%2E") >= 0) || (normalized.indexOf("%5C") >= 0) || (normalized.indexOf("%2f") >= 0) || (normalized.indexOf("%2e") >= 0) || (normalized.indexOf("%5c") >= 0)) { return null; } if (normalized.equals("/.")) return "/"; // Normalize the slashes and add leading slash if necessary if (normalized.indexOf('\\') >= 0) normalized = normalized.replace('\\', '/'); if (!normalized.startsWith("/")) normalized = "/" + normalized; // Resolve occurrences of "//" in the normalized path while (true) { int index = normalized.indexOf("//"); if (index < 0) break; normalized = normalized.substring(0, index) + normalized.substring(index + 1); } // Resolve occurrences of "/./" in the normalized path while (true) { int index = normalized.indexOf("/./"); if (index < 0) break; normalized = normalized.substring(0, index) + normalized.substring(index + 2); } // Resolve occurrences of "/../" in the normalized path while (true) { int index = normalized.indexOf("/../"); if (index < 0) break; if (index == 0) return (null); // Trying to go outside our context int index2 = normalized.lastIndexOf('/', index - 1); normalized = normalized.substring(0, index2) + normalized.substring(index + 3); } // Declare occurrences of "/..." (three or more dots) to be invalid // (on some Windows platforms this walks the directory tree!!!) if (normalized.indexOf("/...") >= 0) return (null); // Return the normalized path that we have completed return (normalized); } }
這個類比較復雜,主要就是創建Request對象和Response對象,在實現上,使用了process方法。可以依次解釋:
首先,這個類需要字段,毋庸置疑,有Request對象和Response對象,其他不是很重要。
可以先來看Request對象:

package test.servlet3.connector.http; import org.apache.catalina.util.Enumerator; import org.apache.catalina.util.ParameterMap; import org.apache.catalina.util.RequestUtil; import javax.servlet.RequestDispatcher; import javax.servlet.ServletInputStream; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.io.*; import java.net.InetAddress; import java.net.Socket; import java.security.Principal; import java.text.SimpleDateFormat; import java.util.*; public class HttpRequest implements HttpServletRequest { private String contentType; private int contentLength; private InetAddress inetAddress; private InputStream input; private String method; private String protocol; private String queryString; private String requestURI; private String serverName; private int serverPort; private Socket socket; private boolean requestedSessionCookie; private String requestedSessionId; private boolean requestedSessionURL; /** * The request attributes for this request. */ protected HashMap attributes = new HashMap(); /** * The authorization credentials sent with this Request. */ protected String authorization = null; /** * The context path for this request. */ protected String contextPath = ""; /** * The set of cookies associated with this Request. */ protected ArrayList cookies = new ArrayList(); /** * An empty collection to use for returning empty Enumerations. Do not * add any elements to this collection! */ protected static ArrayList empty = new ArrayList(); /** * The set of SimpleDateFormat formats to use in getDateHeader(). */ protected SimpleDateFormat formats[] = { new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US), new SimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US), new SimpleDateFormat("EEE MMMM d HH:mm:ss yyyy", Locale.US) }; /** * The HTTP headers associated with this Request, keyed by name. The * values are ArrayLists of the corresponding header values. */ protected HashMap headers = new HashMap(); /** * The parsed parameters for this request. This is populated only if * parameter information is requested via one of the * <code>getParameter()</code> family of method calls. The key is the * parameter name, while the value is a String array of values for this * parameter. * <p> * <strong>IMPLEMENTATION NOTE</strong> - Once the parameters for a * particular request are parsed and stored here, they are not modified. * Therefore, application level access to the parameters need not be * synchronized. */ protected ParameterMap parameters = null; /** * Have the parameters for this request been parsed yet? */ protected boolean parsed = false; protected String pathInfo = null; /** * The reader that has been returned by <code>getReader</code>, if any. */ protected BufferedReader reader = null; /** * The ServletInputStream that has been returned by * <code>getInputStream()</code>, if any. */ protected ServletInputStream stream = null; public HttpRequest(InputStream input) { this.input = input; } public void addHeader(String name, String value) { name = name.toLowerCase(); synchronized (headers) { ArrayList values = (ArrayList) headers.get(name); if (values == null) { values = new ArrayList(); headers.put(name, values); } values.add(value); } } /** * Parse the parameters of this request, if it has not already occurred. * If parameters are present in both the query string and the request * content, they are merged. */ protected void parseParameters() { if (parsed) return; ParameterMap results = parameters; if (results == null) results = new ParameterMap(); results.setLocked(false); String encoding = getCharacterEncoding(); if (encoding == null) encoding = "ISO-8859-1"; // Parse any parameters specified in the query string String queryString = getQueryString(); try { RequestUtil.parseParameters(results, queryString, encoding); } catch (UnsupportedEncodingException e) { ; } // Parse any parameters specified in the input stream String contentType = getContentType(); if (contentType == null) contentType = ""; int semicolon = contentType.indexOf(';'); if (semicolon >= 0) { contentType = contentType.substring(0, semicolon).trim(); } else { contentType = contentType.trim(); } if ("POST".equals(getMethod()) && (getContentLength() > 0) && "application/x-www-form-urlencoded".equals(contentType)) { try { int max = getContentLength(); int len = 0; byte buf[] = new byte[getContentLength()]; ServletInputStream is = getInputStream(); while (len < max) { int next = is.read(buf, len, max - len); if (next < 0 ) { break; } len += next; } is.close(); if (len < max) { throw new RuntimeException("Content length mismatch"); } RequestUtil.parseParameters(results, buf, encoding); } catch (UnsupportedEncodingException ue) { ; } catch (IOException e) { throw new RuntimeException("Content read fail"); } } // Store the final results results.setLocked(true); parsed = true; parameters = results; } public void addCookie(Cookie cookie) { synchronized (cookies) { cookies.add(cookie); } } /** * Create and return a ServletInputStream to read the content * associated with this Request. The default implementation creates an * instance of RequestStream associated with this request, but this can * be overridden if necessary. * * @exception IOException if an input/output error occurs */ public ServletInputStream createInputStream() throws IOException { return (new RequestStream(this)); } public InputStream getStream() { return input; } public void setContentLength(int length) { this.contentLength = length; } public void setContentType(String type) { this.contentType = type; } public void setInet(InetAddress inetAddress) { this.inetAddress = inetAddress; } public void setContextPath(String path) { if (path == null) this.contextPath = ""; else this.contextPath = path; } public void setMethod(String method) { this.method = method; } public void setPathInfo(String path) { this.pathInfo = path; } public void setProtocol(String protocol) { this.protocol = protocol; } public void setQueryString(String queryString) { this.queryString = queryString; } public void setRequestURI(String requestURI) { this.requestURI = requestURI; } /** * Set the name of the server (virtual host) to process this request. * * @param name The server name */ public void setServerName(String name) { this.serverName = name; } /** * Set the port number of the server to process this request. * * @param port The server port */ public void setServerPort(int port) { this.serverPort = port; } public void setSocket(Socket socket) { this.socket = socket; } /** * Set a flag indicating whether or not the requested session ID for this * request came in through a cookie. This is normally called by the * HTTP Connector, when it parses the request headers. * * @param flag The new flag */ public void setRequestedSessionCookie(boolean flag) { this.requestedSessionCookie = flag; } public void setRequestedSessionId(String requestedSessionId) { this.requestedSessionId = requestedSessionId; } public void setRequestedSessionURL(boolean flag) { requestedSessionURL = flag; } /* implementation of the HttpServletRequest*/ public Object getAttribute(String name) { synchronized (attributes) { return (attributes.get(name)); } } public Enumeration getAttributeNames() { synchronized (attributes) { return (new Enumerator(attributes.keySet())); } } public String getAuthType() { return null; } public String getCharacterEncoding() { return null; } public int getContentLength() { return contentLength ; } public String getContentType() { return contentType; } public String getContextPath() { return contextPath; } public Cookie[] getCookies() { synchronized (cookies) { if (cookies.size() < 1) return (null); Cookie results[] = new Cookie[cookies.size()]; return ((Cookie[]) cookies.toArray(results)); } } public long getDateHeader(String name) { String value = getHeader(name); if (value == null) return (-1L); // Work around a bug in SimpleDateFormat in pre-JDK1.2b4 // (Bug Parade bug #4106807) value += " "; // Attempt to convert the date header in a variety of formats for (int i = 0; i < formats.length; i++) { try { Date date = formats[i].parse(value); return (date.getTime()); } catch (ParseException e) { ; } } throw new IllegalArgumentException(value); } public String getHeader(String name) { name = name.toLowerCase(); synchronized (headers) { ArrayList values = (ArrayList) headers.get(name); if (values != null) return ((String) values.get(0)); else return null; } } public Enumeration getHeaderNames() { synchronized (headers) { return (new Enumerator(headers.keySet())); } } public Enumeration getHeaders(String name) { name = name.toLowerCase(); synchronized (headers) { ArrayList values = (ArrayList) headers.get(name); if (values != null) return (new Enumerator(values)); else return (new Enumerator(empty)); } } public ServletInputStream getInputStream() throws IOException { if (reader != null) throw new IllegalStateException("getInputStream has been called"); if (stream == null) stream = createInputStream(); return (stream); } public int getIntHeader(String name) { String value = getHeader(name); if (value == null) return (-1); else return (Integer.parseInt(value)); } public Locale getLocale() { return null; } public Enumeration getLocales() { return null; } public String getMethod() { return method; } public String getParameter(String name) { parseParameters(); String values[] = (String[]) parameters.get(name); if (values != null) return (values[0]); else return (null); } public Map getParameterMap() { parseParameters(); return (this.parameters); } public Enumeration getParameterNames() { parseParameters(); return (new Enumerator(parameters.keySet())); } public String[] getParameterValues(String name) { parseParameters(); String values[] = (String[]) parameters.get(name); if (values != null) return (values); else return null; } public String getPathInfo() { return pathInfo; } public String getPathTranslated() { return null; } public String getProtocol() { return protocol; } public String getQueryString() { return queryString; } public BufferedReader getReader() throws IOException { if (stream != null) throw new IllegalStateException("getInputStream has been called."); if (reader == null) { String encoding = getCharacterEncoding(); if (encoding == null) encoding = "ISO-8859-1"; InputStreamReader isr = new InputStreamReader(createInputStream(), encoding); reader = new BufferedReader(isr); } return (reader); } public String getRealPath(String path) { return null; } public int getRemotePort() { return 0; } public String getLocalName() { return null; } public String getLocalAddr() { return null; } public int getLocalPort() { return 0; } public String getRemoteAddr() { return null; } public String getRemoteHost() { return null; } public String getRemoteUser() { return null; } public RequestDispatcher getRequestDispatcher(String path) { return null; } public String getScheme() { return null; } public String getServerName() { return null; } public int getServerPort() { return 0; } public String getRequestedSessionId() { return null; } public String getRequestURI() { return requestURI; } public StringBuffer getRequestURL() { return null; } public HttpSession getSession() { return null; } public HttpSession getSession(boolean create) { return null; } public String getServletPath() { return null; } public Principal getUserPrincipal() { return null; } public boolean isRequestedSessionIdFromCookie() { return false; } public boolean isRequestedSessionIdFromUrl() { return isRequestedSessionIdFromURL(); } public boolean isRequestedSessionIdFromURL() { return false; } public boolean isRequestedSessionIdValid() { return false; } public boolean isSecure() { return false; } public boolean isUserInRole(String role) { return false; } public void removeAttribute(String attribute) { } public void setAttribute(String key, Object value) { } /** * Set the authorization credentials sent with this request. * * @param authorization The new authorization credentials */ public void setAuthorization(String authorization) { this.authorization = authorization; } public void setCharacterEncoding(String encoding) throws UnsupportedEncodingException { } }
這個類也比較復雜,主要來說,它存儲了被解析好的各種字段,
private String contentType; private int contentLength; private InetAddress inetAddress; private InputStream input; private String method; private String protocol; private String queryString; private String requestURI;
通過get/set函數,我們能得到這些字段。
回到HttpProcessor類,繼續往下看,此類是如何創建並封裝Request類和Response類的。
response.setRequest(request); //將請求傳入 response.setHeader("Server", "Pyrmont Servlet Container"); //設置響應頭 //響應里面很多都留空。 parseRequest(input, output); //解析請求 parseHeaders(input);
關鍵是parseRequest方法和parseHeaders方法。
parseRequest:作用是解析請求,然后封裝Request。
封裝好request和response對象之后,可以去申請靜態資源和servlet資源。此處不講了。
Tomcat的默認連接器
一個Tomcat連接器必須滿以下條件:
Connector接口:重要的三個方法,getContainer,setContainer, createRequest和createResponse。
HttpConnector類:是Connector接口的一個實現。在tomcat后面版本中此實現已經不存在了。
在默認連接器中,HttpConnector擁有一個HttpProcessor對象池,每個HttpProcessor實例擁有一個獨立線程。因此,HttpConnector可以同時處理多個HTTP請求。HttpConnector維護一個HttpProcessor的實例池,從而避免每次創建HttpProcessor實例。這些HttpProcessor實例是存放在一個叫processors的java.io.Stack中:
private Stack processors = new Stack();
每個HttpProcessor實例負責解析HTTP請求行和頭部,並填充請求對象。因此,每個實例關聯着一個請求對象和響應對象。類HttpProcessor 的構造方法包括了類HttpConnector 的createRequest和createResponse方法的調用。
HttpConnector類 為Http請求服務,
while (!stopped) { Socket socket = null; try { socket = serverSocket.accept(); ...
對於每一個前來的http請求,會通過createProcessor方法來獲取一個HttpProcessor實例。這里大多數情況HttpProcessor實例並不是創建一個新的實例,而是從池子里面獲取。如果已經達到池子的最大數量,沒有實例可取,那么套接字就會簡單關閉,前來的請求不會被處理。
if (processor == null) { try { log(sm.getString("httpConnector.noProcessor")); socket.close(); } ... continue;
而得到的HttpProcessor實例實例用於讀取套接字的輸入流,解析http請求的工作。
processor.assign(socket);
assign方法不會等到HttpProcessor完成解析工作,而是必須馬上返回,以便下一個前來的HTTP請求可以被處理。每個HttpProcessor實例有自己的線程用於解析。
HttpProcessor類讓assign方法異步化,HttpProcessor 類實現了java.lang.Runnable 接口,並且每個HttpProcessor 實例運行在稱作處理器線程(processor thread)的自身線程上,對HttpConnector 創建的每個HttpProcessor 實例,它的start 方法將被調用,有效的啟動了HttpProcessor實例的處理線程。
public void run() { // Process requests until we receive a shutdown signal while (!stopped) { // Wait for the next socket to be assigned Socket socket = await(); if (socket == null) continue; // Process the request from this socket try { process(socket); } catch (Throwable t) { log("process.invoke", t); } // Finish up this request connector.recycle(this); } // Tell threadStop() we have shut ourselves down successfully synchronized (threadSync) { threadSync.notifyAll(); } }
run方法中的while循環,先去獲取一個套接字,處理它,然后調用連接器的recycle方法把當前的HttpProcessor實例退回棧中,
void recycle(HttpProcessor processor) { processors.push(processor); }
此處,run方法中while循環在await方法中結束。await方法持有處理線程的控制流,直到從HttpConnector中獲取到一個新的套接字。而await方法和assign方法運行在不同的線程上。assign方法從HttpConnector的run方法中調用。我們就說這個線程是HttpConnector實例的run方法運行的處理線程。assign方法是如何通知已經被調用的await方法的?就是通過一個布爾變量available並且使用java.lang.Object的wait和notifyAll方法。
請求對象:
默認連接器哩變得HTTP請求對象指代org.apache.catalina.Request接口。這個接口被類RequestBase直接實現了,也是HttpRequest的父接口。最終的實現是繼承於HttpRequest的HttpRequestImpl。
處理請求:在HttpProcessor類的run方法中調用的。process方法會做下面這些工作:
容器:容器是一個處理用戶servlet請求並返回對象給web用戶的模塊,org.apache.catalina.Container接口定義了容器的形式。有四種容器:Engine(引擎), Host(主機), Context(上下文), 和Wrapper(包裝器)
容器接口:org.apache.catalina.Container。
容器分類:
Engine:表示整個Catalina的servlet引擎
Host:表示一個擁有數個上下文的虛擬主機
Context:表示一個Web應用,一個context包含一個或多個wrapper
Wrapper:表示一個獨立的servlet
每一個概念之上是用org.apache.catalina包來表示的。Engine、Host、Context和Wrapper接口都實現了Container即可。它們的標准實現是StandardEngine,StandardHost, StandardContext, and StandardWrapper,它們都是org.apache.catalina.core包的一部分。
所有的類都擴展自抽象類ContainerBase。
一個容器可以有一個或多個低層次上的子容器。例如,一個Context有一個或多
個wrapper,而wrapper作為容器層次中的最底層,不能包含子容器。一個容器添加到另一容器中可以使用在Container接口中定義的addChild()方法。
public void addChild(Container child);
刪除一個容器可以使用Container接口中定義的removeChild()方法。
public void removeChild(Container child);
另外容器接口支持子接口查找和獲得所有子接口集合的方法findChild和findChildren方法。
public Container findChild(String name); public Container[] findChildren();
一個容器還包含一系列的部分如Lodder、Loggee、Manager、Realm和Resources。
connector調用容器的Invoke方法后做的工作
Pipelining Tasks(流水線任務):一個pipeline包含了改容器要喚醒的所有任務。每一個閥門表示了一個特定的任務。一個容器的流水線有一個基本的閥門,但是你可以添加任意你想要添加的閥門。閥門的數目定義為添加的閥門的個數(不包括基本閥門)。有趣的是,閥門可以痛苦編輯Tomcat的配置文件server.xml來動態的添加。
一個流水線就像一個過濾鏈,每一個閥門像一個過濾器。跟過濾器一樣,一個閥門可以操作傳遞給它的request和response方法。讓一個閥門完成了處理,則進一步處理流水線中的下一個閥門,基本閥門總是在最后才被調用。
當連接器調用容器的invoke()方法后,容器中要執行的任務並沒有硬編碼在invoke()方法中,而是容器會調用pipeline中的invoke()方法。管道必須保證添加到其中的所有閥及基礎閥都必須被調用一次。通過創建一個ValveContext接口實例來實現的。其中ValveContext重要的方法是invokeNext(),這樣管道會遍歷所有的閥。
閥是實現了Valve接口的實例。用來處理收到的請求,該接口有兩個方法,invoke()方法和getInfo()方法。getInfo方法返回閥的實現信息。
Wrapper接口:
Wrapper級容器的servlet容器表示獨立servlet定義。其接口的實現類要負責管理其基礎servlet類的servlet生命周期。即調用servlet的init()、service()、destroy()等方法。由於Wrapper已經是最低級的servlet容器,因此不能往里添加子容器。若Wrapper的addChild()方法被調用,則拋異常。
Wrapper接口中比較重要的方法是load()和allocate()方法。allocate()方法會分配一個已經初始化的servlet實例。load方法加載並初始化servlet類。
Context接口:
Context接口的實例表示一個Web應用程序。一個Context實例可以有一個或者多個Wrapper實例作為其子容器。
Context接口中比較重要的方法是addWrapper()和createWrapper()。
Wrapper應用程序:
展示如何編寫一個最小的servlet容器。其核心是實現了Wrapper接口的SimpleWrapper類。SimpleWrapper類包含一個Pipeline實例(SimplePipeline),並使用一個Loader實例(SimpleLoader)來載入servlet類。Pipeline實例包含了一個基礎閥和兩個額外的閥。
SimpleLoader類:在servlet容器中載入相關servlet類的工作由Loader接口的實例完成。它知道servlet類的位置,通過調用其getClassLoader方法可以返回一個ClassLoader實例。可用來搜索servlet類的位置。
public SimpleLoader() { try { URL[] urls = new URL[1]; URLStreamHandler streamHandler = null; File classPath = new File(WEB_ROOT); String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString() ; urls[0] = new URL(null, repository, streamHandler); classLoader = new URLClassLoader(urls); } catch (IOException e) { System.out.println(e.toString() ); } }
構造函數中的代碼會初始化一個類加載器,其中的變量container指向了與該servlet容器相關聯的類加載器。
SimplePipeline類:其中最重要的方法invoke()方法。該方法包含一個內部類SimplePipelineValveContext,實現了ValveContext接口。其中一個重要的方法是:invokeNext()。遍歷整個pipeline。
SimpleWrapper類:實現了Wrapper接口,提供了allocate方法和load方法的實現。聲明了兩個變量:loader和parent。load而變量指明了載入servlet類要使用的載入器,parent指明了該Wrapper實例的父容器。
private Loader loader; protected Container parent = null;
public Loader getLoader() { if (loader != null) return (loader); if (parent != null) return (parent.getLoader()); return (null); }
SimpleWrapper類有一個Pipeline實例,並為該Pipeline實例設置了基礎閥,
public SimpleWrapper() { pipeline.setBasic(new SimpleWrapperValve()); }
SimpleWrapperValve類:是一個基礎閥,專門用於處理對SimpleWrapper類的請求。實現了Valve, Contained接口。其中最主要的方法是invoke()方法。
public void invoke(Request request, Response response, ValveContext valveContext) throws IOException, ServletException { SimpleWrapper wrapper = (SimpleWrapper) getContainer(); ServletRequest sreq = request.getRequest(); ServletResponse sres = response.getResponse(); Servlet servlet = null; HttpServletRequest hreq = null; if (sreq instanceof HttpServletRequest) hreq = (HttpServletRequest) sreq; HttpServletResponse hres = null; if (sres instanceof HttpServletResponse) hres = (HttpServletResponse) sres; // Allocate a servlet instance to process this request try { servlet = wrapper.allocate(); if (hres!=null && hreq!=null) { servlet.service(hreq, hres); } else { servlet.service(sreq, sres); } } catch (ServletException e) { } }
該方法直接調用wrapper實例的allocate(),獲取servlet,然后直接調用service。
另外兩個閥是ClientIPLoggerValve和HeaderLoggerValve。
其中ClientIPLoggerValve類所表示的閥用來將客戶端的IP地址輸出到控制台上。其中也是調用invoke()。invoke()方法中使用invokeNext()方法來調用管道的下一個閥。
其中HeaderLoggerValve類把請求頭信息輸出到控制台。其中也是調用invoke()。invoke()方法中使用invokeNext()方法來調用管道的下一個閥。
HttpConnector connector = new HttpConnector(); Wrapper wrapper = new SimpleWrapper(); wrapper.setServletClass("ModernServlet"); Loader loader = new SimpleLoader(); Valve valve1 = new HeaderLoggerValve(); Valve valve2 = new ClientIPLoggerValve(); wrapper.setLoader(loader); ((Pipeline) wrapper).addValve(valve1); ((Pipeline) wrapper).addValve(valve2); connector.setContainer(wrapper); try { connector.initialize(); connector.start(); // make the application wait until we press a key. System.in.read(); } catch (Exception e) { e.printStackTrace(); }
整體來介紹一下該應用:
首先是Bootstrap1類,在該類中創建一個Wrapper實例(SimpleWrapper())。將字符串"Modernservlet"賦值給SimpleWrapper類的setservletClass()方法。告訴Wrapper實例要載入的servlet類的名稱。
然后創建了一個載入器和兩個閥。並將載入器設置到Wrapper實例中。再將創建的兩個閥添加到Wrapper實例的管道中。
最后將Wrapper實例設置為連接器的servlet容器。初始化並啟動連接器。
Context應用程序:對於大部分web應用來說,是需要多個servlet合作的,這類應用程序需要的是Context容器。
接下來,展示包含了兩個Wrapper實例的Context實例來構建Web應用程序,當應用程序中有多個Wrapper實例的時候,需要使用一個映射器,映射器是組件,幫助servlet容器選擇一個子容器來處理某個指定的請求。
SimpleContextMapper類是實現了Mapper接口的映射器。servlet容器可以使用多個映射器來支持不同的協議。在此處,一個映射器支持一個請求協議。例如servlet容器中可以使用一個映射器對HTTP協議的請求進行映射,使用另一個對HTTPS協議的請求進行映射。
Mapper接口的getContainer()方法返回與該映射器相關聯的servlet容器的實例。而setContainer()方法則用來將某個servlet容器與該映射器相關聯。getProtocol()方法返回該映射器負責處理的協議。setProtocol()方法可以指定該映射器負責處理哪種協議。map()方法返回要處理某個特定請求的子容器的實例。
Context容器是一個SimpleContext類的實例。SimpleContext類使用SimpleContextMapper類的實例作為其映射器,將SimpleContextValve的實例作為其基礎閥。Context容器中額外添加兩個閥。HeaderLoggerValve和ClientIPLoggerValve。並包含兩個Wrapper實例作為其子容器。
HttpConnector connector = new HttpConnector(); Wrapper wrapper1 = new SimpleWrapper(); wrapper1.setName("Primitive"); wrapper1.setServletClass("PrimitiveServlet"); Wrapper wrapper2 = new SimpleWrapper(); wrapper2.setName("Modern"); wrapper2.setServletClass("ModernServlet"); Context context = new SimpleContext(); context.addChild(wrapper1); context.addChild(wrapper2); Valve valve1 = new HeaderLoggerValve(); Valve valve2 = new ClientIPLoggerValve(); ((Pipeline) context).addValve(valve1); ((Pipeline) context).addValve(valve2); Mapper mapper = new SimpleContextMapper(); mapper.setProtocol("http"); context.addMapper(mapper); Loader loader = new SimpleLoader(); context.setLoader(loader); // context.addServletMapping(pattern, name); context.addServletMapping("/Primitive", "Primitive"); context.addServletMapping("/Modern", "Modern"); connector.setContainer(context); try { connector.initialize(); connector.start(); // make the application wait until we press a key. System.in.read(); } catch (Exception e) { e.printStackTrace(); }
可以看到,啟動類創建了一個連接器HttpConnector,然后創建兩個 Wrapper實例,並設置servlet名稱;然后創建一個Context實例,將兩個Wrapper實例添加到Context里面。然后創建兩個閥,也添加到這兩個容器里面。然后創建一個映射器,將其加到Context容器里面,創建一個Loader 類,將其加到Context容器里面。
SimpleContext類的invoke()方法調用管道的invoke()方法。
public void invoke(Request request, Response response) throws IOException, ServletException { pipeline.invoke(request, response); }
來看一下SimplePipeline類的invoke()方法:
public void invoke(Request request, Response response) throws IOException, ServletException { // Invoke the first Valve in this pipeline for this request (new SimplePipelineValveContext()).invokeNext(request, response); }
來看一下SimplePipelineValveContext的invokeNext()方法。
public void invokeNext(Request request, Response response) throws IOException, ServletException { int subscript = stage; stage = stage + 1; // Invoke the requested Valve for the current request thread if (subscript < valves.length) { valves[subscript].invoke(request, response, this); } else if ((subscript == valves.length) && (basic != null)) { basic.invoke(request, response, this); } else { throw new ServletException("No valve"); } }
以上就是Context容器的實現。
生命周期:
Catalina包含很多組件,當Catalina啟動時,組件也會一起啟動,當Catalina關閉的時候,組件也會一起關閉。例如:當Servlet容器關閉的時候,它必須調用所有已經載入到容器中的servlet類中的destroy()方法,而Session管理器必須將Session對象保存到輔助存儲器中。通過實現Lifecycle接口,可以達到統一啟動/關閉這些組件的效果。
實現了Lifecycle接口的組件可以觸發一個或者多個下面的事件:
String INIT_EVENT = "init"; String START_EVENT = "start"; String BEFORE_START_EVENT = "before_start"; String AFTER_START_EVENT = "after_start"; String STOP_EVENT = "stop"; String BEFORE_STOP_EVENT = "before_stop"; String AFTER_STOP_EVENT = "after_stop"; String DESTROY_EVENT = "destroy"; String PERIODIC_EVENT = "periodic";
當組件啟動時,正常會觸發3個事件(start、before_start、after_start)。當組件關閉的時候,會觸發后3個事件(before_stop、stop、after_stop)。但需要編寫相應的事件監聽器對這些事件進行響應,事件監聽器是LifecycleListener接口。因此生命周期會有三個類型:Lifecycle、LifecycleEvent和LifecycleListener。
Lifecycle接口:Catalina在設計上允許一個組件包含其他組件。servlet容器可以包含載入器、管理器等。父組件負責啟動/關閉它的子組件。Catalina的設計使所有的組件都置於其父組件的監護之下,而Catalina的啟動類只需要啟動一個組件就可以將全部的應用的組件都啟動起來了。這種單一啟動/關閉機制是通過Lifecycle接口實現的。
Lifecycle接口中最重要的方法是start方法和stop方法。組件必須提供這兩個方法的實現,供父組件調用,實現對其的啟動和關閉。其他的addLifecycleListener、findLifecycleListeners、removeLifecycleListener三個方法都與該組件上的事件監聽器有關。
LifecycleEvent是一個實例,被final修飾。其中三個字段:
private Object data; private Lifecycle lifecycle; private String type;
LifecycleListener接口:只有一個lifecycleEvent方法,當事件監聽器監聽到相關事件發生,就會調用該方法。
LifecycleSupport類:一個事件監聽類的管理類,LifecycleSupport類將所有的生命周期監聽器存儲在一個名為listeners的數組中,並初始化為一個沒有元素的數組對象。當調用addLifecycleListener方法時,會創建一個新數組,大小為原來數組的長度+1,然后將原來的所有元素復制在新數組中,並將新的事件監聽器添加在新數組中。removeLifecycleListener方法的過程正好相反。fireLifecycleEvent方法會觸發一個生命周期事件,首先它會復制監聽器的數組,然后調用數組中的每個成員的lifecycleEvent方法,並傳入要觸發的事件。
建立在Lifecycle接口基礎上的應用程序:
SimpleContext類與之前的相比實現了 Lifecycle接口,SimpleContext類引用LifecycleSupport實例,
protected LifecycleSupport lifecycle = new LifecycleSupport(this);
然后去實現Lifecycle接口的方法:
public void addLifecycleListener(LifecycleListener listener) { lifecycle.addLifecycleListener(listener); }
public void removeLifecycleListener(LifecycleListener listener) { lifecycle.removeLifecycleListener(listener); }
start()
public synchronized void start() throws LifecycleException { if (started) throw new LifecycleException("SimpleContext has already started"); // Notify our interested LifecycleListeners lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null); started = true; try { // Start our subordinate components, if any if ((loader != null) && (loader instanceof Lifecycle)) ((Lifecycle) loader).start(); // Start our child containers, if any Container children[] = findChildren(); for (int i = 0; i < children.length; i++) { if (children[i] instanceof Lifecycle) ((Lifecycle) children[i]).start(); } // Start the Valves in our pipeline (including the basic), // if any if (pipeline instanceof Lifecycle) ((Lifecycle) pipeline).start(); // Notify our interested LifecycleListeners lifecycle.fireLifecycleEvent(START_EVENT, null); } catch (Exception e) { e.printStackTrace(); } // Notify our interested LifecycleListeners lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null); }
start()方法會檢查組件是否已經啟動過了,如果是,則拋出異常。 如果沒有,則去觸發事件:
lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null);
這樣SimpleContext實例中對該事件進行監聽的所有監聽器都會收到通知。接下來,start方法將started的布爾值設置為true,表明組件已經啟動。
然后繼續去啟動SimpleContext實例的組件,Loader組件、pipeline組件、以及Wrapper子容器都實現了Lifecycle接口,都可以運行其start方法去啟動。當所有組件都啟動完畢,start方法會觸發兩個事件,
lifecycle.fireLifecycleEvent(START_EVENT, null);
lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null);
而stop方法的實現與start邏輯相同。
SimpleContextLifecycleListener類實現了LifecycleListener接口,實現了lifecycleEvent方法,
public void lifecycleEvent(LifecycleEvent event) { Lifecycle lifecycle = event.getLifecycle(); System.out.println("SimpleContextLifecycleListener's event " + event.getType().toString()); if (Lifecycle.START_EVENT.equals(event.getType())) { System.out.println("Starting context."); } else if (Lifecycle.STOP_EVENT.equals(event.getType())) { System.out.println("Stopping context."); } }
輸出已觸發事件的類型。
日志記錄器:用來記錄消息的組件。
Logger接口:Tomcat的日志記錄器都必須實現該接口。暫時不說,此接口已不存在
載入器:一個標准的web應用程序的載入器。servlet容器需要實現一個自定義的載入器,而不能簡單的使用系統的載入器。因為servlet容器不應該完全信任它正在運行的servlet類,如果使用一個系統類的載入器載入某個servlet類所使用的全部類,那么servlet就能訪問所有的類,包括當前運行的java虛擬機中環境變量指明的路徑下的所有類和庫。而servlet只允許載入WEB-INF/classes目錄下及其子目錄下的類或者WEB-INF/lib下的類。tomcat需要實現自定義載入器的另一個原因是,為了提高自動重載的功能。當WEB-INF/classes目錄和WEB-INF/lib目錄下的類發生變化的時候,web應用程序會重新載入這些類。
先介紹java的類載入器:每次創建java實例的時候,都必須將類載入到內存去,jvm使用類載入器來載入需要的類,一般情況下,類載入器會在java核心庫里面和classpath中指明的目錄中搜索相關類。如果找不到這些類,它會拋出類找不到異常。
jvm使用了3中類載入器:引導類載入器(bootstrap class loader)、擴展類載入器(extension class loader)、系統類載入器(system class loader)。3個類載入器之間是父子繼承關系。其中引導類在最上層。系統類在最下層。
Tomcat要使用自定義載入器的原因:
在載入web應用程序中需要的servlet類及其相關類時需要遵守一些明確的規則,應用程序中的servlet類只能引用部署在WEB_INF/calsses目錄下的類。但是servelt不能訪問其他路徑的類,即使這些類在包含運行tomcat的jvm的環境變量下,而servlet類只能訪問WEB-INF/lib目錄下的庫。
Loader接口:
Tomcat中的載入器指的是web應用程序的載入器,而不僅僅是指類載入器。載入器必須實現Loader接口。在實現中,會使用一個自定義的類載入器。是WebappClassLoader類的實例,可以使用Loader接口的getclassloader方法來獲取web載入器中classLoader類的實例。
Loader接口還定義了一些方法來對倉庫的集合進行操作,web程序的WEB-INF/classes目錄下及其子目錄下的類或者WEB-INF/lib目錄是作為倉庫添加到類載入器的,使用Loader接口的addRepository方法來添加一個新倉庫,而findRepositories方法所有已添加倉庫的數組對象。
Catalina提供了WebappLoader類作為Loader接口的實現,其中WebappLoader對象中使用了WebappClassLoader類的實例作為其類載入器,該類繼承於URLclassLoader類。
ReLoader接口:支持類的自動重載功能。
WebappLoader類:
當調用WebappLoader類的start方法時,
1.創建一個類載入器
2.設置倉庫
3.設置類路徑
4.設置訪問權限
5.啟動一個新線程來支持自動重載。
WebappClassLoader類,是負責載入類的類加載器,繼承於URLClassLoader類。
Session管理:Catalina通過session管理器的組件來管理建立的session對象。該組件由Manager接口表示。Session管理器需要與一個Context容器相關聯。Session管理器負責創建、更新、銷毀session對象,當有請求到來時,要返回一個有效的session對象。
Session對象:
Session接口:作為catalina內部的Facade類使用