2017-09-25 16:08:37 +0000   |     web java how tomcat works socket io tcp http connector   |   Viewed times   |    

摘要

第三章主要演示了 连接器。连接器的主要职责有3:

  1. 监听客户端的TCP连接请求。创建 已连接套接字 Socket实例
  2. 解析HTTP消息
  3. 构造好一对I/O流HttpServletRequestHttpServletResponse实例

这一章,这3个任务分别由三个类完成,

  1. HttpConnector监听客户端的TCP连接请求
  2. HttpProcessor解析HTTP消息,构造并填充HttpRequestHttpResponse实例
  3. HttpRequestHttpResponse类分别实现了HttpServletRequestHttpServletResponse接口

其中有2个比较重要的点:

  1. 怎么解析HTTP消息?实际由现成的轮子org.appache.catalina.connector.http.SocketInputStream完成
  2. 怎么把一个PrintWriter封装到HttpResponse里,并且让这个PrintWriterwrite()方法能接收char[]字符流,然后输入byte[]字节流。可以用OutputStreamWriter做中介。

一个连接器的职责边界就在于构造好一对I/O流HttpServletRequestHttpServletResponse。再往后调用Servlet#service(ServletRequest request, ServletResponse response)之后就进入了 Servlet容器 的职责范围。

本章Demo全部代码,在我的Github可以找到 -> https://github.com/helloShen/HowTomcatWorks/tree/master/solutions/src/com/ciaoshen/howtomcatworks/ex03

HttpConnector监听客户端的TCP连接请求

HttpConnector的职责很简单,就是创建一个服务器监听套接字java.net.ServerSocket实例,调用它的accept()函数接受客户端的连接请求,创建一个已连接套接字java.net.Socket的实例。然后把这个实例的引用交给HttpProcessor类去处理Request和Response。

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

需要注意HttpConnector有自己独立线程,实现了Runnable接口。但并发的好处在第三章还看不出来。在第四章引入HttpProcessor对象池以后,每个HttpProcessor也有自己的独立线程,HttpConnector调用HttpProcessor#assign(Socket)方法把Socket实例引用交给HttpProcessor之后马上返回不阻塞,就可以继续处理下一个连接请求。达到异步同时处理多个客户端请求的效果。

HttpProcessor解析HTTP消息

HttpProcessor是本章的重头戏。它负责解析HTTP请求消息,把解析后的数据赋值给一个HttpRequest实例。然后再构造一个HttpResponse实例。

动态加载Servlet的class文件以后,运行Servlet需要的就是这一对I/O。

回顾HTTP消息

一个HTTP消息如下,

### 请求行
GET /servlet/ModernServlet?userName=tarzan&password=pwd HTTP/1.1

### 消息头
Server: Microsoft-IIS/4.0
Date: Mon, 5 Jan 2004 13:13:12 GMT
Content-Type: text/html
Last-Modified: Mon, 5 Jan 2004 13:13:12 GMT
Content-Length: 11

### 消息正文
... ...

主要分为3部分,

  1. 请求行:请求消息的第一行。
    • method: GET
    • URI: /servlet/ModernServlet?userName=tarzan&password=pwd
    • protocol: HTTP/1.1
  2. 消息头:一系列表示元信息的“键-值”对
  3. 消息正文:在一个空行之后,是消息内容正文

现成的轮子 org.appache.catalina.connector.http.SocketInputStream

HttpProcessor没有自己造轮,而是利用现成的轮子org.appache.catalina.connector.http.SocketInputStream解析HTTP消息。

本地的com.ciaoshen.howtomcatworks.ex03.connector.SocketInputStreamorg.appache.catalina.connector.http.SocketInputStream的副本。

首先调用SocketInputStream#readRequestLine()把第一行 “请求行” 切成3段。完整的请求行如下,

GET /servlet/ModernServlet?userName=tarzan&password=pwd HTTP/1.1

切成3段后,

解析后的数据被封装在一个HttpRequestLine类实例中。

private HttpRequestLine requestLine = new HttpRequestLine();

// ... ... omitted code

input.readRequestLine(requestLine);

然后调用SocketInputStream#readHeader()解析第二部分 “消息头”。消息头的本质就是一系列以冒号:分割的属性键-值对。每一个键值对都储存在HttpHeader类的实例里。

### 键-值对1
Server: Microsoft-IIS/4.0
### 键-值对2
Date: Mon, 5 Jan 2004 13:13:12 GMT
### 键-值对3
Content-Type: text/html
### 键-值对4
Last-Modified: Mon, 5 Jan 2004 13:13:12 GMT
### 键-值对5
Content-Length: 11

SocketInputStream解析后的数据赋值给HttpRequest实例

