学习tomcat源码(3) 连接器

连接器涉及到的类比较多,绕来绕去最后还是绕出来了:

Catalina中有两个主要的模块:连接器和容器。连接器必须创建javax.servlet.http.HttpServletRequest和javax.servlet.http.HttpServletResponse,并传递给被调用的servlet的service方法,上一篇中,servlet容器只可以运行实现了javax.servlet.Servlet的servlet,并传递 javax.servlet.ServletRequest和javax.servlet.ServletResponse实例给service方法。因为连接器并不知道servlet的类型(例如它是否实现了javax.servlet.Servlet,继承了javax.servlet.GenericServlet,或者继javax.servlet.http.HttpServlet),所以连接器必须始终提供HttpServletRequest和HttpServletResponse的实例。

接下来要实现的程序中,连接器解析HTTP请求头部并让servlet可以获得头部, cookies, 参数名/值等等。并完善上一节中Response类的getWriter方法,让它能够正确运行。由于这些改进,将会从 PrimitiveServlet中获取一个完整的响应,并能够运行更加复杂的ModernServlet。

先来了解一个类:

StringManager类:

Tomcat在一个属性文件里边存储错误信息。Tomcat中有数以百计的类。把所有类使用的错误信 息存储到一个大的属性文件里边将会容易产生维护的噩梦。为了避免这一情况,Tomcat为每个包都分配一个属性文件。例如,在包 org.apache.catalina.connector里边的属性文件包含了该包所有的类抛出的所有错误信息。每个属性文件都会被一个 org.apache.catalina.util.StringManager类的实例所处理。当Tomcat运行时,将会有许多 StringManager实例,每个实例会读取包对应的一个属性文件。此外,由于Tomcat的受欢迎程度,提供多种语言的错误信息也是有意义的。目前,有三种语言是被支持的。英语的错误信息属性文件名为LocalStrings.properties。另外两个是西班牙语和日语,分别放在 LocalStrings_es.properties和LocalStrings_ja.properties里边。 当包里边的一个类需要查找放在该包属性文件的一个错误信息时,它首先会获得一个StringManager实例。不过,相同包里边的许多类可能也需要 StringManager,为每个对象创建一个StringManager实例是一种资源浪费。因此,StringManager类被设计成一个StringManager实例可以被包里边的所有类共享。StringManager是一个单例 (singleton)类。仅有的一个构造方法是私有的,所有不能在类的外部使用new关键字来实例化。通过传递一个包名来调用它的公共静态方法 getManager来获得一个实例。每个实例存储在一个以包名为键(key)的Hashtable中。

   // 保证一个包一个StringManager,私有化构造函数+Hashtable维护实现
138	    // 从而避免大量的StringManager实例化和销毁的操作,毕竟写日志属于比较频繁的操作。
139	    public synchronized static StringManager getManager(String packageName) {
140	        // 用一个Hashtable来管理控制,保证每个包提供一个StringManager
141	        StringManager mgr = (StringManager)managers.get(packageName);
142	        // 属于这个包的Manager有了吗
143	        if (mgr == null) {
144	            mgr = new StringManager(packageName);
145	            // 实例化好后,把它放进Hashtable里去,下次就不用实例化了
146	            managers.put(packageName, mgr);
147	        }
148	        return mgr;
149	    }
150	}



该应用程序由三个模块组成:connector, startup和core。 startup模块只有一个类,Bootstrap,用来启动应用的。connector模块的类可以分为五组:
 连接器和它的支撑类(HttpConnector和HttpProcessor)。
 指代HTTP请求的类(HttpRequest)和它的辅助类。
 指代HTTP响应的类(HttpResponse)和它的辅助类。

 Facade类(HttpRequestFacade和HttpResponseFacade)。
 Constant类
core模块由两个类组成:ServletProcessor和StaticResourceProcessor。

