【0】README
0.1)本文部分内容转自“深入剖析tomcat”,旨在学习 tomcat(3)连接器 的基础知识;
0.2)Catalina 中有两个主要的模块:连接器(ServerSocket) 和 容器(Servlet容器);(干货——Catalina 中有两个主要的模块:连接器(ServerSocket) 和 容器(Servlet容器))
0.3)for complete source code, please visit https://github.com/pacosonTang/HowTomcatWorks/tree/master/chapter3;
0.4)温馨建议:建议阅读本文之前,已阅读过 tomcat(1~2)的系列文章,因为它们是环环相扣的;
【1】StringManager类
0)intro to StringManager:该类用于处理 properties文件, 这些文件记录其所在包的类的错误信息;
1)problem+solution:
1.1)
problem:Tomcat处理错误消息的方法是,将错误消息存储在一个 properties 文件中,便于读取和编辑,如果将所有类使用的错误消息都存储在一个大的properties文件中,那维护这个文件将会很头疼;
1.2)solution:Tomcat 将properties文件划分到不同的包中,每个properties文件都是用 org.apache.catalina.util.StringManager 类的一个实例来处理;当包中的某个类需要在其包内的properties文件中查找错误消息时,它会先获取对应的 StringManager 实例;StringManager可以被包下的所有 类所共享;
2)StringManager是单例类:代码如下
// StringManager的source code.
// 该类用于处理 properties文件, 这些文件记录其所在包的类的错误信息;
public class StringManager {private static Hashtable managers = new Hashtable();private String packageName;private StringManager(String packageName) {this.packageName = packageName;}public synchronized static StringManager getManager(String packageName) {StringManager manager = (StringManager) managers.get(packageName);if(manager == null) {manager = new StringManager(packageName);managers.put(packageName, manager);}return manager;}
}
2.1)StringManager的app 荔枝:从 com.baidu 包下的类中使用 StringManager 的方法
StringManager manager = StringManger.getManager("com.baidu");
2.2)LocalStrings.properties 文件的第一行非注释内容如下:
httpConnector.alreadyInitialized=HTTP connector has already been initialized
调用 StringManager 的 getString() 方法,传入httpConnector.alreadyInitialized 就可以返回 value=HTTP connector has already been initialized
【2】应用程序
1)本应用程序包含3个模块: 连接器模块,启动模块 和 核心模块;
1.1)连接器模块有以下5个类型(types):
t1)连接器及其支持类(HttpConnector and HttpProcessor);t2)表示HTTP 请求的类(HttpRequest)及其支持类;t3)表示HTTP响应的类(HttpResponse)及其支持类;t4)外观类(HttpRequestFacade and HttpResponseFacade);t5)常量类;
1.2)启动模块:只有一个类(Bootstrap),负责启动应用程序;
1.3)核心模块包括两个类:servletProcessor and StaticResourceProcessor;
2)应用程序的细节(details)
d1)启动应用程序;d2)连接器;d3)创建 HttpRequest 对象;d4)创建 HttpResponse 对象;d5)静态资源处理器和 servlet处理器;d6)运行应用程序;
【2.1】启动应用程序 Bootstrap类
// 启动应用程序
public final class Bootstrap {public static void main(String[] args) {HttpConnector connector = new HttpConnector(); // 连接器connector.start(); //启动一个线程}
}
【2.2】HttpConnector类
1)intro to HttpConnector类:该类负责创建一个服务器套接字,该套接字会等待传入的HTTP请求;
2)HttpConnector类: 实现了 Runnable接口,这样可以专用于自己的线程;
3)run方法包含了一个while循环,该循环中执行如下3个操作(operations):
(干货——HttpConnector需要完成的操作——
创建服务器套接字并接受 client发出的HTTP请求,之后调用连接器的支持类 HttpProcessor(HTTP处理器)的process方法.)
o1)等待HTTP请求;o2)为每个请求创建一个 HttpProcessor实例;o3)调用 HttpProcessor 对象的 process() 方法;(干货——我们赶紧转向 HttpProcessor):
// 等待HTTP 请求 的工作(连接器)
public class HttpConnector implements Runnable { // 创建服务器套接字并接受 client发出的HTTP请求,之后调用连接器的支持类 HttpProcessor(HTTP处理器)的process方法.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 socketSocket socket = null;// 负责创建一个服务器套接字。try {socket = serverSocket.accept(); // 等待HTTP请求}catch (Exception e) {continue;}// Hand this socket off to an HttpProcessorHttpProcessor processor = new HttpProcessor(this);processor.process(socket);}}public void start() {Thread thread = new Thread(this);thread.start();}
}
【2.2.1】HttpProcessor类
1)HttpProcessor类的process()方法接收来自传入的HTTP请求的套接字。对每个传入的HTTP请求,它要完成4个操作(operations):(干货——HttpProcessor类需要完成4个操作)
HttpProcessor)
HttpProcessor)
o1)创建一个 HttpRequest对象;o2)创建一个 HttpResponse对象;o3)解析HTTP 请求的第一行内容和请求头信息,填充 HttpRequest对象;o4)将 HttpRequest 对象和 HttpResponse对象传递给 servletProcessor 或 StaticResourceProcessor 的process() 方法;public void process(Socket socket) {@SuppressWarnings("deprecation")SocketInputStream input = null;OutputStream output = null;try {input = new SocketInputStream(socket.getInputStream(), 2048);output = socket.getOutputStream();// create HttpRequest object and parserequest = new HttpRequest(input);// create HttpResponse objectresponse = new HttpResponse(output);response.setRequest(request);response.setHeader("Server", "Pyrmont Servlet Container"); // 向客户发送响应头信息parseRequest(input, output); // 解析请求,填充HttpReqeust对象(请求的是静态资源还是servlet,具体是什么静态资源或servlet)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 socketsocket.close();// no shutdown for this application}catch (Exception e) {e.printStackTrace();}}
对上述代码的分析(Analysis):
A1)对于HTTP请求的review:(第一行是请求方法——URI——协议/版本;之后的文本行直到空行,空行前的文本是请求头信息,空行后的文本是请求实体正文)
- Post /examples/default.jsp HTTP/1.1 // the first line
- Accept: text/plain; text/html // request header begins
- Accept-Language: en-gb
- Connection: Keep-Alive
- Host: localhost
- User-Agent: Mozilla/4.0 (compatible; MSIE 4.01; Windows 98)
- Content-Length: 33
- Content-Type: application/x-www-form-urlencoded
- Accept-Encoding: gzip, deflate // request header ends
- // 这里是空行(CRLF) // empty line
- lastName=Yun&firstName=Lin // request entiry body
A2)parseRequest方法的解析对象:是 http请求的 第一行(请求方法——URI——协议/版本)
A3)parseHeader方法的解析对象:是http请求的请求头信息;
【2.3】创建 HttpRequest对象
0)HttpProcessor 中的process()方法:该方法负责填充该对象中的属性,一句话说完,该类就是封装了一些Http请求信息;(servlet程序员可以通过调用 HttpserveltRequest类的一些方法获取 HTTP请求信息)
1)HttpRequest实现了 HttpservletRequest接口:其外观类是 HttpRequestFacade类:
2)HttpProcessor的process方法调用 parseRequest 和 parseHeader 方法,而parseHeader() 方法调用 addHeader()方法和addCookie() 填充HttpReqeust类的相关信息;
3)解析HTTP请求,process方法的执行流程,非常复杂,分为5个steps:
step1)读取套接字的输入流;step2)解析请求行;step3)解析请求头;step4)解析Cookie;step5)获取参数;
3.1)读取套接字的输入流
3.2)解析请求行:HttpProcessor 类的 process() 方法会调用私有方法parseRequest() 来解析请求行,即HTTP请求的第1行内容;
3.2.1)parseRequest方法首先会调用 SocketInputStream类的readRequestLine方法;3.2.2)parseRequest方法从请求行中获取请求方法,URI 和 请求协议的版本信息:(对parseRequest()方法进行分解阐述)private void parseRequest(@SuppressWarnings("deprecation") SocketInputStream input, OutputStream output)throws IOException, ServletException {input.readRequestLine(requestLine); <span style="font-family: 宋体;">// Parse the incoming request line</span>String method =new String(requestLine.method, 0, requestLine.methodEnd);String uri = null;String protocol = new String(requestLine.protocol, 0, requestLine.protocolEnd);
3.2.3)在URI后面可能还有查询字符串,调用setQueryString方法来解决;(干货——查询字符串如name=tang&pwd=123456)// Parse any query parameters out of the request URIint 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);}
3.2.4)parseRequest会进行相对路径和绝对路径的检查;// Checking for an absolute URI (with the HTTP protocol)if (!uri.startsWith("/")) {int pos = uri.indexOf("://");// Parsing out protocol and host nameif (pos != -1) {pos = uri.indexOf('/', pos + 3);if (pos == -1) {uri = "";}else {uri = uri.substring(pos);}}}
3.2.5)检查查询字符串中是否包含会话标识符(jsessionid),解析后并将其填充到 HttpRequest;(干货——会话标识符:浏览器的会话使用存储在 SessionID 属性中的唯一标识符进行标识。)// 检查是否有会话标识符,如果有的话,做进一步解析// Parse any requested session ID out of the request URIString 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);
3.2.6)parseRequest方法将URI 传入到 normaliza方法,对非正常的URL 进行修正;(如,出现'\'将其修正为 '/')3.2.7)最后,parseRequest方法会设置HttpRequest对象的一些属性// 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 + "'");}}
3.3)解析请求头(HttpHeader):有5件事情需要了解(things)
t1)可以通过无参构造器创建HttpHeader 实例;t2)可以将其实例传给 SocketInputStream类的readHeader() 方法;t3)获取请求头的名字和值,使用如下方法:
String name = new String(header.name, 0, header.nameEnd);
String value = new String(header.value, 0, header.valueEnd);
t4)parseHeaders() 方法有一个while循环,后者不断从SocketInputStream 中读取请求头信息,直到全部读完;t5)然后,可以通过检查HttpHeader实例的nameEnd 和valueEnd 字段来判断是否已经从输入流中读取了所有的请求头信息;
3.3.1)当读取完请求头的名称和值之后,调用 HttpReqeust 的 addHeader() 方法,将其添加到HttpRequest 对象的HashMap请求头中:request.addHeader(name, value);// protected HashMap headers = new HashMap(); headers 就是一个hashmap(键值对)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);}}
3.3.2)某些请求头包含一些属性设置信息private void parseHeaders(@SuppressWarnings("deprecation") SocketInputStream input)throws IOException, ServletException {while (true) {@SuppressWarnings("deprecation")HttpHeader header = new HttpHeader();;// Read the next headerinput.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); // this line.// do something for some headers, ignore others.if (name.equals("cookie")) {Cookie cookies[] = RequestUtil.parseCookieHeader(value); // this line , parse Cookie infofor (int i = 0; i < cookies.length; i++) {if (cookies[i].getName().equals("jsessionid")) {// Override anything requested in the URLif (!request.isRequestedSessionIdFromCookie()) {// Accept only the first session id cookierequest.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}
3.4)解析Cookie
3.4.1)intro to Cookie:Cookie是由浏览器作为HTTP请求头的一部分发送的。这样的请求头名称是 cookie,其对应值是一些名值对。(干货——intro to Cookie)Cookie是由服务器端生成,发送给User-Agent(一般是浏览器),浏览器会将Cookie的key/value保存到某个目录下的文本文件内,下次请求同一网站时就发送该Cookie给服务器(前提是浏览器设置为启用cookie)。Cookie名称和值可以由服务器端开发自己定义,对于JSP而言也可以直接写入jsessionid,这样服务器可以知道该用户是否是合法用户以及是否需要重新登录等,服务器可以设置或读取Cookies中包含信息,借此维护用户跟服务器会话中的状态。(干货——Cookie的作用)
3.4.2)看个荔枝:下面是一个Cookie请求头的荔枝,包含两个Cookie:userName 和 password;Cookie:userName=tang; password=xiao;3.4.3)对Cookie的解析 通过 RequestUtil.parseCookieHeader() 方法来完成:解析完Cookie后,交由 HttpProcessor 类 的 parseHeader() 方法处理;
// do something for some headers, ignore others.if (name.equals("cookie")) {Cookie cookies[] = RequestUtil.parseCookieHeader(value); // this line , parse Cookie infofor (int i = 0; i < cookies.length; i++) {if (cookies[i].getName().equals("jsessionid")) {// Override anything requested in the URLif (!request.isRequestedSessionIdFromCookie()) {// Accept only the first session id cookierequest.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
public static Cookie[] parseCookieHeader(String header) {if ((header == null) || (header.length() < 1))return (new Cookie[0]);ArrayList cookies = new ArrayList();while (header.length() > 0) {int semicolon = header.indexOf(';');if (semicolon < 0)semicolon = header.length();if (semicolon == 0)break;String token = header.substring(0, semicolon);if (semicolon < header.length())header = header.substring(semicolon + 1);elseheader = "";try {int equals = token.indexOf('=');if (equals > 0) {String name = token.substring(0, equals).trim();String value = token.substring(equals+1).trim();cookies.add(new Cookie(name, value));}} catch (Throwable e) {;}}return ((Cookie[]) cookies.toArray(new Cookie[cookies.size()]));}
3.5)获取参数
0)intro: 参数由 ParameterMap进行封装;参数可以出现在查询字符串或请求体中。若用户使用GET 方法请求servlet,则所有的参数都会在查询字符串中;若用户使用 POST 方法请求servlet,则请求体中也可能会有参数;所有的键值对都会存储在一个 HashMap对象中;
1)parseParameters() 方法如何工作: 由于参数可以存在于查询字符串或 HTTP请求体中,故parseParameter()方法 必须对这两者进行检查
1.1)首先检查parse是否为true,是否被解析过(参数只需要解析一次即可)
if (parsed)return;
1.2)然后该方法会创建一个ParameterMap对象以存储参数信息;
ParameterMap results = parameters;
if (results == null)results = new ParameterMap();
1.3)打开ParameterMap的锁,使其可写:
results.setLocked(false);
1.4)检查字符串encoding,若encoding为null,使用默认编码;
String encoding = getCharacterEncoding();if (encoding == null)encoding = "ISO-8859-1";
1.5)对参数进行解析,调用RequestUtil.parseParameters() 方法完成;
// Parse any parameters specified in the query stringString queryString = getQueryString();try {RequestUtil.parseParameters(results, queryString, encoding);}catch (UnsupportedEncodingException e) {;}
1.6)检查HTTP 请求体是否包含请求参数。若用户使用POST提交请求时,请求体会包含参数;下面的代码用于解析请求体:
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 stringString queryString = getQueryString();try {RequestUtil.parseParameters(results, queryString, encoding);}catch (UnsupportedEncodingException e) {;}// Parse any parameters specified in the input streamString 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 resultsresults.setLocked(true);parsed = true;parameters = results;}
1.7)最后,parseParameters() 方法会锁定ParameterMap对象,将parsed设置为true,将result设置为 parameters;
// Store the final resultsresults.setLocked(true);parsed = true;parameters = results;
【2.4】创建 HttpResponse对象
1)intro to HttpResponse对象:该类实现 HttpservletResponse接口,与其对应的外观类是 HttpResponseFacade;
2)什么是 Writer? 在servlet中,可以使用PrintWriter 对象向输出流中写字符。可以使用任意 编码格式,但在向浏览器发送字符的时候,实际上都是字节流。
3)如何创建PringWriter对象呢?可以通过传入一个java.io.OutputStream 实例来创建 PrintWriter对象。所传给PrintWriter类的print方法或println方法的任何字符串都会被转换为字节流,使用基本的输出流发送到客户端;(使用一个 ResponseWriter实例 和 一个基本的ResponseStream对象)
4)OutputStreamWriter类:传入的字符会被转换为使用指定字符集的字节数组。其中所使用的字符集可以通过名称显式指定,也可以使用平台的默认字符集。每次调用写方法时,都会先使用编码转换器对给定字符进行编码转换。在被写入基本输出流之前,返回的字节数组会先存储在缓冲区中。缓冲区的大小是固定的,其默认值足够大。注意,传递给写方法的字符是没有缓冲的;
5)getWriter方法实现如下:
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;}
【2.5】静态资源处理器和servlet处理器
1)本文的ServletProcessor 类中的process 方法:都会接受一个 HttpRequest对象和一个 HttpResponse对象,而不是Request 和 Response的实例,下面是process方法签名:
public void process(HttpRequest request, HttpResponse response) {
2)process() 方法使用 HttpRequestFacade类 和 HttpResponseFacade 类作为 request 和response 对象的外观类。当调用完 servlet 的 service() 方法后,它还会调用一次 HttpRespose类的 finishResponse()方法;
((HttpResponse) response).finishResponse();
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 URLClassLoaderURL[] 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("servlet." + 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();}...}
}
补充)本文总结了上述应用程序的调用流程图
对上图的分析(Analysis):
A1)HttpConnector(http连接器).process()方法:创建服务器套接字,接收 client发出的http请求,并调用HttpProcessor(http处理器)进行处理;
A2)HttpProcessor(http处理器).process()方法:
step1)通过套接字创建输入输出流;
step2)传入输入流创建Reqeust对象,传入输出流创建Response对象,且设置Response 的某变量引用Request实例对象;
step3)parseRequest方法:解析请求体的第一行(请求方法——URI——协议/版本),并附加解析出查询字符串(?name=tang&pwd=123456),请求路径是否为绝对路径,会话标识符等;
step4)parseHeader方法:解析请求体的请求头信息;(附加会解析cookie)
step5)通过parseRequest方法解析出的URI 判断client请求的是资源是servlet 还是 静态资源,并做相应处理;
step5.1)若是静态资源:直接调用response.sendStaticResource() 方法 读取静态文件数据并发送给client;
step5.2)若是servlet:调用ServletProcessor.process() 方法进行如下处理,首先构建类加载器加载servlet,然后创建该servlet实例,并调用其service方法,service方法发送响应信息到client;
【2.6】运行应用程序
Attention)运行参数(设置classpath)为:
E:\bench-cluster\cloud-data-preprocess\HowTomcatWorks\src>java -cp .;lib/servlet.jar;lib/catalina_4_1_24.jar;E:\bench-cluster\cloud-data-preprocess\HowTomcatWorks\webroot com.tomcat.chapter3.startup.Bootstrap