解析出来的数据,无论是HttpRequestLine还是HttpHeader,最后都被逐一赋值到HttpRequest实例里。 在HttpRequest类里有专门的字段来存放请求行的method,uriprotocol,还有一个HashMap专门用来储存消息头里的键值对。

public class HttpRequest implements HttpServletRequest {

    private String contentType;
    private int contentLength;
    private InetAddress inetAddress;
    private InputStream input;
    private String method;              // 请求行的method
    private String protocol;            // 请求行的protocol
    private String queryString;         // 请求行的uri的后半部分
    private String requestURI;          // 请求行uri的前半部分
    private String serverName;
    private int serverPort;
    private Socket socket;
    private boolean requestedSessionCookie;
    private String requestedSessionId;
    private boolean requestedSessionURL;

    // ... ... omitted code

    protected HashMap headers = new HashMap();  // 请求头的键-值对

    // ... ... omitted code
}

HTTP消息的数据解析完赋值给HttpRequest,当ModernServlet拿到HttpRequest实例的引用之后,就是很简单地把各部分数据打印出来。

HttpResponse里封装PrintWriter

前面的工作HttpProcessor成功解析了Http请求消息,把解析后的数据赋值给了HttpRequest实例。接下来要构造HttpResponse实例。

HttpResponse只能通过getWriter()方法拿到PrintWriter实例引用

HttpProcessor类在构造HttpResponse的时候就传入了Socket#OutputStream

public class HttpProcessor {

        // omitted code ... ...
    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);

          // omitted code ... ...

        } catch (Exception e) {
            e.printStackTrace();
        }
    }      

    // omitted code ... ...

}

但Servlet程序是不能直接访问这个OutputStream类型字段的。HttpResponse实现HttpServletResponse接口。HttpServletResponse接口继承自ServletResponse接口。所以只能通过getWriter()方法可以拿到内部PrintWriter实例的引用,作为想客户端传送输出流的出口。

public class PrimitiveServlet implements Servlet {

  // omitted code ... ...

  public void service(ServletRequest request, ServletResponse response)
    throws ServletException, IOException {
    /** 通过getWriter()方法拿大PrintWriter实例引用 */
    PrintWriter out = response.getWriter();

    /** 拿到PrintWriter以后,直接向客户端浏览器输出 */
    out.println("HTTP/1.1 200 OK");
    out.println("");
    out.println("Hello. Roses are red.");
    out.print("Violets are blue.");
  }

  // omitted code ... ...

}

PrintWriter输出字节流

PrintWriter继承自Writer,是面向字符流的输出。但HTTP协议需要传输的数据大部分是像图片这样的纯字节流。要让PrintWriter能传输字节流,需要利用利用OutputStreamWriter为中介,先将面向字节流的OutputStream封装到一个OutputStreamWriter实例里,然后再把这个OutputStreamWriter实例封装到一个PrintWriter实例里返回。这样,PrintWriter#write()方法实际调用的会是OutputStream#write()方法。

最简单的做法是像第二章Response#getWriter()方法这样,用直接用一个OutputStream实例构造PrintWriter

class Response implements ServletResponse {

    private static final int BUFFER_SIZE = 1024;
    Request request;
    OutputStream output;
    PrintWriter writer;

    // omitted code ... ...

    public PrintWriter getWriter() throws IOException {
        /**
         * autoflush is true, println() will flush,
         * but print() will not.
         * output is an instance of OutputStream
         */
        writer = new PrintWriter(output, true);
        return writer;
    }

    // omitted code ... ...
}

因为PrintWriter(OutputStream out)构造器会再调用另一个有两个参数的构造器,在构造PrintWriter之前,会先用OutputStreamWriterBufferedWriter装饰,

public PrintWriter(OutputStream out, boolean autoFlush) {
    /** out is an instance of OutputStream */
    this(new BufferedWriter(new OutputStreamWriter(out)), autoFlush);
    if (out instanceof java.io.PrintStream) {
        psOut = (PrintStream) out;
        // omitted code ... ...
    }
    // omitted code ... ...
}

这样getWriter()函数拿到的PrintWriter实例就可以输出字节流了。

但第三章的getWriter()方法要更复杂一点。先用HttpResponse构造一个ResponseStream实例。ResponseStream实际继承自OutputStream类,它的作用是强制在调用OutputStream#write()方法之前检查HttpResponse消息长度。

然后ResponseStream再通过OutputStreamWriter做为过度,构造出ResponseWriter实例。ResponseWriter就继承自PrintWriter类。装饰一层ResponseWriter的目的是为了让每个print()println()方法都自动调用flush()方法。

/**
 * 利用OutputStreamWriter为中介,将面向字节流的OutputStream封装在PrintWriter实例里返回
 */
