一個簡單的連接器
下面我們來學(xué)習(xí)tomcat中的連接器。 首先我們先了解一下Catalina的結(jié)構(gòu)圖。
catalina 就是Tomcat服務(wù)器使用Servlet容器的名字。 Tomcat的核心可以分為3個部分:
Web容器—處理靜態(tài)頁面;catalina —處理servlet;jsp容器 — jsp頁面翻譯成一般的servlet我們可以把catalina看成是兩個主要模塊組成的,連接器(connector)和容器(container)。
連接器是用來“連接”容器里邊的請求的。它的工作是為接收到每一個HTTP請求構(gòu)造一個request和response對象。然后它把流程傳遞給容器。容器從連接器接收到requset和response對象之后調(diào)用servlet的service方法用于響應(yīng)。
在本系列的前一篇博文中,一個簡單的servlet容器,我們把創(chuàng)建request和response對象的功能直接交給了我們的容器使用,而本篇博文,我們將寫一個可以創(chuàng)建更好的請求和響應(yīng)對象的連接器,用來改善之前的程序。
在Tomcat中,錯誤信息對于系統(tǒng)管理員和servlet程序員都是有用的。例 如,Tomcat記錄錯誤信息,讓系統(tǒng)管理員可以定位發(fā)生的任何異常。對servlet程序員來說,Tomcat會在拋出的任何一個 javax.servlet.ServletException中發(fā)送一個錯誤信息,這樣程序員可以知道他/她的servlet究竟發(fā)送什么錯誤了。
Tomcat所采用的方法是在一個屬性文件里邊存儲錯誤信息,這樣,可以容易的修改這些信息。不過,Tomcat中有數(shù)以百計的類。把所有類使用的錯誤信 息存儲到一個大的屬性文件里邊將會容易產(chǎn)生維護(hù)的噩夢。為了避免這一情況,Tomcat為每個包都分配一個屬性文件。例如,在包 org.apache.catalina.connector里邊的屬性文件包含了該包所有的類拋出的所有錯誤信息。每個屬性文件都會被一個 org.apache.catalina.util.StringManager類的實例所處理。當(dāng)Tomcat運(yùn)行時,將會有許多 StringManager實例,每個實例會讀取包對應(yīng)的一個屬性文件。此外,由于Tomcat的受歡迎程度,提供多種語言的錯誤信息也是有意義的。
當(dāng)包里邊的一個類需要查找放在該包屬性文件的一個錯誤信息時,它首先會獲得一個StringManager實例。不過,相同包里邊的許多類可能也需要 StringManager,為每個對象創(chuàng)建一個StringManager實例是一種資源浪費(fèi)。因此,StringManager類被設(shè)計成一個StringManager實例可以被包里邊的所有類共享,這里,StringManager被設(shè)計成了單例模式的。我們通過傳遞一個包名來調(diào)用它的公共靜態(tài)方法 getManager來獲得一個實例。每個實例存儲在一個以包名為鍵(key)的Hashtable中。
PRivate static Hashtable managers = new Hashtable();public synchronized static StringManager getManager(String packageName){ StringManager mgr = (StringManager)managers.get(packageName); if (mgr == null) { mgr = new StringManager(packageName); managers.put(packageName, mgr); } return mgr;}我們將在這篇博文中的程序中使用這種思想。
下面我們自己仿照tomcat來實現(xiàn)一個自己的連接器,我們將把本篇博文中的程序分成三個模塊,connector, startup和core。
startup模塊只有一個類,Bootstrap,用來啟動應(yīng)用的。
connector模塊的類可以分為五組: - 連接器和它的支撐類(HttpConnector和HttpProcessor) - 指代HTTP請求的類(HttpRequest)和它的輔助類 - 指代HTTP響應(yīng)的類(HttpResponse)和它的輔助類。 - Facade類(HttpRequestFacade和HttpResponseFacade) - Constant類
core模塊由兩個類組成:ServletProcessor和StaticResourceProcessor。
程序的uml圖如下圖所示:
startup模塊中只有一個啟動類。
Bootstrap類 Bootstrap類中的main方法實例化HttpConnector類并調(diào)用它的start方法
import ex03.pyrmont.connector.http.HttpConnector;public final class Bootstrap { public static void main(String[] args) { HttpConnector connector = new HttpConnector(); connector.start(); }}HttpConnector類的定義見下面模塊。
HttpConnector類
HttpConnector類指代一個連接器,職責(zé)是創(chuàng)建一個服務(wù)器套接字用來等待前來的HTTP請求。 HttpConnector類實現(xiàn)了java.lang.Runnable,所以它能被它自己的線程專用。當(dāng)你啟動應(yīng)用程序,一個HttpConnector的實例被創(chuàng)建,并且它的run方法被執(zhí)行。 一個HttpConnector主要完成下面的事情:
等待HTTP請求為每個請求創(chuàng)建個HttpProcessor實例調(diào)用HttpProcessor的process方法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 String getScheme() { return scheme; } 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; } // Hand this socket off to an HttpProcessor HttpProcessor processor = new HttpProcessor(this); processor.process(socket); } } public void start() { Thread thread = new Thread(this); thread.start(); }}HttpProcessor類 HttpProcessor類的process方法接受前來的HTTP請求的套接字,會做下面的事情
創(chuàng)建一個HttpRequest對象創(chuàng)建一個HttpResponse對象解析HTTP請求的第一行和頭部,并放到HttpRequest對象解析HttpRequest和HttpResponse對象到一個ServletProcessor或者 StaticResourceProcessorimport ex03.pyrmont.ServletProcessor;import ex03.pyrmont.StaticResourceProcessor;import java.net.Socket;import java.io.OutputStream;import java.io.IOException;import javax.servlet.ServletException;import javax.servlet.http.Cookie;import org.apache.catalina.util.RequestUtil;import org.apache.catalina.util.StringManager;/* 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("ex03.pyrmont.connector.http"); public void process(Socket socket) { SocketInputStream input = null; OutputStream output = null; try { input = new SocketInputStream(socket.getInputStream(), 2048); output = socket.getOutputStream(); // create HttpRequest object and parse request = new HttpRequest(input); // create HttpResponse object response = new HttpResponse(output); 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); 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)); uri = new String(requestLine.uri, 0, question); } 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 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); } // Normalize URI (using String Operations at the moment) String normalizedUri = normalize(uri); // Set the corresponding request properties ((HttpRequest) request).setMethod(method); request.setProtocol(protocol); if (normalizedUri != null) { ((HttpRequest) request).setRequestURI(normalizedUri); } else { ((HttpRequest) request).setRequestURI(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 "http://" in the normalized path while (true) { int index = normalized.indexOf("http://"); 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); }}SocketInputStream 是org.apache.catalina.connector.http.SocketInputStream。該類提供了獲取請求行(request line)和請求頭(request header)的方法。通過傳入一個 InputStream 對象和一個代表緩沖區(qū)大小的整數(shù)值來創(chuàng)建 SocketInputStream 對象。
HttpProcessor 的 process 調(diào)用其私有方法 parseRequest 來解析請求行(request line,即 http 請求的第一行)。下面是一個請求行(request line)的例子:
GET /myApp/ModernServlet?userName=tarzan&passWord=pwd HTTP/1.1注意:“GET”后面和“HTTP”前面各有一個空格。 請求行的第 2 部分是 uri 加上查詢字符串。在上面的例子中,uri 是: /myApp/ModernServlet 問號后面的都是查詢字符串,這里是: userName=tarzan&password=pwd 在 servlet/jsp 編程中,參數(shù) jsessionid 通常是嵌入到 cookie 中的,也可以將其嵌入到查詢字符串中 。
請求頭(request header)由 HttpHeader 對象表示。可以通過 HttpHeader 的無參構(gòu)造方法建立對象,并將其作為參數(shù)傳給 SocketInputStream 的 readHeader 方法,該方法會自動填充 HttpHeader 對象。parseHeader 方法內(nèi)有一個循環(huán)體,不斷的從 SocketInputStream 中讀取 header 信息,直到讀完。獲取 header 的 name 和value 值可使用下米娜的語句: String name = new String(header.name, 0, header.nameEnd); String value = new String(header.value, 0, header.valueEnd); 獲取到 header 的 name 和 value 后,要將其填充到 HttpRequest 的 header 屬性(hashMap 類型)中: request.addHeader(name, value); 其中某些 header 要設(shè)置到 request 對象的屬性中,如 contentLength 等。
cookie 是由瀏覽器作為請求頭的一部分發(fā)送的,這樣的請求頭的名字是 cookie,它的值是一個 keyvalue 對。舉例如下: Cookie: userName=budi; password=pwd; 對 cookie 的解析是通過 org.apache.catalina.util.RequestUtil 類的 parseCookieHeader 方法完成的。該方法接受一個 cookie 頭字符串,返回一個 javax.servlet.http.Cookie 類型的數(shù)組。
我們通過解析http請求的信息并存在httprequest和httpresponse中,并通過process方法傳遞到core模塊。
關(guān)于httprequest和httpresponse如何編寫,可以參考之前的博文。
httpconnector調(diào)用httpprocessor的process方法,通過傳遞socket對象,連接器解析HTTP請求頭部并讓servlet可以獲得頭部, cookies, 參數(shù)名/值等等。這就是連接器的重要作用。
StaticResourceProcessor import ex03.pyrmont.connector.http.HttpRequest; import ex03.pyrmont.connector.http.HttpResponse; import java.io.IOException;
public class StaticResourceProcessor {
public void process(HttpRequest request, HttpResponse response) { try { response.sendStaticResource(); } catch (IOException e) { e.printStackTrace(); } }
}
ServletProcessor
import ex03.pyrmont.connector.http.Constants;import ex03.pyrmont.connector.http.HttpRequest;import ex03.pyrmont.connector.http.HttpResponse;import ex03.pyrmont.connector.http.HttpRequestFacade;import ex03.pyrmont.connector.http.HttpResponseFacade;import java.io.File;import java.io.IOException;import java.net.URL;import java.net.URLClassLoader;import java.net.URLStreamHandler;import javax.servlet.Servlet;public class ServletProcessor { public void process(HttpRequest request, HttpResponse response) { String uri = request.getRequestURI(); 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); String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString() ; 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(); HttpRequestFacade requestFacade = new HttpRequestFacade(request); HttpResponseFacade responseFacade = new HttpResponseFacade(response); servlet.service(requestFacade, responseFacade); ((HttpResponse) response).finishResponse(); } catch (Exception e) { System.out.println(e.toString()); } catch (Throwable e) { System.out.println(e.toString()); } }}新聞熱點
疑難解答