<commit_before>---
title: Tomcat：引子
category: ['Tomcat']
tags: ['Tomcat']
author: 景阳
email: <EMAIL>
description: 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](https://en.wikipedia.org/wiki/Protocol_(computing)): A transport protocol, e.g., [TCP](https://en.wikipedia.org/wiki/Transmission_Control_Protocol), [UDP](https://en.wikipedia.org/wiki/User_Datagram_Protocol), [raw IP](https://en.wikipedia.org/wiki/Raw_socket). 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 实现。

### ServerSocket

ServerSocket 是 TCP 协议的服务端，服务器端只有一个。构造函数的签名（选了参数最多的一个构造函数）：

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

三个参数：

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

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

```java
public Socket accept() throws IOException
```

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

### Socket

Socket 是 TCP 协议的客户端，客户端可以有多个。构造函数签名（选了本文关心的一个）：

```java
public Socket(String host, int port) throws IOException
```

两个参数：

- host：远程服务端地址；
- port：远程端口。

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

---

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

## 2. HTTP 协议

在本文我们要了解 [HTTP 协议](<https://www.w3.org/Protocols/HTTP/1.1/rfc2616.pdf>)的一些基本内容。HTTP 请求包含三部分：

- 请求方法、URI、协议/版本
- 请求头：HTTP 的请求头的参数是用换行符（CRLF）分隔的。
- 请求体

如：

```http
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 模拟服务器。

这个简易服务器读取 HTTP 请求并解析 URI，然后在本地目录中查找 URI 对应的文件，如果有就返回，如果没有显示 404 页面。

## Request

Request 作为请求，我们的目标是读取 HTTP 请求头并解析。

```java
public class Request {
    private InputStream inputStream;

    private String uri;

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

    public void parse() {
        StringBuffer request = new StringBuffer(2018);
        int i;
        byte[] buffer = new byte[2048];

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

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

        System.out.println(request.toString());
        uri = parseUri(request.toString());
    }

    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 页面：

```java
public class Response {

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

    public Response(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 {
            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 {
                // file not found
                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>";
                output.write(errorMessage.getBytes());
            }
        } catch (Exception e) {
            // thrown if cannot instantiate a File object
            System.out.println(e.toString());
        } finally {
            if (fis != null)
                fis.close();
        }
    }
}
```

## Server

```java
public class HttpServer {

  	// 本地文件目录路径
    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();
    }

    public 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 = null;
            InputStream input = null;
            OutputStream output = null;
            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 命令
                shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
            } catch (Exception e) {
                e.printStackTrace();
                continue;
            }
        }
    }
}
```

## 参考

[Network Socket](<https://en.wikipedia.org/wiki/Network_socket>)



[How Tomcat Works](<https://book.douban.com/subject/1943128/>)<commit_msg>tomcat1<commit_after>---
title: Tomcat：引子
category: ['Tomcat']
tags: ['Tomcat']
author: 景阳
email: <EMAIL>
description: 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](https://en.wikipedia.org/wiki/Protocol_(computing)): A transport protocol, e.g., [TCP](https://en.wikipedia.org/wiki/Transmission_Control_Protocol), [UDP](https://en.wikipedia.org/wiki/User_Datagram_Protocol), [raw IP](https://en.wikipedia.org/wiki/Raw_socket). 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 实现。

### ServerSocket

ServerSocket 是 TCP 协议的服务端，服务器端只有一个。构造函数的签名（选了参数最多的一个构造函数）：

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

三个参数：

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

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

```java
public Socket accept() throws IOException
```

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

### Socket

Socket 是 TCP 协议的客户端，客户端可以有多个。构造函数签名（选了本文关心的一个）：

```java
public Socket(String host, int port) throws IOException
```

两个参数：

- host：远程服务端地址；
- port：远程端口。

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

---

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

## 2. HTTP 协议

在本文我们要了解 [HTTP 协议](<https://www.w3.org/Protocols/HTTP/1.1/rfc2616.pdf>)的一些基本内容。HTTP 请求包含三部分：

- 请求方法、URI、协议/版本
- 请求头：HTTP 的请求头的参数是用换行符（CRLF）分隔的。
- 请求体

如：

```http
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 模拟服务器。

Request：读取请求信息并打印到 console；解析 URI。

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

HttpServer：监听本地 8080 端口，如果 URI 是 `/SHUTDOWN` 则关闭服务器。

## Request

Request 作为请求，我们的目标是读取 HTTP 请求头并解析。

```java
public class Request {
    private InputStream inputStream;

    private String uri;

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

    public void parse() {
        StringBuffer request = new StringBuffer(2018);
        int i;
        byte[] buffer = new byte[2048];

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

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

        System.out.println(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;
    }
}
```

## Response

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

```java
public class Response {

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

    public Response(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 {
          // 拼装本地文件路径
            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 {
                // 文件未找到
                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>";
                output.write(errorMessage.getBytes());
            }
        } catch (Exception e) {
            // IO 异常
            System.out.println(e.toString());
        } finally {
            if (fis != null)
                fis.close();
        }
    }
}
```

## Server

```java
public class HttpServer {
    // 本地文件目录路径
    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();
    }

    public 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 = null;
            InputStream input = null;
            OutputStream output = null;
            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 命令
                shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
            } catch (Exception e) {
                e.printStackTrace();
                continue;
            }
        }
    }
}
```

## 参考

[Network Socket](<https://en.wikipedia.org/wiki/Network_socket>)



[How Tomcat Works](<https://book.douban.com/subject/1943128/>)
{'fraction_non_alphanumeric': 0.11736404723720659, 'fraction_numerical': 0.01625601399620936, 'mean_word_length': 2.3242064453598257, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 25, 'https://': 14, 'lorem ipsum': 0, 'www.': 4, 'xml': 0}, 'pii_count': 2, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6172937', 'n_tokens_mistral': 5225, 'n_tokens_neox': 5208, 'n_words': 1063}