Tomcat剖析(三):连接器(1)

Tomcat剖析(三):连接器(1)

第一部分:概述

这一节基于《深度剖析Tomcat》第三章:连接器 总结而成。

大家都知道Catalina 中有两个主要的模块:连接器和容器。本节将HttpServer2完善为HttpConnect,创建一个更好的请求和响应对象的连接器。

最好先到我的github上下载本书相关的代码,同时去上网下载这本书。

上一节是使用Reques对象,因为我们不知道请求的类型,而这一节中,如果是HTTP请求,那么Request对象中需要更多的信息,所以本节的重点是连接器(中的处理器)解析 HTTP 请求头部和cookie并让 servlet 可以获得头部, cookies, 参数名/值等等,不再只是解析URI。同时,将会完善第 2 节中 Response 类的 getWriter 方法,让它能够正确运行。由于这些改进,你将会从上一节的PrimitiveServlet 中获取一个完整的响应,并能够运行更加复杂的ModernServlet。

上一节的HttpServer 类被分离为两个类:HttpConnector和 HttpProcessor,Request 被 HttpRequest 所取代,而 Response 被 HttpResponse 所取代。HttpServer 类的职责是等待 HTTP 请求并创建请求和响应对象。在本节的应用中,等待 HTTP 请求的工作交给 HttpConnector 实例,而创建请求和响应对象的工作交给了HttpProcessor 实例。

一个 HttpRequest 对象将会给转换为一个 HttpServletRequest 实例并传递给被调用的 servlet 的 service 方法。因此,每个 HttpRequest 实例必须适当增加字段,以便 servlet可以使用它们。值需要赋给 HttpRequest 对象,包括 URI,查询字符串,参数,cookies 和其他的头部等等(可以在代码中看到)。因为连接器并不知道被调用的 servlet(就是我们自己定义的servlet可能是很复杂的,需要获取请求中所有的参数)需要哪个值,所以连接器必须从 HTTP 请求中解析所有可获得的值。

同时,不是简简单单的调用自己的await方法,而是用线程启动。

核心类有4个 (上一节讲过的类这一节有的重复出现的,不列出来了):

  • HttpConnector.java: 等待 HTTP 请求并创建请求和响应对象

  • HttpProcessor.java:创建请求和响应对象

  • SocketInputStream.java:负责直接从输入流中获取头部和请求行信息

  • RequestUtil.java:这节的功能是对请求头中cookie的解析

  • HttpHeader.java、HttpRequest.java、HttpRequestLine.java、HttpResponse.java分别封装了请求头,请求,请求行,响应需要的内容。HttpRequestFacade.java和HttpResponseFacade.java同样是为了安全性而写的,实现方式和上一节类似(哪里不安全了,可参照上一节)。ServletProcessor.java也只是将相应的类(如RequestFacade改为HttpRequestFacade)进行修改。

第二部分:代码讲解

HttpConnector.java

  1. 等待 HTTP 请求
  2. 为每个请求创建个 HttpProcessor 实例
  3. 调用 HttpProcessor 的 process 方法

这个类是这一节中最简单的,不用详细说明了,不同的可以参考上一节 代码注释解释如下:

package ex03.pyrmont.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";

    // 返回一个 scheme,这里是HttpConnector,当然是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;
            }
            // 除了用线程启动外,这里也是与上节的不同之处,不从连接器中直接判断请求类型,而是交给HttpProcessor处理
            HttpProcessor processor = new HttpProcessor(this);
            processor.process(socket);
        }
    }

    public void start() {
        Thread thread = new Thread(this);
        thread.start();
    }
}

HttpProcessor.java

HttpProcessor 类的 process 方法接受前来的 HTTP 请求的套接字,会做下面的事情:

  1. 创建一个 HttpRequest 对象。

  2. 创建一个 HttpResponse 对象。

  3. 解析 HTTP 请求的第一行和头部,并放到 HttpRequest 对象。

  4. 解析 HttpRequest 和 HttpResponse 对 象 到 一 个 ServletProcessor 或者StaticResourceProcessor

所以这个类需要有HttpRequest、HttpRequestLine和HttpResponse实例

package ex03.pyrmont.connector.http;

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;

public class HttpProcessor {

    public HttpProcessor(HttpConnector connector) {
        this.connector = connector;
    }
    //HttpProcessor对应的HttpConnector,由构造器传入
    private HttpConnector connector = null;
    private HttpRequest request;
    private HttpRequestLine requestLine = new HttpRequestLine();
    private HttpResponse response;

    protected String method = null;
    protected String queryString = null;