public PrintWriter getWriter() throws IOException {
  /** "this" is an instance of HttpResponse */
  ResponseStream newStream = new ResponseStream(this);
  newStream.setCommit(false);
  OutputStreamWriter osr = new OutputStreamWriter(newStream, getCharacterEncoding());
  writer = new ResponseWriter(osr);
  return writer;
}

Servlet容器

本章有两个Servlet容器实现类。一个和第二章一样相对简单的PrimitiveServlet,一个是这一章的ModernServletPrimitiveServlet直接实现了Servlet接口。直接在service()方法里提供服务逻辑。ModernServlet继承了HttpServlet骨架实现类,service()方法被架空,实际服务在doGet()方法里提供。

加载Servlet容器

ServletProcessor类还是用URLClassLoader在运行时动态加载PrimitiveServletModernServlet类,用Class#newInstance()方法创建实例,并调用此实例的service()入口方法。

HttpServlet.service()架空的入口

基本的Servlet的服务可以像PrimitiveServlet这样,直接在主入口service()方法里提供服务。

public class PrimitiveServlet implements Servlet {

  // omitted code ... ...

  public void service(ServletRequest request, ServletResponse response)
    throws ServletException, IOException {
    System.out.println("Call PrimitiveServlet Service!");
    PrintWriter out = response.getWriter();
    /** HttpResponse里什么也没有,输出信息全靠硬编码打印 */
    out.println("HTTP/1.1 200 OK");         // 不加回应头浏览器会默认HTTP/0.9而报错
    out.println("");
    out.println("Hello. Roses are red.");
    out.print("Violets are blue.");
  }

  // omitted code ... ...
}

需要注意!PrimitiveServlet里不能直接向浏览器输出文本,至少要加上一个回应头,标明HTTP协议版本HTTP/1.1,否则多数浏览器会默认使用的是HTTP/0.9而报错。

out.println("HTTP/1.1 200 OK");

HttpServlet类比较特别。它是一个骨架实现,它的主入口service()函数,会根据传入的客户请求类型调用不同的方法。 具体就是看传入的ServletRequest.method字段,如果是GET,就调用doGet()函数,如果是POST就调用doPost()函数,等等。所以继承HttpServlet类,不需要重写service()函数,而是相应的重写doGet()或者doPost()等方法。

ModernServlet继承的就是HttpServlet

public class ModernServlet extends HttpServlet {

    // omitted code ... ...

    public void doGet(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
        // some code here ... ...
    }

    // omitted code ... ...
}

关于StringManager

Tomcat用StringManager实例来处理错误消息。方法是将错误消息储存在多个扩展名为.properties文件中。具体是每个包有一个.properties文件储存这个包内的类产生的错误消息。每个.properties文件由一个StringManager实例管理。

一共有3个地方用到了StringManager,分别是:

通过getManager()方法获得StringManager实例的引用。传进去的参数Constants.PACK_HTTP是下面例子里调用StringManager的HttpProcessor的完整包名:com.ciaoshen.howtomcatworks.ex03.connector.http

public class HttpProcessor {

    // omitted code ... ...

    /** The string manager for this package. */
    protected StringManager sm = StringManager.getManager(Constants.PACK_HTTP);
}

构造StringManager需要3个东西,

  1. 包名(baseName): getManager()的参数提供。
  2. 语言/地域(Local): getManager()会调用java.util.Locale#getDefault() Locale.getDefault()方法获得。
  3. 加载器(loader): java.lang.ClassLoader#getResource这里不是加载一个类,只是加载文件。

.properties文件的路径和文件名就主要由“包名”加上”地域”加上后缀组成,

[classpath]/[basename]/LocalStrings_[local].properties

比如我com.ciaoshen.howtomcatworks.ex03.connector.http包里的的.properties文件绝对路径和文件名就是:

/Users/Wei/github/HowTomcatWorks/solutions/bin/com/ciaoshen/howtomcatworks/ex03/connector/http/LocalStrings.properties

其中:

实际我http包下有3个.properties文件,

后两个分别是西班牙语和日语的版本。但最基本的只需要有LocalStrings.properties就可以保证正常工作,因为StringManager加载.properties的工作是由java.util.ResourceBundle完成,它会调用java.util.Locale#getDefault()Locale.getDefault()方法找到我系统的国家和语言参数。比如我在加拿大魁北克,使用法语,系统的Local参数就是fr_CA。但这不是说我就必须有LocalStrings_fr_CA.properties. 因为ResourceBundle的算法是列出一系列的候选文件名单,如果能找到其中的一个,就正常加载。如果一个都没找到,最后保底会去找默认的LocalStrings.properties。如果连LocalStrings.properties都没有,才会抛出java.util.MissingResourceException

