一个简单的连接器
下面我们来学习tomcat中的连接器。 首先我们先了解一下Catalina的结构图。
catalina 就是Tomcat服务器使用Servlet容器的名字。 Tomcat的核心可以分为3个部分:
Web容器—处理静态页面;catalina —处理servlet;jsp容器 — jsp页面翻译成一般的servlet我们可以把catalina看成是两个主要模块组成的,连接器(connector)和容器(container)。
连接器是用来“连接”容器里边的请求的。它的工作是为接收到每一个HTTP请求构造一个request和response对象。然后它把流程传递给容器。容器从连接器接收到requset和response对象之后调用servlet的service方法用于响应。
在本系列的前一篇博文中,一个简单的servlet容器,我们把创建request和response对象的功能直接交给了我们的容器使用,而本篇博文,我们将写一个可以创建更好的请求和响应对象的连接器,用来改善之前的程序。
在Tomcat中,错误信息对于系统管理员和servlet程序员都是有用的。例 如,Tomcat记录错误信息,让系统管理员可以定位发生的任何异常。对servlet程序员来说,Tomcat会在抛出的任何一个 javax.servlet.ServletException中发送一个错误信息,这样程序员可以知道他/她的servlet究竟发送什么错误了。
Tomcat所采用的方法是在一个属性文件里边存储错误信息,这样,可以容易的修改这些信息。不过,Tomcat中有数以百计的类。把所有类使用的错误信 息存储到一个大的属性文件里边将会容易产生维护的噩梦。为了避免这一情况,Tomcat为每个包都分配一个属性文件。例如,在包 org.apache.catalina.connector里边的属性文件包含了该包所有的类抛出的所有错误信息。每个属性文件都会被一个 org.apache.catalina.util.StringManager类的实例所处理。当Tomcat运行时,将会有许多 StringManager实例,每个实例会读取包对应的一个属性文件。此外,由于Tomcat的受欢迎程度,提供多种语言的错误信息也是有意义的。
当包里边的一个类需要查找放在该包属性文件的一个错误信息时,它首先会获得一个StringManager实例。不过,相同包里边的许多类可能也需要 StringManager,为每个对象创建一个StringManager实例是一种资源浪费。因此,StringManager类被设计成一个StringManager实例可以被包里边的所有类共享,这里,StringManager被设计成了单例模式的。我们通过传递一个包名来调用它的公共静态方法 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来实现一个自己的连接器,我们将把本篇博文中的程序分成三个模块,connector, startup和core。
startup模块只有一个类,Bootstrap,用来启动应用的。
connector模块的类可以分为五组: - 连接器和它的支撑类(HttpConnector和HttpProcessor) - 指代HTTP请求的类(HttpRequest)和它的辅助类 - 指代HTTP响应的类(HttpResponse)和它的辅助类。 - Facade类(HttpRequestFacade和HttpResponseFacade) - Constant类
core模块由两个类组成:ServletProcessor和StaticResourceProcessor。
程序的uml图如下图所示:
startup模块中只有一个启动类。
Bootstrap类 Bootstrap类中的main方法实例化HttpConnector类并调用它的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类指代一个连接器,职责是创建一个服务器套接字用来等待前来的HTTP请求。 HttpConnector类实现了java.lang.Runnable,所以它能被它自己的线程专用。当你启动应用程序,一个HttpConnector的实例被创建,并且它的run方法被执行。 一个HttpConnector主要完成下面的事情:
等待HTTP请求为每个请求创建个HttpProcessor实例调用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请求的套接字,会做下面的事情
创建一个HttpRequest对象创建一个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 "//" 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); }}SocketInputStream 是org.apache.catalina.connector.http.SocketInputStream。该类提供了获取请求行(request line)和请求头(request header)的方法。通过传入一个 InputStream 对象和一个代表缓冲区大小的整数值来创建 SocketInputStream 对象。
HttpProcessor 的 process 调用其私有方法 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 编程中,参数 jsessionid 通常是嵌入到 cookie 中的,也可以将其嵌入到查询字符串中 。
请求头(request header)由 HttpHeader 对象表示。可以通过 HttpHeader 的无参构造方法建立对象,并将其作为参数传给 SocketInputStream 的 readHeader 方法,该方法会自动填充 HttpHeader 对象。parseHeader 方法内有一个循环体,不断的从 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 要设置到 request 对象的属性中,如 contentLength 等。
cookie 是由浏览器作为请求头的一部分发送的,这样的请求头的名字是 cookie,它的值是一个 keyvalue 对。举例如下: Cookie: userName=budi; password=pwd; 对 cookie 的解析是通过 org.apache.catalina.util.RequestUtil 类的 parseCookieHeader 方法完成的。该方法接受一个 cookie 头字符串,返回一个 javax.servlet.http.Cookie 类型的数组。
我们通过解析http请求的信息并存在httprequest和httpresponse中,并通过process方法传递到core模块。
关于httprequest和httpresponse如何编写,可以参考之前的博文。
httpconnector调用httpprocessor的process方法,通过传递socket对象,连接器解析HTTP请求头部并让servlet可以获得头部, cookies, 参数名/值等等。这就是连接器的重要作用。
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()); } }}新闻热点
疑难解答