    //Tomcat的错误管理方式,将在本节最后讲解
    protected StringManager sm = StringManager
            .getManager("ex03.pyrmont.connector.http");

    //由HttpConnector类调用,socket为当前发出请求的用户
    public void process(Socket socket) {

        //SocketInputStream将在本文后面讲解
        SocketInputStream input = null;
        OutputStream output = null;
        try {
            //对获取输入流进行封装。
            //里面的readHeader(HttpHeader)和readRequestLine(HttpRequestLine)
            //用于直接从得到的流中获取Heander和RequestLine对象
            input = new SocketInputStream(socket.getInputStream(), 2048);
            output = socket.getOutputStream();

            //创建HttpRequest对象
            request = new HttpRequest(input);

            //创建HttpRequest对象
            response = new HttpResponse(output);
            response.setRequest(request);
            //这里可以设置响应给浏览器的响应头
            response.setHeader("Server", "Pyrmont Servlet Container");

            //本节的重点,解析请求行和请求头
            parseRequest(input, output);
            parseHeaders(input);

            //if else块和上一节一样,通过判断URI的不同调用不同的Processor处理器
            if (request.getRequestURI().startsWith("/servlet/")) {
                ServletProcessor processor = new ServletProcessor();
                processor.process(request, response);
            } else {
                StaticResourceProcessor processor = new StaticResourceProcessor();
                processor.process(request, response);
            }

            //关闭socket
            socket.close();
            //注意:这个应用还没有关闭程序功能,需要自己强制关闭
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //功能:解析请求头
    //这个方法是org.apache.catalina.connector.http.HttpProcessor的简化版本
    //这个方法解析了一些“简单”的头部,像"cookie", "content-length","content-type",忽略了其他头部
    private void parseHeaders(SocketInputStream input) throws IOException,
            ServletException {
        //进入死循环,直到解析请求头结束跳出
        while (true) {
            HttpHeader header = new HttpHeader();
            //SocketInputStream流中获取Header对象,本文后面具体讲解
            //从SocketInputStream代码的readHeader中可以看到通过这里的while方法,一次提取出一个键值对的请求头的值
            //所以下面的name和value就获得到了相应的值
            //parseRequest也是使用这样的方法,下面就忽略了
            input.readHeader(header);

            //检测 HttpHeader 实例的 nameEnd 和 valueEnd 字段来测试是否可以从输入流中读取下一个头部信息
            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);

            //将获取到的请求头的名称和值放入HttpRquest对象中
            //如名称可以为content-length,值可以为10164(某个数字)
            request.addHeader(name, value);

            //判断是否是cookie(cookie包含在请求头中),格式如
//Cookie: BD_UPN=1d314753; ispeed_lsm=4; sugstore=1; BAIDUID=3E664426E867095427DD59:FG=1; BIDUPSID=3E664426E827DD59; PSTM=1440774226; BDUSS=Ex4NkJ0bEF0WTgwMwAAAA; ATS_PSID=1
            if (name.equals("cookie")) {
                //如果是cookie,还要对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]);
                }
                //判断请求中是否有content-length
            } else if (name.equals("content-length")) {
                int n = -1;
                try {
                    //有的话直接转为int类型保存到HttpRequest对象中
                    n = Integer.parseInt(value);
                } catch (Exception e) {
                    throw new ServletException(
                            sm.getString("httpProcessor.parseHeaders.contentLength"));
                }
                request.setContentLength(n);
            } else if (name.equals("content-type")) {
                //如果是content-type直接保存
                request.setContentType(value);
            }
        } // end while
    }