上一节中的HttpServer类的职责是等待HTTP请求并创建请求和响应对象。在这节应用中,等待HTTP请求的工作交给HttpConnector实例,而创建请求和响应对象的工作交给了HttpProcessor实例。 HTTP请求对象由实现了javax.servlet.http.HttpServletRequest的HttpRequest类来代表。一个 HttpRequest对象将会给转换为一个HttpServletRequest实例并传递给被调用的servlet的service方法。因此,每个 HttpRequest实例必须适当增加字段,以便servlet可以使用它们。值需要赋给HttpRequest对象,包括URI,查询字符串,参数,cookies和其他的头部等等。因为连接器并不知道被调用的servlet需要哪个值,所以连接器必须从HTTP请求中解析所有可获得的值。不过,解析一个HTTP请求牵涉昂贵的字符串和其他操作,假如只是解析servlet需要的值的话,连接器就能节省许多CPU周期。例如,假如servlet不 解析任何一个请求参数(例如不调用javax.servlet.http.HttpServletRequest的getParameter, getParameterMap,getParameterNames或者getParameterValues方法),连接器就不需要从查询字符串或者 HTTP请求内容中解析这些参数。Tomcat的默认连接器(和本章应用程序的连接器)试图不解析参数直到servlet真正需要它的时候,通过这样来获得更高效率。


Tomcat的默认连接器和连接器使用SocketInputStream类来从套接字的InputStream中读取字节流。一个 SocketInputStream实例对从套接字的getInputStream方法中返回的java.io.InputStream实例进行包装。 SocketInputStream类提供了两个重要的方法:readRequestLine和readHeader。readRequestLine返回一个HTTP请求的第一行。例如,这行包括了URI,方法和HTTP版本。因为从套接字的输入流中处理字节流意味着只读取一次,从第一个字节到最后一个字节(并且不回退),因此readHeader被调用之前,readRequestLine必须只被调用一次。readHeader每次被调用来获得一个头部的名/值对,并且应该被重复的调用知道所有的头部被读取到。readRequestLine的返回值是一个HttpRequestLine的实例,而 readHeader的返回值是一个HttpHeader对象。


HttpConnector类指代一个连接器,职责是创建一个服务器套接字用来等待前来的HTTP请求。HttpConnector类实现了Runnable接口,所以它能被它自己的线程专用。当启动应用程序,一个HttpConnector的实例被创建,并且它的run方法被执行。

package 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";

  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类的process方法接受前来的HTTP请求的套接字,会做下面的事情: 

1. 创建一个HttpRequest对象。

 2. 创建一个HttpResponse对象。

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

 4. 解析HttpRequest和HttpResponse对象到一个ServletProcessor或者 StaticResourceProcessor。ServletProcessor调用被请求的servlet的service方 法,而StaticResourceProcessor发送一个静态资源的内容。

解析请求行:

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来嵌入

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 + "'");
    }
  }

修正URI:

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);
  }




解析头部  cookies:

一个HTTP头部是用类HttpHeader来代表:
 可以通过使用类的无参数构造方法构造一个HttpHeader实例。
 一旦拥有一个HttpHeader实例,可以把它传递给SocketInputStream的readHeader方法。假如这里有头部需要读取,readHeader方法将会相应的填充HttpHeader对象。假如再也没有头部需要读取了,HttpHeader实例的nameEnd和valueEnd字段将会置零。
 为了获取头部的名称和值,使用下面的方法:
 String name = new String(header.name, 0, header.nameEnd);
 String value = new String(header.value, 0, header.valueEnd);
parseHeaders方法包括一个while循环用于持续的从SocketInputStream中读取头部,直到再也没有头部出现为止。


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
  }



获取参数

不需要马上解析查询字符串或者HTTP请求内容,直到servlet需要通过调用javax.servlet.http.HttpServletRequest的getParameter, getParameterMap, getParameterNames或者getParameterValues方法来读取参数。因此,HttpRequest的这四个方法开头调用了parseParameter方法。 这些参数只需要解析一次就够了,因为假如参数在请求内容里边被找到的话,参数解析将会使得SocketInputStream到达字节流的尾部。类HttpRequest使用一个布尔变量parsed来指示是否已经解析过了。

 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;
  }

最后就是Response类了,现在不想写,明天继续。


  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值