Tomcat剖析(一):一个简单的Web服务器

Tomcat剖析(一):一个简单的Web服务器

第一部分:概述

这一节基于《深度剖析Tomcat》第一章:一个简单的Web服务器 总结而成。

对Tomcat而言,如果直接对其源码进行分析是困难的,所以这里被设计得足够简单使得你能理解一个 servlet 容器是如何工作的,没有对Tomcat本身的连接器和容器进行分析,本节旨在明白服务器的整个流程大致是如何进行的。需要知道如何完善Web服务器,可以看下一节。

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

文中详细说明是从书中相关章节中copy下来的(因为书中讲得很清楚,不用再进行更细致的讲解) 注释是为了便于理解自己添加或进行简单翻译的,其它部分也是自己写的。

总的来说,一个简单的Web服务器的流程是这样的:

  1. Server创建一个serverSocket对象,等待Client发送请求
  2. Client发送请求后,Server获取用户socket,从而得到请求的输入输出流
  3. 从输入输出流中创建request和response对象
  4. 解析request,同时response设置静态资源
  5. 关闭用户socket
  6. 从request中获取URI,判断文件是否存在,如果不存在就响应404,如果是就关闭服务器,否则将对应的文件内容响应给浏览器写入页面
  7. 判断URI是否为u/SHUTDOWN,如果不是则重新进入等待请求状态,回到第2步,否则关闭服务器。

第二部分:代码讲解

HttpServer.java

package ex01.pyrmont;

import java.net.Socket;
import java.net.ServerSocket;
import java.net.InetAddress;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.io.File;

public class HttpServer {

    // 获取项目webroot目录的实际物理路径,判断目标文件是否存在
    public static final String WEB_ROOT = System.getProperty("user.dir")
            + File.separator + "webroot";

    // 用于判断是否需要关闭服务器,默认是false
    private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
    private boolean shutdown = false;

    public static void main(String[] args) {
        HttpServer server = new HttpServer();
        server.await();
    }

    public void await() {
        ServerSocket serverSocket = null;
        int port = 8080;
        try {
            //创建服务器端的ServerSocket
            serverSocket = new ServerSocket(port, 1,
                    InetAddress.getByName("127.0.0.1"));
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }

        //进入死循环,直到shutdown==/SHUTDOWN
        while (!shutdown) {
            Socket socket = null;
            InputStream input = null;
            OutputStream output = null;
            try {
                socket = serverSocket.accept();//Server一直等待直到Client发送请求
                input = socket.getInputStream(); //接收请求后获取输入输出流
                output = socket.getOutputStream();

                //创建request对象,传入input参数用于获取输入流的参数
                Request request = new Request(input);
                request.parse(); //解析request对象,这一节只是获取请求中请求行的URI

             //创建response对象,传入output对象用于获取Writer对象将响应内容写到浏览器
                Response response = new Response(output);
             //设置request,用于sendStaticResource()方法获取URI判断WEB_ROOT下是否存在目标文件
                response.setRequest(request);
                response.sendStaticResource();//如果请求不存在就发送404错误,否则写入文件内容

                //关闭用户socket
                socket.close();

                //如果URI是/SHUTDOWN说明需要关闭服务器
                shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
            } catch (Exception e) {
                e.printStackTrace();
                continue;
            }
        }
    }
}

HttpServer.java详细说明:

  • await 方法首先创建一个 ServerSocket 实例然后进入一个 while 循环
  • while 循环里边的代码运行到 ServletSocket 的 accept 方法停了下来,只会在 8080 端口接 收到一个 HTTP 请求的时候才返回:
  • 接收到请求之后,await方法从accept方法返回的Socket实例中取得java.io.InputStream 和 java.io.OutputStream 对象
  • await 方法接下去创建一个 Request 对象并且调用它的 parse 方法去解析 HTTP 请求的原始数据。
  • 在这之后,创建一个 Response 对象,把 Request 对象设置给它,并调用它的 sendStaticResource 方法。
  • 最后,await 关闭套接字并调用 Request 的 getUri 来检测 HTTP 请求的 URI 是不是一个 shutdown 命令。假如是的话,shutdown 变量将被设置为 true 且程序会退出 while 循环。

如何发送请求:

  1. 为了请求一个静态资源,在你的浏览器的地址栏或者网址框里边敲入以下的 URL: http://machineName:port/staticResource
  2. 如果你要从一个不同的机器上发送请求到你的应用程序正在运行的机器上,machineName 应 该是正在运行应用程序的机器的名称或者 IP 地址。假如你的浏览器在同一台机器上,你可以使 用 localhost 作为 machineName。端口是 8080,staticResource 是你需要请求的文件的名称, 且必须位于 WEB_ROOT 里边。举例来说,假如你正在使用同一台计算机上测试应用程序,并且你想要调用 HttpServer 对 象去发送一个 index.html 文件,你可以使用一下的 URL: http://localhost:8080/index.html
  3. 要停止服务器,你可以在 web 浏览器的地址栏或者网址框里边敲入预定义字符串,就在 URL 的 host:port 的后面,发送一个 shutdown 命令。 http://localhost:8080/SHUTDOWN

Request.java

解析请求流中的内容:本节仅仅获取URI

package ex01.pyrmont;

import java.io.InputStream;
import java.io.IOException;

public class Request {

    private InputStream input;
    private String uri;

    public Request(InputStream input) {
        this.input = input;
    }