    //解析请求行
    //请求行如:GET /myApp/ModernServlet?userName=tarzan&password=pwd HTTP/1.1
    private void parseRequest(SocketInputStream input, OutputStream output)
            throws IOException, ServletException {

        //从SocketInputStream对象中直接获取RequestLine对象
        input.readRequestLine(requestLine);

        //获取请求的方式:如GET
        String method = new String(requestLine.method, 0, requestLine.methodEnd);
        //这里没有直接获取请求的URI
        //因为如/myApp/ModernServlet?userName=tarzan&password=pwd后面有查询的字符串,需要先分割
        String uri = null;
        //获取请求的协议版本:如HTTP/1.1
        String protocol = new String(requestLine.protocol, 0,
                requestLine.protocolEnd);

        //请求行无效的情况:没有请求的方式或没有请求的URI
        if (method.length() < 1) {
            throw new ServletException("Missing HTTP request method");
        } else if (requestLine.uriEnd < 1) {
            throw new ServletException("Missing HTTP request URI");
        }
        //判断和获取请求行中第二项中的请求参数,并获取到URI
        int question = requestLine.indexOf("?");
        if (question >= 0) {//有参数的
            //得到"?"后面的查询字符串:如userName=tarzan&password=pwd,并保存到HttpRequest对象中
            request.setQueryString(new String(requestLine.uri, question + 1,
                    requestLine.uriEnd - question - 1));
            //得到URI
            uri = new String(requestLine.uri, 0, question);
        } else {
            //没参数的
            request.setQueryString(null);
            uri = new String(requestLine.uri, 0, requestLine.uriEnd);
        }

        //这里的if语句用于请求的不是以/开头的相对资源,
        //即获取以绝对地址的请求方式的URI
        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);
                }
            }
        }

        //检查并解析第二项中的可能存在的 jsessionid
        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) {
                //将获取到的值放到HttpRequest对象中
                request.setRequestedSessionId(rest.substring(0, semicolon2));
                rest = rest.substring(semicolon2);
            } else {
                request.setRequestedSessionId(rest);
                rest = "";
            }
            //当 jsessionid 被找到,也意味着会话标识符是携带在查询字符串里边,而不是在 cookie里边。需要传递true
            request.setRequestedSessionURL(true);
            uri = uri.substring(0, semicolon) + rest;
        } else {
            request.setRequestedSessionId(null);
            request.setRequestedSessionURL(false);
        }

        //用于纠正“异常”的 URI。
        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 + "'");
        }
    }

    //纠正“异常”的 URI。例如,任何\的出现都会给/替代。
    //这里涉及到URL的编码解码:编码的格式为:%加字符的ASCII码的十六进制表示
    //如果URL不能纠正返回null,否则返回相同的或者被纠正后的 URI
    protected String normalize(String path) {
        if (path == null)
            return null;
        // Create a place for the normalized path
        String normalized = path;

        //如果URI是/~开头的,除去URI前面前四个字符并加上/~
        //%7E->~          
        if (normalized.startsWith("/%7E") || normalized.startsWith("/%7e"))
            normalized = "/~" + normalized.substring(4);

        //下面是解码后对应的结果,这些字符不能在URI中出现
        //%25->%   %2F->/  %2E->.  %5c->\
        //如果找到如下字符的其中一个,说明URI错误
        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;
        }
        //如果URI仅仅只是/.则返回/
        //如www.cnblogs.com/.是可以纠正的
        if (normalized.equals("/."))
            return "/";

        //将\转为/,这里的\\是指\,第一个\是转义字符
        if (normalized.indexOf('\\') >= 0)
            normalized = normalized.replace('\\', '/');
        //URI字符串如果没有以/开头就加给它
        if (!normalized.startsWith("/"))
            normalized = "/" + normalized;
        //如果存在//,将剩下/
        //如http://www.cnblogs.com/lzb1096101803/p//4797948.html变为
        //http://www.cnblogs.com/lzb1096101803/p/4797948.html
        while (true) {
            int index = normalized.indexOf("//");
            if (index < 0)
                break;
            normalized = normalized.substring(0, index)
                    + normalized.substring(index + 1);
        }
        //如果存在/./,变成/
        while (true) {
            int index = normalized.indexOf("/./");
            if (index < 0)
                break;
            normalized = normalized.substring(0, index)
                    + normalized.substring(index + 2);
        }
        //如果存在/../
        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);
        }
        //URI中存在/...或者3个点以上,认为不能纠正
        if (normalized.indexOf("/...") >= 0)
            return (null);

        //返回修改后的URI
        return (normalized);

    }

}

HttpProcessor.java详细说明:

