深入学习Java Web服务器系列三
一个简单的连接器
下面我们来学习tomcat中的连接器。
首先我们先了解一下Catalina的结构图。
1. 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对象的功能直接交给了我们的容器使用,而本篇博文,我们将写一个可以创建更好的请求和响应对象的连接器,用来改善之前的程序。
2. StringManager类(Tomcat5)
在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;
}
我们将在这篇博文中的程序中使用这种思想。
3. 模块划分
下面我们自己仿照tomcat来实现一个自己的连接器,我们将把本篇博文中的程序分成三个模块,connector, startup和core。
startup模块只有一个类,Bootstrap,用来启动应用的。
connector模块的类可以分为五组:
- 连接器和它的支撑类(HttpConnector和HttpProcessor)
- 指代HTTP请求的类(HttpRequest)和它的辅助类
- 指代HTTP响应的类(HttpResponse)和它的辅助类。
- Facade类(HttpRequestFacade和HttpResponseFacade)
- Constant类
core模块由两个类组成:ServletProcessor和StaticResourceProcessor。
程序的uml图如下图所示:
3.1 startup模块
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类的定义见下面模块。
3.2 connector模块
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或者
StaticResourceProcessor
import 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对象
HttpRequest类在上面两节的内容上增加了三个变量和其一些方法:
protected HashMap headers = new HashMap();
protected ArrayList cookies = new ArrayList();
protected ParameterMap parameters = null;
这三个变量存放着Http请求的头部,cookies和参数。
因此,一个servlet程序员可以从javax.servlet.http.HttpServletRequest中的下列方法中取得正确的返回 值:
getCookies,getDateHeader,getHeader, getHeaderNames, getHeaders, getParameter, getPrameterMap,getParameterNames和getParameterValues。
HttpRequest
import ex03.pyrmont.connector.RequestStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.servlet.http.Cookie;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletInputStream;
import java.security.Principal;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.Socket;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import org.apache.catalina.util.Enumerator;
import org.apache.catalina.util.ParameterMap;
import org.apache.catalina.util.RequestUtil;
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 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 {
}
}
HttpResponse类
import ex03.pyrmont.connector.ResponseStream;
import ex03.pyrmont.connector.ResponseWriter;
import ex03.pyrmont.connector.http.Constants;
import java.io.OutputStream;
import java.io.IOException;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.File;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.text.SimpleDateFormat;
import java.util.Locale;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import org.apache.catalina.util.CookieTools;
public class HttpResponse implements HttpServletResponse {
// the default buffer size
private static final int BUFFER_SIZE = 1024;
HttpRequest request;
OutputStream output;
PrintWriter writer;
protected byte[] buffer = new byte[BUFFER_SIZE];
protected int bufferCount = 0;
/**
* Has this response been committed yet?
*/
protected boolean committed = false;
/**
* The actual number of bytes written to this Response.
*/
protected int contentCount = 0;
/**
* The content length associated with this Response.
*/
protected int contentLength = -1;
/**
* The content type associated with this Response.
*/
protected String contentType = null;
/**
* The character encoding associated with this Response.
*/
protected String encoding = null;
/**
* The set of Cookies associated with this Response.
*/
protected ArrayList cookies = new ArrayList();
/**
* The HTTP headers explicitly added via addHeader(), but not including
* those to be added with setContentLength(), setContentType(), and so on.
* This collection is keyed by the header name, and the elements are
* ArrayLists containing the associated values that have been set.
*/
protected HashMap headers = new HashMap();
/**
* The date format we will use for creating date headers.
*/
protected final SimpleDateFormat format =
new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz",Locale.US);
/**
* The error message set by <code>sendError()</code>.
*/
protected String message = getStatusMessage(HttpServletResponse.SC_OK);
/**
* The HTTP status code associated with this Response.
*/
protected int status = HttpServletResponse.SC_OK;
public HttpResponse(OutputStream output) {
this.output = output;
}
/**
* call this method to send headers and response to the output
*/
public void finishResponse() {
// sendHeaders();
// Flush and close the appropriate output mechanism
if (writer != null) {
writer.flush();
writer.close();
}
}
public int getContentLength() {
return contentLength;
}
public String getContentType() {
return contentType;
}
protected String getProtocol() {
return request.getProtocol();
}
/**
* Returns a default status message for the specified HTTP status code.
*
* @param status The status code for which a message is desired
*/
protected String getStatusMessage(int status) {
switch (status) {
case SC_OK:
return ("OK");
case SC_ACCEPTED:
return ("Accepted");
case SC_BAD_GATEWAY:
return ("Bad Gateway");
case SC_BAD_REQUEST:
return ("Bad Request");
case SC_CONFLICT:
return ("Conflict");
case SC_CONTINUE:
return ("Continue");
case SC_CREATED:
return ("Created");
case SC_EXPECTATION_FAILED:
return ("Expectation Failed");
case SC_FORBIDDEN:
return ("Forbidden");
case SC_GATEWAY_TIMEOUT:
return ("Gateway Timeout");
case SC_GONE:
return ("Gone");
case SC_HTTP_VERSION_NOT_SUPPORTED:
return ("HTTP Version Not Supported");
case SC_INTERNAL_SERVER_ERROR:
return ("Internal Server Error");
case SC_LENGTH_REQUIRED:
return ("Length Required");
case SC_METHOD_NOT_ALLOWED:
return ("Method Not Allowed");
case SC_MOVED_PERMANENTLY:
return ("Moved Permanently");
case SC_MOVED_TEMPORARILY:
return ("Moved Temporarily");
case SC_MULTIPLE_CHOICES:
return ("Multiple Choices");
case SC_NO_CONTENT:
return ("No Content");
case SC_NON_AUTHORITATIVE_INFORMATION:
return ("Non-Authoritative Information");
case SC_NOT_ACCEPTABLE:
return ("Not Acceptable");
case SC_NOT_FOUND:
return ("Not Found");
case SC_NOT_IMPLEMENTED:
return ("Not Implemented");
case SC_NOT_MODIFIED:
return ("Not Modified");
case SC_PARTIAL_CONTENT:
return ("Partial Content");
case SC_PAYMENT_REQUIRED:
return ("Payment Required");
case SC_PRECONDITION_FAILED:
return ("Precondition Failed");
case SC_PROXY_AUTHENTICATION_REQUIRED:
return ("Proxy Authentication Required");
case SC_REQUEST_ENTITY_TOO_LARGE:
return ("Request Entity Too Large");
case SC_REQUEST_TIMEOUT:
return ("Request Timeout");
case SC_REQUEST_URI_TOO_LONG:
return ("Request URI Too Long");
case SC_REQUESTED_RANGE_NOT_SATISFIABLE:
return ("Requested Range Not Satisfiable");
case SC_RESET_CONTENT:
return ("Reset Content");
case SC_SEE_OTHER:
return ("See Other");
case SC_SERVICE_UNAVAILABLE:
return ("Service Unavailable");
case SC_SWITCHING_PROTOCOLS:
return ("Switching Protocols");
case SC_UNAUTHORIZED:
return ("Unauthorized");
case SC_UNSUPPORTED_MEDIA_TYPE:
return ("Unsupported Media Type");
case SC_USE_PROXY:
return ("Use Proxy");
case 207: // WebDAV
return ("Multi-Status");
case 422: // WebDAV
return ("Unprocessable Entity");
case 423: // WebDAV
return ("Locked");
case 507: // WebDAV
return ("Insufficient Storage");
default:
return ("HTTP Response Status " + status);
}
}
public OutputStream getStream() {
return this.output;
}
/**
* Send the HTTP response headers, if this has not already occurred.
*/
protected void sendHeaders() throws IOException {
if (isCommitted())
return;
// Prepare a suitable output writer
OutputStreamWriter osr = null;
try {
osr = new OutputStreamWriter(getStream(), getCharacterEncoding());
}
catch (UnsupportedEncodingException e) {
osr = new OutputStreamWriter(getStream());
}
final PrintWriter outputWriter = new PrintWriter(osr);
// Send the "Status:" header
outputWriter.print(this.getProtocol());
outputWriter.print(" ");
outputWriter.print(status);
if (message != null) {
outputWriter.print(" ");
outputWriter.print(message);
}
outputWriter.print("\r\n");
// Send the content-length and content-type headers (if any)
if (getContentType() != null) {
outputWriter.print("Content-Type: " + getContentType() + "\r\n");
}
if (getContentLength() >= 0) {
outputWriter.print("Content-Length: " + getContentLength() + "\r\n");
}
// Send all specified headers (if any)
synchronized (headers) {
Iterator names = headers.keySet().iterator();
while (names.hasNext()) {
String name = (String) names.next();
ArrayList values = (ArrayList) headers.get(name);
Iterator items = values.iterator();
while (items.hasNext()) {
String value = (String) items.next();
outputWriter.print(name);
outputWriter.print(": ");
outputWriter.print(value);
outputWriter.print("\r\n");
}
}
}
// Add the session ID cookie if necessary
/* HttpServletRequest hreq = (HttpServletRequest) request.getRequest();
HttpSession session = hreq.getSession(false);
if ((session != null) && session.isNew() && (getContext() != null)
&& getContext().getCookies()) {
Cookie cookie = new Cookie("JSESSIONID", session.getId());
cookie.setMaxAge(-1);
String contextPath = null;
if (context != null)
contextPath = context.getPath();
if ((contextPath != null) && (contextPath.length() > 0))
cookie.setPath(contextPath);
else
cookie.setPath("/");
if (hreq.isSecure())
cookie.setSecure(true);
addCookie(cookie);
}
*/
// Send all specified cookies (if any)
synchronized (cookies) {
Iterator items = cookies.iterator();
while (items.hasNext()) {
Cookie cookie = (Cookie) items.next();
outputWriter.print(CookieTools.getCookieHeaderName(cookie));
outputWriter.print(": ");
outputWriter.print(CookieTools.getCookieHeaderValue(cookie));
outputWriter.print("\r\n");
}
}
// Send a terminating blank line to mark the end of the headers
outputWriter.print("\r\n");
outputWriter.flush();
committed = true;
}
public void setRequest(HttpRequest request) {
this.request = request;
}
/* This method is used to serve a static page */
public void sendStaticResource() throws IOException {
byte[] bytes = new byte[BUFFER_SIZE];
FileInputStream fis = null;
try {
/* request.getUri has been replaced by request.getRequestURI */
File file = new File(Constants.WEB_ROOT, request.getRequestURI());
fis = new FileInputStream(file);
/*
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
*/
int ch = fis.read(bytes, 0, BUFFER_SIZE);
while (ch!=-1) {
output.write(bytes, 0, ch);
ch = fis.read(bytes, 0, BUFFER_SIZE);
}
}
catch (FileNotFoundException e) {
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());
}
finally {
if (fis!=null)
fis.close();
}
}
public void write(int b) throws IOException {
if (bufferCount >= buffer.length)
flushBuffer();
buffer[bufferCount++] = (byte) b;
contentCount++;
}
public void write(byte b[]) throws IOException {
write(b, 0, b.length);
}
public void write(byte b[], int off, int len) throws IOException {
// If the whole thing fits in the buffer, just put it there
if (len == 0)
return;
if (len <= (buffer.length - bufferCount)) {
System.arraycopy(b, off, buffer, bufferCount, len);
bufferCount += len;
contentCount += len;
return;
}
// Flush the buffer and start writing full-buffer-size chunks
flushBuffer();
int iterations = len / buffer.length;
int leftoverStart = iterations * buffer.length;
int leftoverLen = len - leftoverStart;
for (int i = 0; i < iterations; i++)
write(b, off + (i * buffer.length), buffer.length);
// Write the remainder (guaranteed to fit in the buffer)
if (leftoverLen > 0)
write(b, off + leftoverStart, leftoverLen);
}
/** implementation of HttpServletResponse */
public void addCookie(Cookie cookie) {
if (isCommitted())
return;
// if (included)
// return; // Ignore any call from an included servlet
synchronized (cookies) {
cookies.add(cookie);
}
}
public void addDateHeader(String name, long value) {
if (isCommitted())
return;
// if (included)
// return; // Ignore any call from an included servlet
addHeader(name, format.format(new Date(value)));
}
public void addHeader(String name, String value) {
if (isCommitted())
return;
// if (included)
// return; // Ignore any call from an included servlet
synchronized (headers) {
ArrayList values = (ArrayList) headers.get(name);
if (values == null) {
values = new ArrayList();
headers.put(name, values);
}
values.add(value);
}
}
public void addIntHeader(String name, int value) {
if (isCommitted())
return;
// if (included)
// return; // Ignore any call from an included servlet
addHeader(name, "" + value);
}
public boolean containsHeader(String name) {
synchronized (headers) {
return (headers.get(name)!=null);
}
}
public String encodeRedirectURL(String url) {
return null;
}
public String encodeRedirectUrl(String url) {
return encodeRedirectURL(url);
}
public String encodeUrl(String url) {
return encodeURL(url);
}
public String encodeURL(String url) {
return null;
}
public void flushBuffer() throws IOException {
//committed = true;
if (bufferCount > 0) {
try {
output.write(buffer, 0, bufferCount);
}
finally {
bufferCount = 0;
}
}
}
public int getBufferSize() {
return 0;
}
public String getCharacterEncoding() {
if (encoding == null)
return ("ISO-8859-1");
else
return (encoding);
}
public Locale getLocale() {
return null;
}
public ServletOutputStream getOutputStream() throws IOException {
return null;
}
public PrintWriter getWriter() throws IOException {
ResponseStream newStream = new ResponseStream(this);
newStream.setCommit(false);
OutputStreamWriter osr =
new OutputStreamWriter(newStream, getCharacterEncoding());
writer = new ResponseWriter(osr);
return writer;
}
/**
* Has the output of this response already been committed?
*/
public boolean isCommitted() {
return (committed);
}
public void reset() {
}
public void resetBuffer() {
}
public void sendError(int sc) throws IOException {
}
public void sendError(int sc, String message) throws IOException {
}
public void sendRedirect(String location) throws IOException {
}
public void setBufferSize(int size) {
}
public void setContentLength(int length) {
if (isCommitted())
return;
// if (included)
// return; // Ignore any call from an included servlet
this.contentLength = length;
}
public void setContentType(String type) {
}
public void setDateHeader(String name, long value) {
if (isCommitted())
return;
// if (included)
// return; // Ignore any call from an included servlet
setHeader(name, format.format(new Date(value)));
}
public void setHeader(String name, String value) {
if (isCommitted())
return;
// if (included)
// return; // Ignore any call from an included servlet
ArrayList values = new ArrayList();
values.add(value);
synchronized (headers) {
headers.put(name, values);
}
String match = name.toLowerCase();
if (match.equals("content-length")) {
int contentLength = -1;
try {
contentLength = Integer.parseInt(value);
}
catch (NumberFormatException e) {
;
}
if (contentLength >= 0)
setContentLength(contentLength);
}
else if (match.equals("content-type")) {
setContentType(value);
}
}
public void setIntHeader(String name, int value) {
if (isCommitted())
return;
//if (included)
//return; // Ignore any call from an included servlet
setHeader(name, "" + value);
}
public void setLocale(Locale locale) {
if (isCommitted())
return;
//if (included)
//return; // Ignore any call from an included servlet
// super.setLocale(locale);
String language = locale.getLanguage();
if ((language != null) && (language.length() > 0)) {
String country = locale.getCountry();
StringBuffer value = new StringBuffer(language);
if ((country != null) && (country.length() > 0)) {
value.append('-');
value.append(country);
}
setHeader("Content-Language", value.toString());
}
}
public void setStatus(int sc) {
}
public void setStatus(int sc, String message) {
}
}
httpconnector调用httpprocessor的process方法,通过传递socket对象,连接器解析HTTP请求头部并让servlet可以获得头部, cookies, 参数名/值等等。这就是连接器的重要作用。
3.3 core模块
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());
}
}
}

本文深入探讨了Tomcat的连接器,包括Catalina架构、StringManager类的作用以及连接器模块的划分。连接器负责接收HTTP请求,创建request和response对象,将流程传递给容器处理servlet。同时介绍了如何模仿Tomcat实现一个简单的连接器,将其分为startup、connector和core三个模块,讲解了各模块的功能和实现细节。
954

被折叠的 条评论
为什么被折叠?



