第三章 连接器

Catalina有两个主要的模块 Connector 和 Container,在本章的程序中所要建立的连接器是 Tomcat 4 中的默认连接器的简化版

3.1 StringManage

Tomcat 将存储错误消息的 properties 文件划分到不同的包中,每个 properties 文件都是使用 org.apache.catalina.util.StringManager 类的一个实例进行处理。同时 StringManager 是单例模式的,避免空间的浪费。

3.2 应用程序

本章的应用程序包含三个模块:

  • 启动模块:负责启动应用程序。
  • 连接器模块:
    • 连接器及其支持类(HttpConnector 和 HttpProcessor);
    • HttpRequest 类和 HttpResponse 类及其支持类;
    • 外观类(HttpRequesFaced 类和 HttpResponseFaced);
    • 常量类。
  • 核心模块:
    • ServletProcessor : 处理 servlet 请求(类似 Wrapper);
    • StaticResourceProcessor : 处理静态资源请求。
本章应用程序的 UML 类图

3.2.1 启动应用程序

  public final class Bootstrap {
    public static void main(String[] args) {
      //创建 HttpConnector 实例
      HttpConnector connector = new HttpConnector();
      //启动 Connector
      connector.start();
    }
  }

3.2.2 HttpConnector 类

Connector 不知道 servlet 接受 Request 和 Response 的具体类型,所以使用 HttpServletRequest 和 HttpResponse 进行传参。同时解析 HTTP 请求对参数是懒加载的,在这些参数被 servlet 实例真正调用前是不会进行解析的。
Tomcat 的默认连接器和本章程序的 Connector 都使用 SocketInputStream 获取字节流。SocketInputStream 是 InputStream 的包装类,提供了两个重要的方法:

  • readRequestLine:获取请求行,包括 URI、请求方法和 HTTP 版本信息。
  • readHead: 获取请求头,每次调用 readHead 方法都会返回一个键值对,可以通过遍历读取所有的请求头信息。
public class HttpConnector implements Runnable {
  boolean stopped;
  private String scheme = "http";
  public void start() {
    //启动一个新的线程,调用自身的 run 方法
    Thread thread = new Thread(this);
    thread.start();
  }
  public void run() {
    //为减少文章篇幅省略 try catch 语句块
    //创建 ServerSocket 
    ServerSocket serverSocket = null;
    int port = 8080;
      serverSocket =  new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));
  
    while (!stopped) {
      // 等待连接请求
      Socket socket = null;
      socket = serverSocket.accept();
      // 为当前请求创建一个 HttpProcessor 
      HttpProcessor processor = new HttpProcessor(this);
      //调用 process 方法处理请求
      processor.process(socket);
    }
  }
HttpProcessor 类

HttpProcessor 的 process 方法对每个传入的 HTTP 请求,要完成4个操作:

  • 创建一个 HttpRequest 对象;
  • 创建一个 HttpResponse 对象;
  • 解析 HTTP 请求的第一行内容和请求头信息,填充 HttpRequest 对象;
  • 将 HttpRequest 和 HttpResponse 传递给 ServletProcessor 或 Static-ResourceProcessor 的 process 方法。
public void process(Socket socket) {
      SocketInputStream input= new SocketInputStream(socket.getInputStream(), 2048);
      OutputStream output = socket.getOutputStream();

      //创建 HttpRequest 和 HttpResponse 对象
      request = new HttpRequest(input);
      response = new HttpResponse(output);
      response.setRequest(request);
      //向客户端发送响应头信息
      response.setHeader("Server", "Pyrmont Servlet Container");
      //解析请求行
      parseRequest(input, output);
      //解析请求头
      parseHeaders(input);

      //根据请求的 URI 模式判断处理请求的类
      if (request.getRequestURI().startsWith("/servlet/")) {
        ServletProcessor processor = new ServletProcessor();
        processor.process(request, response);
      }
      else {
        StaticResourceProcessor processor = new StaticResourceProcessor();
        processor.process(request, response);
      }  
  }

parseRequest 方法解析请求行:
处理 URI 字符串 --> 绝对路径检查 --> 会话标识符检查 --> URI 修正 --> 设置 request 属性

private void parseRequest(SocketInputStream input, OutputStream output) {
    // requestLine 是 HttpRequestLine 的实例
    //使用 SocketInputStream 中的信息填充 requestLine,并从 requestLine 中获取请求行信息
    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
    //验证输入的 request line 是否合法, 略
    ...

    //处理 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);
    }
    //检查是否是绝对路径
    if (!uri.startsWith("/")) {
      int pos = uri.indexOf("://");
      // 获取 protocol 的类型
      if (pos != -1) {
        pos = uri.indexOf('/', pos + 3);
        if (pos == -1) {
          uri = "";
        }
        else {
          uri = uri.substring(pos);
        }
      }
    }

    // 检查查询字符串是否还携带会话标识符
    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);
    }

    //对 URI 进行修正转化为合法的 URI,如将 "\" 替换成 "/"
    String normalizedUri = normalize(uri);

    // 属性 request 的属性
    ((HttpRequest) request).setMethod(method);
    request.setProtocol(protocol);
    if (normalizedUri != null) {
      ((HttpRequest) request).setRequestURI(normalizedUri);
    }
    else {
      ((HttpRequest) request).setRequestURI(uri);
    }
  }

parseHeaders 方法解析请求头:
获取下一个 header --> 如果 header name 和 value 为空则退出循环 --> 从 header 中获取 key/value 并放入 request --> 对 cookie 和 content-type 做处理 --> 循环

private void parseHeaders(SocketInputStream input) {
    while (true) {
      HttpHeader header = new HttpHeader();
      // 获取下一个 header
      input.readHeader(header);
      if (header.nameEnd == 0) {
        if (header.valueEnd == 0) {
          return;
        }
        else {
          throw new ServletException ();
        }
      }
      
      // 从 header 中获取请求头的 key/value,并存入 request 中
      String name = new String(header.name, 0, header.nameEnd);
      String value = new String(header.value, 0, header.valueEnd);
      request.addHeader(name, value);
      // 对一些特别的 header 做处理
      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;
        n = Integer.parseInt(value);
        request.setContentLength(n);
      }
      else if (name.equals("content-type")) {
        request.setContentType(value);
      }
    } 
  }

获取参数:

  • 在获取参数前会调用 HttpRequest 的 parseParameter 方法解析请求参数。参数只需解析一次也只会解析一次。
  • 参数存储在特殊的 hashMap 中: ParameterMap
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;
  }

4
4
4
4
4
4
4
4
4
4

4

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容