    // 解析input输入流,这里只是获取请求行的URI
    // 实际的解析过程远不止这些
    public void parse() {

        //下面是用最常见的read()方法获取输入流的内容,也是为什么要传入输入流的原因
        StringBuffer request = new StringBuffer(2048);
        int i;
        byte[] buffer = new byte[2048];
        try {
            i = input.read(buffer);
        } catch (IOException e) {
            e.printStackTrace();
            i = -1;
        }
        for (int j = 0; j < i; j++) {
            request.append((char) buffer[j]);
        }
        System.out.print(request.toString());
        uri = parseUri(request.toString());
    }
    //获取URI,通过对字符串进行简单的查询和切割获得
    private String parseUri(String requestString) {
        int index1, index2;
        index1 = requestString.indexOf(' ');
        if (index1 != -1) {
            index2 = requestString.indexOf(' ', index1 + 1);
            if (index2 > index1)
                return requestString.substring(index1 + 1, index2);
        }
        return null;
    }

    public String getUri() {
        return uri;
    }

}

Request.java详细说明:

  • Request 类代表一个 HTTP 请求。从负责与客户端通信的 Socket 中传递过来 InputStream 对象来构造这个类的一个实例。你调用 InputStream 对象其中一个 read 方法来获 取 HTTP 请求的原始数据。
  • parse 方法解析 HTTP 请求里边的原始数据。这个方法没有做很多事情。它唯一可用的信息 是通过调用HTTP请求的私有方法parseUri获得的URI。parseUri方法在uri变量里边存储URI。 公共方法 getUri 被调用并返回 HTTP 请求的 URI。
  • 为了理解parse和parseUri方法是怎样工作的,你需要知道“超文本传输协议(HTTP)” 中 HTTP 请求的结构。在这一节中,我们仅仅关注 HTTP 请求的第一部分,请求行。请求行从 一个方法标记开始,接下去是请求的 URI 和协议版本,最后是用回车换行符(CRLF)结束。请求行 里边的元素是通过一个空格来分隔的。例如,使用 GET 方法来请求 index.html 文件的请求行如下所示。 GET /index.html HTTP/1.1
  • parse 方法从传递给 Requst 对象的套接字的 InputStream 中读取整个字节流并在一个缓冲 区中存储字节数组。然后它使用缓冲区字节数据的字节来填入一个 StringBuffer 对象,并且把 代表 StringBuffer 的字符串传递给 parseUri 方法。
  • 然后 parseUri 方法从请求行里边获得 URI。

Response.java

对目标文件存在与否进行两种不同的处理 如果存在就将文件的内容写入浏览器,否则返回404页面到浏览器 从这个类可以看出,这个类只是简单的文件作为静态资源,将文件的内容写到浏览器中,没有加载servlet的代码

package ex01.pyrmont;

import java.io.OutputStream;
import java.io.IOException;
import java.io.FileInputStream;
import java.io.File;

/*
 HTTP Response = Status-Line
 *(( general-header | response-header | entity-header ) CRLF)
 CRLF
 [ message-body ]
 Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
 */

public class CopyOfResponse {

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

    public CopyOfResponse(OutputStream output) {
        this.output = output;
    }

    public void setRequest(Request request) {
        this.request = request;
    }
    //设置静态资源
    public void sendStaticResource() throws IOException {
        byte[] bytes = new byte[BUFFER_SIZE];
        FileInputStream fis = null;
        try {
            //获取URI对应磁盘下的文件对象,因为需要用到URI,所以传入request参数
            File file = new File(HttpServer.WEB_ROOT, request.getUri());
            if (file.exists()) {
                //文件存在的话就将页面写到浏览器上
                fis = new FileInputStream(file);
                int ch = fis.read(bytes, 0, BUFFER_SIZE);
                while (ch != -1) {
                    output.write(bytes, 0, ch); //传入输出流是用于将内容写到浏览器上
                    ch = fis.read(bytes, 0, BUFFER_SIZE);
                }
            } else {
                //文件不存在,返回404页面
                String errorMessage = "HTTP/1.1 404 File Not Found
"
                        + "Content-Type: text/html
"
                        + "Content-Length: 23
" + "
"
                        + "<h1>File Not Found</h1>";
                output.write(errorMessage.getBytes());
            }
        } catch (Exception e) {

            System.out.println(e.toString());
        } finally {
            if (fis != null)
                fis.close();
        }
    }
}

Response.java详细说明

  • response对象是通过传递由套接字获得的OutputStream对象给HttpServer类的await方法来构造的。
  • Response 类有两个公共方法:setRequest 和 sendStaticResource。setRequest 方法用来 传递一个 Request 对象给 Response 对象,sendStaticResource 方法是用来发送一个静态资源,例如一个 HTML 文件。它首先通过传递上一级目录的路径和子路径给 File 累的构造方法来实例化 java.io.File 类。 File file = new File(HttpServer.WEB_ROOT, request.getUri());
  • 然后它检查该文件是否存在。假如存在的话,通过传递 File 对象让 sendStaticResource 构造一个 java.io.FileInputStream 对象。然后,它调用 FileInputStream 的 read 方法并把字节数组写入 OutputStream 对象。请注意,这种情况下,静态资源是作为原始数据发送给浏览器的。
  • 假如文件并不存在,sendStaticResource 方法发送一个错误信息到浏览器。

第三部分:小结

这一节,我们大体知道了一个Web服务器的大致的整体流程,虽然其中有很多问题没有考虑到,但是这里提供了一个很好的学习工具。

下一节会将这个最简单的servlet容器演变为第二个稍微复杂的 servlet 容器

如果有什么疑问或错误,可以发表评论或者加我QQ:1096101803告知。

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

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

原文地址:https://www.cnblogs.com/lzb1096101803/p/4779387.html