(其实我更推荐看我注释的代码结合github上的源码进行分析,下面是书上的文字解释,可以帮助大家了解整个流程)

  • 解析请求行

    HttpProcessor 的 process 方法调用私有方法 parseRequest 用来解析请求行例如一个 HTTP请求的第一行。这里是一个请求行的例子:GET /myApp/ModernServlet?userName=tarzan&password=pwd HTTP/1.1请求行的第二部分是 URI 加上一个查询字符串。在上面的例子中,URI 是这样的:/myApp/ModernServlet

    另外,在问好后面的任何东西都是查询字符串。因此,查询字符串是这样的:userName=tarzan&password=pwd查询字符串可以包括零个或多个参数。在上面的例子中,有两个参数名 / 值对,userName/tarzan 和 password/pwd。在 servlet/JSP 编程中,参数名 jsessionid 是用来携带一个会话标识符。会话标识符经常被作为 cookie 来嵌入,但是程序员可以选择把它嵌入到查询字符串去,例如,当浏览器的 cookie 被禁用的时候。

    当 parseRequest 方法被 HttpProcessor 类的 process 方法调用的时候,request 变量指向一个 HttpRequest 实例。parseRequest 方法解析请求行用来获得几个值并把这些值赋给HttpRequest 对象。parseRequest 方法首先调用 SocketInputStream 类的 readRequestLine 方法,在这里 requestLine 是 HttpProcessor 里边的 HttpRequestLine 的一个实例.调用它的 readRequestLine 方法来告诉 SocketInputStream 去填入 HttpRequestLine 实例。接下去,parseRequest 方法获得请求行的方法,URI 和协议.不过,在 URI 后面可以有查询字符串,假如存在的话,查询字符串会被一个问好分隔开来。因此,parseRequest 方法试图首先获取查询字符串。并调用 setQueryString 方法来填充HttpRequest 对象.

    大多数情况下,URI 指向一个相对资源,URI 还可以是一个绝对值

    然后,查询字符串也可以包含一个会话标识符,用 jsessionid 参数名来指代。因此,parseRequest 方法也检查一个会话标识符。假如在查询字符串里边找到 jessionid,方法就取得会话标识符,并通过调用 setRequestedSessionId 方法把值交给 HttpRequest 实例当 jsessionid 被找到,也意味着会话标识符是携带在查询字符串里边,而不是在 cookie里边。因此,传递 true 给 request 的 setRequestSessionURL 方法。否则,传递 false 给setRequestSessionURL 方法并传递 null 给 setRequestedSessionURL 方法。   

    到这个时候,uri 的值已经被去掉了 jsessionid。接下去,parseRequest 方法传递 uri 给 normalize 方法,用于纠正“异常”的 URI。例如,任何\的出现都会给/替代。假如 uri 是正确的格式或者异常可以给纠正的话, normalize 将会返回相同的或者被纠正后的 URI。假如 URI 不能纠正的话,它将会给认为是非法的并且通常会返回null。在这种情况下(通常返回 null),parseRequest 将会在方法的最后抛出一个异常。

  • 解析头部

    parseHeaders 方法包括一个 while 循环用于持续的从 SocketInputStream 中读取头部,直到再也没有头部出现为止。循环从构建一个 HttpHeader 对象开始,并把它传递给类SocketInputStream 的 readHeader 方法.然后,你可以通过检测 HttpHeader 实例的 nameEnd 和 valueEnd 字段来测试是否可以从输入流中读取下一个头部信息

    一旦你获取到头部的名称和值,你通过调用 HttpRequest 对象的 addHeader 方法来把它加入headers 这个 HashMap 中。

    一些头部也需要某些属性的设置。例如,当 servlet 调用 javax.servlet.ServletRequest的 getContentLength 方法的时候, content-length 头部的值将被返回。而包含 cookies 的 cookie头部将会给添加到 cookie 集合中。

RequestUtil.java

功能:解析cookie

cookie格式如下:Cookie: userName=budi; password=pwd

解析cookie过程其实只是简单的切割字符串然后将key value放入一个cookie对象中,因为不是很关键的代码。

SocketInputStream.java

上一节没有试图为那两个应用程序去进一步解析请求。org.apache.catalina.connector.http.SocketInputStream 提供的方法不仅用来获取请求行,还有请求头部。通过传递一个 InputStream 和一个指代实例使用的缓冲区大小的整数,来构建一个 SocketInputStream 实例。

对于如何SocketInputStream中如何做到解析请求头和请求行不用太care,学习tomcat的核心注意是对整个框架有个了解,不用关心太过细的地方,

我认为只要清楚请求中大部分信息的解析流程就足够了。

第三部分:小结

下一节补充Tomcat是如何在请求到达是才获取请求参数的。

因为对Tomcat而言,它不需要马上解析查询字符串或者 HTTP 请求内容,直到 servlet 才读取参数。因此,HttpRequest 的这四个方法开头调用了 parseParameter 方法。参数解析将会使得 SocketInputStream 到达字节流的尾部。类 HttpRequest 使用一个布尔变量 parsed 来指示是否已经解析过了。

同时也加入关于Tomcat错误信息获取和其中涉及到的国际化的内容。

相应代码可以在我的github上找到下载,拷贝到eclipse,然后打开对应包的代码即可。

如发现编译错误,可能是由于jdk不同版本对编译的要求不同导致的,可以不管,供学习研究使用。

posted @ 2015-09-11 19:22  多啦A  阅读(3058)  评论(0编辑  收藏  举报