Exception in thread "Thread-0" java.util.MissingResourceException: Can't find bundle for base name com.ciaoshen.howtomcatworks.connector.http.LocalStrings, locale fr_CA
	at java.util.ResourceBundle.throwMissingResourceException(ResourceBundle.java:1564)
	at java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:1387)
	at java.util.ResourceBundle.getBundle(ResourceBundle.java:773)
	at org.apache.catalina.util.StringManager.<init>(StringManager.java:107)
	at org.apache.catalina.util.StringManager.getManager(StringManager.java:252)
	at com.ciaoshen.howtomcatworks.ex03.connector.http.HttpProcessor.<init>(HttpProcessor.java:50)
	at com.ciaoshen.howtomcatworks.ex03.connector.http.HttpConnector.run(HttpConnector.java:45)
	at java.lang.Thread.run(Thread.java:745)

上面的错误就是我在http包下没有LocalStrings.properties文件。创建这个文件以后就正常运行。

关于HTTP/1.1,必须计算消息正文的长度

HTTP/1.1版本开始,传输数据需要准确计算好消息正文的大小,把长度放在消息头的content-length段告诉浏览器。

下面书中默认的sendStaticResource()函数传输静态网页,没有加请求行,默认使用HTTP/0.9。但现在市面上的浏览器已经不支持HTTP/0.9。所以必须在请求行HTTP/1.1 200 OK指定使用HTTP/1.1。但HTTP/1.1版如果没有content-length消息头,浏览器默认是持久链接,图片传输完毕不关闭链接,没有长度提示,浏览器不知道图片是否完整传输,所以图片无法正常显示。

public void sendStaticResource() throws IOException {
  byte[] bytes = new byte[BUFFER_SIZE];
  FileInputStream fis = null;
  try {
    // 书上默认HTTP/0.9版,并没有请求行。但加上请求行里注明HTTP/1.1后,没有"content-length"头信息,导致浏览器无法判断消息正文结尾。因为HTTP/1.1开始默认的是持久链接。
    output.write("HTTP/1.1 200 OK\r\n".getBytes());
    File file = new File(Constants.WEB_ROOT, request.getRequestURI());
    fis = new FileInputStream(file);
    System.out.println("Static Resource File: " + file.getPath());
    // 没有消息头,浏览器无法知道图片传输完毕,无法正常显示图片

    // 空一行直接消息正文
    output.write("\n\r".getBytes());
    int ch = fis.read(bytes, 0, BUFFER_SIZE);
    while (ch!=-1) {
      output.write(bytes, 0, ch);
      System.out.println("\nLength = " + ch + "\n");
      System.out.write(bytes,0,ch);
      ch = fis.read(bytes, 0, BUFFER_SIZE);
    }
  } catch (FileNotFoundException e) {
    System.out.println(e);    
  }
  // omitted code ... ...
}

下面计算好图片大小,并且在消息头里告诉浏览器,就可以正常显示图片,

public void sendStaticResource() throws IOException {
  byte[] bytes = new byte[BUFFER_SIZE];
  FileInputStream fis = null;
  try {
    /*********************************************************
     * 读取文件
     ********************************************************/
    File file = new File(Constants.WEB_ROOT, request.getRequestURI());
    fis = new FileInputStream(file);
    /*********************************************************
     * 先缓存所有正文,并计算长度长度。
     ********************************************************/
    int MAX_INFO = 4096;
    int totalLen = 0; // 资源长度
    byte[] info = new byte[MAX_INFO]; // 缓存
    int count = fis.read(bytes, 0, BUFFER_SIZE);
    while (count != -1) {
        for (int i = 0; i < count; i++) {
            if (totalLen == info.length) { info = Arrays.copyOf(info,info.length * 2); }
            info[totalLen++] = bytes[i];
        }
        count = fis.read(bytes, 0, BUFFER_SIZE);
    }
    /*********************************************************
     * 编辑带有"状态行"和"响应头"的HTTP头
     ********************************************************/
    String httpHeader = "HTTP/1.1 200 OK\r\n" +
        "Server: SHEN's First Java HttpServer\r\n" +
        "Content-Type: text/html\r\n" +
        "Content-Length: " + String.valueOf(totalLen) + "\r\n" +
        "\r\n"; // 将消息正文长度告知浏览器

    /*********************************************************
     * 写HTTP头
     ********************************************************/
    output.write(httpHeader.getBytes());
    output.write(info, 0, totalLen);
  } catch (FileNotFoundException e) {
    System.out.println(e);    
  }
// omitted code ... ...
}

项目中已经弃用的类

最初版本的Tomcat时间已经有点久远,例子中的很多类现在已经被弃用。这些类包括:

另外编译的时候如果用javac -Xlint会有很多泛型的rawtypes警告。选择忽略即可。