Tomcat:引子


本文(也有可能是本系列文章)是笔者自己对 Tomcat 的思考,肯定有很多不当之处,仅作为自己学习记录之用。

1. Socket

An Internet socket is characterized by at least the following:

  • local socket address, consisting of the local IP address and (for TCP and UDP, but not IP) a port number
  • protocol: A transport protocol, e.g., TCP, UDP, raw IP. This means that (local or remote) endpoints with TCP port 53 and UDP port 53 are distinct sockets, while IP does not have ports.

以上是维基百科对 Socket 的定义。

从我的角度看,Tomcat 是和 Socket 有很多相似之处的:都拥有一个 IP 地址、监听一个端口、接收请求并返回响应,所以我觉得从 Socket 入手比较合适。

定义中提到 Socket 可以由多种协议实现,以下我的思考全是基于 TCP 协议及其 Java 实现:TCP 有服务端,并且客户端需要和服务端建立连接,提供的是可靠服务(UDP 则不需要建立连接,提供的服务是不可靠的)。

ServerSocket

ServerSocket 是 TCP 协议的服务端,服务器端只有一个。构造函数的签名(选了参数最多的一个构造函数):

public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException

三个参数:

  • port:端口号;
  • backlog:请求最大的排队个数;
  • bindAddr:服务器端绑定的 IP 地址。

ServerSocket 建立之后,调用 accept() 方法接收客户端连接,这个返回在客户端连接之前会一直阻塞:

public Socket accept() throws IOException

accept() 方法返回 Socket 对象,ServerSocket 会对所有连接到服务器端的客户端建立一个 Socket。

Socket

Socket 是 TCP 协议的客户端,客户端可以有多个。构造函数签名(选了本文关心的一个):

public Socket(String host, int port) throws IOException

两个参数:

  • host:远程服务端地址;
  • port:远程端口。

Socket 有两个重要的方法 getInputStream()getOutputStream(),Socket 的 InputStream 可以理解为 Request,而 OutputStream 可以理解为 Response


描述了上面两个类,实际上我们已经可以开发出自己的一个服务器,因为我们已经有了监听端口、绑定地址的能力,而且我们能接收请求,并返回响应——和平时接触到的 Tomcat 的功能几乎一模一样了!

2. HTTP 协议

在本文我们要了解 HTTP 协议的一些基本内容。HTTP 请求包含三部分:

  • 请求方法、URI、协议/版本
  • 请求头:HTTP 的请求头的参数是用换行符(CRLF)分隔的。
  • 请求体

如:

POST /examples/default.jsp HTTP/1.1
Accept: text/plain; text/html 
Accept-Language: en-gb 
Connection: Keep-Alive
Host: localhost
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36
Content-Length: 33
Content-Type: application/x-www-form-urlencoded 
Accept-Encoding: gzip, deflate

3. Java 实现的简易服务器

下面我们用 ServerSocket 和 Socket 来开发一个服务器。用 Socket 模拟请求、响应,ServerSocket 模拟服务器。

例子来自《How Tomcat Works》,稍作改动。

各个类的作用:

  • Request:读取请求头信息;解析 URI,作为文件名。

  • Response:从 URI 的解析结果查找本地文件目录的对应文件,如果没找到返回 404 页面。

  • HttpServer:监听本地 8080 端口,如果 URI 是 /SHUTDOWN 则关闭服务器。

Request

Request 作为请求,读取 HTTP 请求头并解析。

public class Request {
    private static final int BUFFER_SIZE = 2048;

    private InputStream inputStream;

    private String uri;

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

    /**
     * 获取请求头
     */
    public void parse() {
        StringBuffer request = new StringBuffer(BUFFER_SIZE);
        byte[] buffer = new byte[BUFFER_SIZE];
        int i;

        try {
            i = inputStream.read(buffer);
        } catch (IOException e) {
            e.printStackTrace();
            i = -1;
        }

        for (int j = 0; j < i; j++) {
            request.append((char) buffer[j]);
        }

        uri = parseUri(request.toString());
    }

    /**
     * 获取 URI
     * @param requestString
     * @return
     */
    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;
    }
}

Response

查找本地目录中对应的文件,如果没有返回 404 页面:

public class Response {
    private static final int BUFFER_SIZE = 1024;

    private Request request;

    private OutputStream outputStream;

    public Response(OutputStream output) {
        this.outputStream = output;
    }

    public void setRequest(Request request) {
        this.request = request;
    }

    public void sendStaticResource() throws IOException {
        byte[] bytes = new byte[BUFFER_SIZE];
        FileInputStream fis = null;
        try {
            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) {
                    outputStream.write(bytes, 0, ch);
                    ch = fis.read(bytes, 0, BUFFER_SIZE);
                }
            } else {
                // 文件未找到
                String errorMessage = "HTTP/1.1 404 File Not Found\r\n" + "Content-Type: text/html\r\n"
                        + "Content-Length: 23\r\n" + "\r\n" + "<h1>File Not Found</h1>";
                outputStream.write(errorMessage.getBytes());
            }
        } catch (Exception e) {
            System.out.println(e.toString());
        } finally {
            if (fis != null)
                fis.close();
        }
    }
}

Server

启动之后等待客户端请求,并监听停止服务器命令。

public class HttpServer {
    // 文件目录:用户的 home 目录 + webroot
    public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot";

    // 关闭服务器命令
    private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";

    // 关闭服务器命令是否收到
    private boolean shutdown = false;

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

    private void await() {
        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 (!shutdown) {
            Socket socket;
            InputStream input;
            OutputStream output;
            try {
                socket = serverSocket.accept();
                input = socket.getInputStream();
                output = socket.getOutputStream();

                // 创建 Request 并解析
                Request request = new Request(input);
                request.parse();

                // 创建 Response
                Response response = new Response(output);
                response.setRequest(request);
                response.sendStaticResource();

                // 关闭 Socket
                socket.close();

                // 检查 URI 是否关闭命令
                shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
            } catch (Exception e) {
                e.printStackTrace();
                continue;
            }
        }
    }
}

启动 HttpServer,在浏览器输入访问 127.0.0.1:8080,网址后面加上任意 URI,Response 就会到指定目录下去找同名文件。

参考

Network Socket

How Tomcat Works


 Toc
 Tags