与Tomcat的区别:参考 Tomcat 的源码,自己做了一个 具备 Servlet 容器功能的 web 服务器,但是并不是和 Tomcat 源码一样。Tomcat 本身的功能十分丰富,而且系统架构也比较复杂,并不适合直接通过完全仿照来学习,在研习了 Tomcat 源码的基础之上去繁化简。

【问题】Tomcat 主要由以下组件构成:

  • Catalina:Catalina 是 Tomcat 的核心组件,它负责处理 HTTP 请求并将它们转发到适当的 Web 应用程序或 Servlet 容器进行处理。

  • Coyote:Coyote 是一个 HTTP 连接器,它处理传入的 HTTP 请求,并将它们传递给 Catalina 进行处理。Coyote 还支持 HTTPS 和 SSL。

  • Jasper:Jasper 是 JSP 引擎,它将 JSP 文件编译成 Java Servlet,并将其传递给 Catalina 进行处理。

  • Cluster:Cluster 是 Tomcat 的集群组件,它允许多个 Tomcat 服务器之间共享会话和负载均衡请求。

  • Manager:Manager 是 Tomcat 的管理应用程序,它允许用户管理 Tomcat 服务器,包括管理 Web 应用程序、会话和连接器等。

  • Realm:Realm 是 Tomcat 的身份验证和授权组件,它允许管理员配置用户和角色,并将它们与 Web 应用程序和 Servlet 容器进行关联。

  • JDBC 连接池:Tomcat 包含一个 JDBC 连接池,它允许 Web 应用程序和 Servlet 容器访问数据库连接,并提高了应用程序的性能和可伸缩性。

【问题】Tomcat部署多个应用?

  • 使用Context容器:在一个Tomcat中可以使用多个Context容器来部署多个应用程序。每个Context容器可以为应用程序提供单独的配置,并将应用程序部署在不同的上下文路径中,从而实现应用程序之间的隔离。

  • 配置多个Host:同样地,可以在一个Tomcat中配置多个Host,每个Host拥有自己的应用程序。通过在server.xml文件中配置不同的Host,可以实现不同应用程序之间的隔离。

【问题】Tomcat请求过程?

  • 用户点击网页内容,请求被发送到本机端口8080/6060,被在那里监听的Coyote HTTP/1.1 Connector获得。
  • Connector把该请求交给它所在的Service的Engine来处理,并等待Engine的回应。
  • Engine获得请求localhost/test/index.jsp,匹配所有的虚拟主机Host。
  • Engine匹配到名为localhost的Host(即使匹配不到也把请求交给该Host处理,因为该Host被定义为该Engine的默认主机),名为localhost的Host获得请求/test/index.jsp,匹配它所拥有的所有的Context。Host匹配到路径为/test的Context(如果匹配不到就把该请求交给路径名为“ ”的Context去处理)。
  • path=“/test”的Context获得请求/index.jsp,在它的mapping table中寻找出对应的Servlet。Context匹配到URL PATTERN为*.jsp的Servlet,对应于JspServlet类。
  • 构造HttpServletRequest对象和HttpServletResponse对象,作为参数调用JspServlet的doGet()或 doPost()执行业务逻辑、数据存储等程序。
  • Context把执行完之后的HttpServletResponse对象返回给Host。
  • Host把HttpServletResponse对象返回给Engine。
  • Engine把HttpServletResponse对象返回Connector。
  • Connector把HttpServletResponse对象返回给客户Browser。

【问题】Tomcat 有哪几种Connector 运行模式(优化)?

  • BIO:一个线程处理一个请求。缺点:并发量高时,线程数较多,浪费资源。Tomcat7版本或更低版本中,在Linux系统中默认使用这种方式。
  • NIO:利用Java的异步IO处理,可以通过少量的线程处理大量的请求。tomcat8.0.x中默认使用的是NIO。
  • APR:即Apache Portable Runtime,从操作系统层面解决io阻塞问题。Tomcat7或Tomcat8在Win7或以上的系统中启动默认使用这种方式。

【问题】Tomcat有几种部署方式?

  • 利用Tomcat的自动部署:把web应用拷贝到webapps目录(生产环境不建议放在该目录中)。Tomcat在启动时会加载目录下的应用,并将编译后的结果放入work目录下。
  • 使用Manager App控制台部署:在tomcat主页点击“Manager App” 进入应用管理控制台,可以指定一个web应用的路径或war文件。
  • 修改conf/server.xml文件部署:在server.xml文件中,增加Context节点可以部署应用。
  • 增加自定义的Web部署文件:在conf/Catalina/localhost/路径下增加 xyz.xml文件,内容是Context节点,可以部署应用。

【问题】Tomcat容器是如何创建servlet类实例?用到了什么原理?

  • 当容器启动时,会读取在webapps目录下所有的web应用中的web.xml文件,然后对 xml文件进行解析,并读取servlet注册信息。然后,将每个应用中注册的servlet类都进行加载,并通过 反射的方式实例化。(有时候也是在第一次请求时实例化)
  • 在servlet注册时加上1如果为正数,则在一开始就实例化,如果不写或为负数,则第一次请求实例化。

【问题】如何优化Tomcat?tomcat作为Web服务器,它的处理性能直接关系到用户体验,下面是几种常见的优化措施:

  • 掉对web.xml的监视,把jsp提前编辑成Servlet。
  • 有富余物理内存的情况,加大tomcat使用的jvm的内存服务器所能提供CPU、内存、硬盘的性能对处理能力有决定性影响。
  • 利用缓存和压缩:对于静态页面最好是能够缓存起来,这样就不必每次从磁盘上读。这里我们采用了Nginx作为缓存服务器,将图片、css、js文件都进行了缓存,有效的减少了后端tomcat的访问。
  • 采用集群:单个服务器性能总是有限的,最好的办法自然是实现横向扩展,那么组建tomcat集群是有效提升性能的手段。我们还是采用了Nginx来作为请求分流的服务器,后端多个tomcat共享session来协同工作。

  • 优化线程数优化:找到Connector port="8080" protocol="HTTP/1.1",增加maxThreads和acceptCount属性(使acceptCount大于等于maxThreads)。

  • 使用线程池优化:在server.xml中增加executor节点,然后配置connector的executor属性。

  • 内存优化:因为tomcat启动起来后就是一个java进程,所以这块可以参照JVM部分的优化思路。

1,基本组件

1.1,HelloWord

创建Java项目,导入包:

  • hutool-all-4.3.1.jar: hutool 工具类包,提供各种各样的便利工具。
  • jsoup-1.12.1.jar: 解析xml用。
  • jspc_all.jar:把 jsp 编译成为 servlet 用,这是阅读了 tomcat源码,然后对其进行了修改与提取之后,专门提供给本项目使用。
  • junit-4.9.jar:单元测试。
  • log4j-1.2.17.jar:log4j日志。
  • servlet-api.jar:servlet 包。

Hello DiyTomcat

package com.ysy.diytomcat;

import cn.hutool.core.util.NetUtil;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Bootstrap {
    public static void main(String[] args) {
        try {
            int port = 18080;
            //NetUtil.isUsableLocalPort 用来判断端口是否被占用,返回 true 表示没有被占用。
            if (!NetUtil.isUsableLocalPort(port)) {
                System.out.println(port + " 端口已经被占用了!");
                return;
            }
            //在端口18080上启动 ServerSocket。 服务端和浏览器通信是通过 Socket进行通信的,所以这里需要启动一个 ServerSocket。
            ServerSocket ss = new ServerSocket(port);
            while (true) {
                //这表示收到一个浏览器客户端的请求
                Socket s = ss.accept();
                //打开输入流,准备接受浏览器提交的信息
                InputStream is = s.getInputStream();
                //准备一个长度是 1024 的字节数组,把浏览器的信息读取出来放进去。 
                int bufferSize = 1024;
                byte[] buffer = new byte[bufferSize];
                is.read(buffer);
                //把字节数组转换成字符串,并且打印出来
                String requestString = new String(buffer, "utf-8");
                System.out.println("浏览器的输入信息: \r\n" + requestString);
                //打开输出流,准备给客户端输出信息
                OutputStream os = s.getOutputStream();
                //这里准备发送给给客户端的数据。
                String response_head = "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html\r\n\r\n";
                String responseString = "Hello DIY Tomcat";
                responseString = response_head + responseString;
                //把字符串转换成字节数组发送出去
                os.write(responseString.getBytes());
                os.flush();
                //关闭客户端对应的 socket
                s.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

发给浏览器的信息是Hello DIY Tomcat, 但是在前面还增加了一个 response_head, 因为作为web 服务器,和浏览器之间通信,需要遵循 http 协议,所以需要加上一个头信息:response_head。

1.2,MiniBrowser

为了更好的理解浏览器是如何与服务器进行通信的,做了一个迷你浏览器 MiniBrowser。这个浏览器会模拟发送 Http 协议的请求,并且获取完整的 Http 响应,通过这种方式,可以更好的理解浏览器与服务器是如何通信。

MiniBrowser 主要提供如下方法:

  • getHttpBytes 返回二进制的 http 响应。
  • getHttpString 返回字符串的 http 响应。
  • getContentBytes 返回二进制的 http 响应内容 (可简单理解为去掉头的 html 部分)。
  • getContentString 返回字符串的 http 响应内容 (可简单理解为去掉头的 html 部分)。
  • 以上4个方法都增加了个 gzip 参数,可以获取压缩后的数据,便于实现 gzip 压缩。
package com.ysy.diytomcat.util;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
 
/**
 * @author za-lincanjian
 * @注意:核心是方法:getHttpBytes,看懂这个方法就看懂全部了
 */
public class MiniBrowser {
 
    public static void main(String[] args) {
        //初始化请求地址,这个请求地址就是待会会去链接的地址
        String url = "http://static.how2j.cn/diytomcat.html";
        //调用该方法,获取 http请求返回值的返回内容(简而言之就是去除掉了返回头的那些字符串)(请进到这个调用方法继续看)
        String contentString = getContentString(url,false);
        System.out.println(contentString);
        //这个方法就是获取全部的Http返回内容的字符串的方法了,各位看了刚才的一些解析这个方法就没啥好说的了
        String httpString = getHttpString(url,false);
        System.out.println(httpString);
    }
 
    //这些重载方法其实就是备用的,以后可以直接调url,默认不gzip
    public static byte[] getContentBytes(String url) {
        return getContentBytes(url,false);
    }
 
    //这些重载方法其实就是备用的,以后可以直接调url,默认不gzip
    public static String getContentString(String url) {
        return getContentString(url,false);
    }
 
 
    public static String getContentString(String url, boolean gzip) {
        //这里获取返回体具体内容的字节数组,请跟进去看
        byte[] result = getContentBytes(url,gzip);
        //getContentString 表示获取内容的字符串,我们获取到具体内容的字节数组后还需要进行编码
        if (result == null) {
            return null;
        }
        //这里就是一个编码过程了,我这里跟源代码不同,用StandarCahrset这个类可以避免抛异常,这里引入一个知识,因为这个是个常量,jvm可以知道你会选的是utf-8,所以不要求你抛异常
        return new String(result, StandardCharsets.UTF_8).trim();
 
    }
 
    public static byte[] getContentBytes(String url, boolean gzip) {
        //这里是真正的逻辑,就是与请求地址建立连接的逻辑,是整个类的核心,其他方法都只是处理这个方法返回值的一些逻辑而已
        byte[] response = getHttpBytes(url,gzip);
        //这个doubleReturnq其实是这样来的:我们获取的返回值正常其实是这样的
        /**(响应头部分)
         *  xxxxx
         *  xxxxx
         *  xxxxx
         *
         *  (具体内容部分,在这个代码中是hello diytomcat)
         *  xxx
         */
        //也就是说响应头部分和具体内容部分其实隔了一行, \r表示回到行首\n表示换到下一行,那么\r\n就相当于说先到了空格一行的那一行的行首,接着又到了具体内容的那部分的行首
        byte[] doubleReturn = "\r\n\r\n".getBytes();
 
        //接着这里初始化一个记录值,做记录用,往下看
        int pos = -1;
        //开始遍历返回内容
        for (int i = 0;i < response.length - doubleReturn.length;i++) {
            //这里的意思就是不断去初始化一个数组(从原数组进行拷贝),目的其实是为了获取到\r\n这一行的起始位置
            byte[] temp = Arrays.copyOfRange(response,i,i + doubleReturn.length);
 
            //来到这里,就是比较内容,当走到这里,说明temp这个字节数组的内容就是\r\n\r\n的内容了,说明我们找到了他的其实位置
            if (Arrays.equals(temp,doubleReturn)) {
                //将pos等于i,记录位置
                pos = i;
                break;
            }
        }
        //如果没记录到,那就说明压根没具体内容,那其实就是null
        if (-1 == pos) {
            return null;
        }
 
        //接着pos就是\r\n\n的第一个\的这个位置,加上\r\n\r\n的长度,相当于来到了具体内容的其实位置
        pos += doubleReturn.length;
 
        //最后,确定了具体内容是在哪个字节开始,就拷贝这部分内容返回
        byte[] result = Arrays.copyOfRange(response,pos,response.length);
        return result;
    }
 
    public static String getHttpString(String url,boolean gzip) {
        //这里也没啥了,就是少了截取内容的那部分操作,直接就返回整个返回值的字节数组出来
        byte[] bytes = getHttpBytes(url,gzip);
        return new String(bytes).trim();
    }
 
    //这些重载方法其实就是备用的,以后可以直接调url,默认不gzip
    public static String getHttpString(String url) {
        return getHttpString(url,false);
    }
 
    public static byte[] getHttpBytes(String url, boolean gzip) {
        //首先初始化一个返回值,这个返回值是一个字节数组,utf-8编码的
        byte[] result = null;
 
        try {
            //通过url来new一个URL对象,这样你就不用自己去截取他的端口啊或者请求路径啥的,可以直接调他的方法获取
            URL u = new URL(url);
            //开启一个socket链接,client指的就是你现在的这台计算机
            Socket client = new Socket();
            //获取到端口号,要是端口号是-1,那就默认取80端口(这个端口也是web常用端口)
            int port = u.getPort();
            if (port == -1) {
                port = 80;
            }
            //这个是socket编程的内容,简单来说就是通过一个host+端口,和这个url建立连接
            InetSocketAddress inetSocketAddress = new InetSocketAddress(u.getHost(),port);
            //开始连接了,1000是超时时间,等于说超过1秒就算你超时了
            client.connect(inetSocketAddress,1000);
            //初始化请求头
            Map<String,String> requestHeaders = new HashMap<>();
 
            //这几个参数都是http请求时会带上的请求头
            requestHeaders.put("Host", u.getHost() + ":" + port);
            requestHeaders.put("Accept", "text/html");
            requestHeaders.put("Connection", "close");
            requestHeaders.put("User-Agent", "how2j mini browser / java1.8");
 
            //gzip是确定客户端或服务器端是否支持压缩
            if (gzip) {
                requestHeaders.put("Accept_Encoding", "gzip");
            }
 
            //获取到path,其实就是/diytomcat.html,如果没有的话就默认是/
            String path = u.getPath();
            if (path.length() == 0) {
                path = "/";
            }
 
            //接着开始拼接请求的字符串,其实所谓的请求头和请求内容就是这么一串字符串拼接出来
            String firstLine = "GET " + path + " HTTP/1.1\r\n";
 
            StringBuffer httpRequestString = new StringBuffer();
            //拼接firstLine的内容
            httpRequestString.append(firstLine);
            Set<String> headers = requestHeaders.keySet();
 
            //遍历header的那个map进行拼接
            for (String header : headers) {
                String headerLine = header + ":" + requestHeaders.get(header) + "\r\n";
                httpRequestString.append(headerLine);
            }
 
            /**走到这的时候,httpRequestString已经拼接好了,内容是:
             GET /diytomcat.html HTTP/1.1
             Accept:text/html
             Connection:close
             User-Agent:how2j mini browser / java1.8
             Host:static.how2j.cn:80
             */
            //通过输出流,将这么一串字符串输出给连接的地址,后面的true是autoFlush,表示开始自动刷新
            PrintWriter printWriter = new PrintWriter(client.getOutputStream(),true);
            printWriter.println(httpRequestString);
            //这时候你已经将需要的请求所需的字符串发给上面那个url了,其实所谓的http协议就是这样,你发给他这么一串符合规范的字符串,他就给你响应,接着他那边就给你返回数据
            //所以这时候我们开启一个输出流
            InputStream inputStream = client.getInputStream();
 
            //一次接受1m的数据就行了,1024byte = 1m
            int bufferSize = 1024;
            //这里初始化一个输出流,待会存取url返回给我们的数据用
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            //开始new一个1m大小的字节数组
            byte[] buffer = new byte[bufferSize];
            //循环继续读取
            while (true) {
                //从输入流获取数据,调read方法存到buffer数组中
                int length = inputStream.read(buffer);
                //读到的长度如果是-1,说明没读到数据了,直接退出
                if (length == -1) {
                    break;
                }
                //接着先将读到的1m数据输出到我们初始化的那个输出流中
                byteArrayOutputStream.write(buffer,0,length);
                //这里是一个结束的操作,length != bufferSize,说明已经是最后一次读取了,为什么这么说?
                //举个例子,如果你的数据是1025字节,当你第二次循环的时候就是只有一个字节了,这时候就说明处理完这一个字节的数组就可以结束了,因为已经没数据了
                if (length != bufferSize) {
                    break;
                }
            }
            //通过方法,将这个输出流返回成字节数组result,为什么要用这个输出流来存返回的字节数组呢?因为如果你用数组的话其实你不能确定
            //整个返回数据有多大,用集合理论上是可以实现的,各位有兴趣可以试试
            result = byteArrayOutputStream.toByteArray();
            //这是个好习惯,不过最好是放在finally进行关闭比较好,这里就是关闭连接了
            client.close();
        } catch (Exception e) {
            //接着这里是异常打印
            e.printStackTrace();
            //这里是将返回的异常信息进行字节数组编码,其实就是兼容这个方法
            try {
                result = e.toString().getBytes("utf-8");
 
            } catch (UnsupportedEncodingException e1) {
                e1.printStackTrace();
            }
        }
 
        //返回结果
        return result;
    }
}

1.3,自动测试

不断对这个项目进行持续的改造和重构, 那么如何保证重构之后,以前的功能依旧可以使用呢? 当然可以通过手动测试来进行,但是随着功能越来越多,频繁的手动测试开始变得无聊和难以持续。 所以这个时候,我们就会引入单元自动测试来检查在进行了代码修改之后,以前的代码依然可以其作用。 如果测试失败,那么就可以及时得到提醒有地方改得不对,这样就可以在重构的同时保证项目本身的质量了。

package com.ysy.diytomcat.test;

import cn.hutool.core.util.*;
import com.ysy.diytomcat.util.MiniBrowser;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;

public class TestTomcat {
    private static int port = 18080;
    private static String ip = "127.0.0.1";

    @BeforeClass
    public static void beforeClass() {
        //所有测试开始前看diy tomcat 是否已经启动了
        if (NetUtil.isUsableLocalPort(port)) {
            System.err.println("请先启动 位于端口: " + port + " 的diy tomcat,否则无法进行单元测试");
            System.exit(1);
        } else {
            System.out.println("检测到 diy tomcat已经启动,开始进行单元测试");
        }
    }

    @Test
    public void testHelloTomcat() {
        String html = getContentString("/");
        Assert.assertEquals(html, "Hello DIY Tomcat");
    }
    //准备一个工具方法,用来获取网页返回。
    private String getContentString(String uri) {
        String url = StrUtil.format("http://{}:{}{}", ip, port, uri);
        String content = MiniBrowser.getContentString(url);
        return content;
    }
}

1.4,Request对象

目前我们从浏览器获取信息是所有的都放在一个 String 里面,伴随着服务器功能的开发,需要从这个字符串里解构出更加丰富的信息,为了方便后续的解析工作,我们引入 Request 对象,用来代表浏览器发过来的请求信息。 伴随着服务器的功能完善,这个Request对象会不断地改进和重构。

改进MiniBrowser:添加readBytes通用方法(方便后续调用)

public static byte[] readBytes(InputStream is) throws IOException {
    int buffer_size = 1024;
    byte buffer[] = new byte[buffer_size];
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    while (true) {
        int length = is.read(buffer);
        if (-1 == length)
            break;
         baos.write(buffer, 0, length);
         if (length != buffer_size)
            break;
        }
     byte[] result = baos.toByteArray();
     return result;
}

Request对象:创建 Request 对象用来解析 requestString 和 uri。

package com.ysy.diytomcat.http;
import cn.hutool.core.util.StrUtil;
import com.ysy.diytomcat.util.MiniBrowser;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;

public class Request {

    private String requestString;
    private String uri;
    private Socket socket;

    public Request(Socket socket) throws IOException {
        this.socket = socket;
        parseHttpRequest();
        if (StrUtil.isEmpty(requestString))
            return;
        parseUri();
    }
    //解析 http请求字符串, 这里面就调用了 MiniBrowser里重构的 readBytes 方法。
    private void parseHttpRequest() throws IOException {
        InputStream is = this.socket.getInputStream();
        byte[] bytes = MiniBrowser.readBytes(is);
        requestString = new String(bytes, "utf-8");
    }
    //解析真实链接
    private void parseUri() {
        String temp = StrUtil.subBetween(requestString, " ", " ");
        //不包括?,链接即是真实链接
        if (!StrUtil.contains(temp, '?')) {
            uri = temp;
            return;
        }
        //包括?,截取?之前的字符串
        temp = StrUtil.subBefore(temp, '?', false);
        uri = temp;
    }

    public String getUri() {
        return uri;
    }

    public String getRequestString() {
        return requestString;
    }
}

Bootstrap:修改 Bootstrap 使得数据从 Request 对象里获取来。

package com.ysy.diytomcat;
import cn.hutool.core.util.NetUtil;
import com.ysy.diytomcat.http.Request;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Bootstrap {
    public static void main(String[] args) {

        try {
            int port = 18080;

            if (!NetUtil.isUsableLocalPort(port)) {
                System.out.println(port + " 端口已经被占用了!");
                return;
            }
            ServerSocket ss = new ServerSocket(port);

            while (true) {
                Socket s = ss.accept();
                Request request = new Request(s);
                System.out.println("浏览器的输入信息: \r\n" + request.getRequestString());
                System.out.println("uri: " + request.getUri());

                OutputStream os = s.getOutputStream();
                String response_head = "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html\r\n\r\n";
                String responseString = "Hello DIY Tomcat";
                responseString = response_head + responseString;
                os.write(responseString.getBytes());
                os.flush();
                s.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

再运行 TestTomcat,此时单元测试的作用就可以显示出来了。经过上述的重构之后,单元测试依然可以通过,那么说明并没有影响到原来的功能性。

浏览器的输入信息: 

uri: null
浏览器的输入信息: 
GET / HTTP/1.1
Accept:text/html
Connection:close
User-Agent:how2j mini brower / java1.8
Host:127.0.0.1:18080


uri: /

运行单元测试之后,会看到上面所示的情况,会发送两条消息。第二条消息是正常的测试信息,那么第一条是什么呢?它是TestTomcat中beforeClass 里的检查端口是否启动的代码导致的空消息。

1.5,Response对象

首先准备一个常量工具类,用于存放响应的头信息模板。

public class Constant {
    public final static String response_head_202 = "HTTP/1.1 200 OK\r\n Content-Type: {}\r\n\r\n";
}

创建 Response 类

package com.ysy.diytomcat.http;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;

public class Response {
    //用于存放返回的 html 文本
    private StringWriter stringWriter;
    private PrintWriter writer;
    //contentType就是对应响应头信息里的 Content-type ,默认是 "text/html"。
    private String contentType;

    public Response() {
        //用于提供一个 getWriter() 方法,这样就可以像 HttpServletResponse 那样写成 response.getWriter().println(); 这种风格了。
        this.stringWriter = new StringWriter();
        this.writer = new PrintWriter(stringWriter);
        this.contentType = "text/html";
    }

    public String getContentType() {
        return contentType;
    }

    public PrintWriter getWriter() {
        return writer;
    }

    public byte[] getBody() throws UnsupportedEncodingException {
        String content = stringWriter.toString();
        byte[] body = content.getBytes("utf-8");
        return body;
    }
}

重构 Bootstrap

package com.ysy.diytomcat;

import cn.hutool.core.util.*;
import com.ysy.diytomcat.http.*;
import com.ysy.diytomcat.util.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Bootstrap {

    public static void main(String[] args) {

        try {
            int port = 18080;

            if (!NetUtil.isUsableLocalPort(port)) {
                System.out.println(port + " 端口已经被占用了!");
                return;
            }
            ServerSocket ss = new ServerSocket(port);

            while (true) {
                Socket s = ss.accept();
                Request request = new Request(s);
                System.out.println("浏览器的输入信息: \r\n" + request.getRequestString());
                System.out.println("uri: " + request.getUri());

                Response response = new Response();
                String html = "Hello DIY Tomcat";
                response.getWriter().println(html);
                //把返回 200 响应重构到了一个独立的方法里,看上去更清爽了。
                handle200(s, response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void handle200(Socket s, Response response) throws IOException {
        String contentType = response.getContentType();
        String headText = Constant.response_head_202;
        headText = StrUtil.format(headText, contentType);
        byte[] head = headText.getBytes();
        //根据 response 对象上的 contentType ,组成返回的头信息,并且转换成字节数组。
        byte[] body = response.getBody();
        //获取主题信息部分,即 html 对应的 字节数组。
        byte[] responseBytes = new byte[head.length + body.length];
        //拼接头信息和主题信息,成为一个响应字节数组。
        ArrayUtil.copy(head, 0, responseBytes, 0, head.length);
        ArrayUtil.copy(body, 0, responseBytes, head.length, body.length);

        OutputStream os = s.getOutputStream();
        os.write(responseBytes);
        //close 自动 flush
        s.close();
    }
}

为什么要把头和主体分开,而不直接使用合并的 html 呢? 因为在接下来的工作里,会对头部做更复杂的处理, 主体部分也会面对二进制文件和gzip压缩,现在分开来,后续处理起来更加游刃有余。

1.6,文本文件

根据 tomcat 的目录结构,我们在 diytomcat 目录下新建 webapps/ROOT 目录,然后在里面新建文件 a.html,内容如下:

I am fun, Thanks.

新增 webappsFolder 和 rootFolder, 方便后续使用。

public final static File webappsFolder = new File(SystemUtil.get("user.dir"),"webapps");
public final static File rootFolder = new File(webappsFolder,"ROOT");

重构 Bootstrap

package com.ysy.diytomcat;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.*;
import com.ysy.diytomcat.http.*;
import com.ysy.diytomcat.util.*;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class Bootstrap {

    public static void main(String[] args) {

        try {
            int port = 18080;

            if (!NetUtil.isUsableLocalPort(port)) {
                System.out.println(port + " 端口已经被占用了!");
                return;
            }
            ServerSocket ss = new ServerSocket(port);

            while (true) {
                Socket s = ss.accept();
                Request request = new Request(s);
                System.out.println("浏览器的输入信息: \r\n" + request.getRequestString());
                System.out.println("uri: " + request.getUri());

                Response response = new Response();
                String uri = request.getUri();
                //首先判断 uri 是否为空,如果为空就不处理了。 什么情况为空呢? 在 TestTomcat 里的 NetUtil.isUsableLocalPort(port) 这段代码就会导致为空。
                if (null == uri)
                    continue;
                System.out.println(uri);
                //如果是 "/", 那么依然返回原字符串。
                if ("/".equals(uri)) {
                    String html = "Hello DIY Tomcat";
                    response.getWriter().println(html);
                } else {
                    //接着处理文件,首先取出文件名,比如访问的是 /a.html, 那么文件名就是 a.html
                    String fileName = StrUtil.removePrefix(uri, "/");
                    //然后获取对应的文件对象 file
                    File file = FileUtil.file(Constant.rootFolder, fileName);
                    if (file.exists()) {
                        //如果文件存在,那么获取内容并通过 response.getWriter 打印。
                        String fileContent = FileUtil.readUtf8String(file);
                        response.getWriter().println(fileContent);
                    } else {
                        //如果文件不存在,那么打印 File Not Found。
                        response.getWriter().println("File Not Found");
                    }
                }
                //把返回 200 响应重构到了一个独立的方法里,看上去更清爽了。
                handle200(s, response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void handle200(Socket s, Response response) throws IOException {
        String contentType = response.getContentType();
        String headText = Constant.response_head_202;
        headText = StrUtil.format(headText, contentType);
        byte[] head = headText.getBytes();
        //根据 response 对象上的 contentType ,组成返回的头信息,并且转换成字节数组。
        byte[] body = response.getBody();
        //获取主题信息部分,即 html 对应的 字节数组。
        byte[] responseBytes = new byte[head.length + body.length];
        //拼接头信息和主题信息,成为一个响应字节数组。
        ArrayUtil.copy(head, 0, responseBytes, 0, head.length);
        ArrayUtil.copy(body, 0, responseBytes, head.length, body.length);

        OutputStream os = s.getOutputStream();
        os.write(responseBytes);
        //close 自动 flush
        s.close();
    }
}

1.7,日志

tomcat 的日志会放在 logs/ 目录下,日子文件默认是 "catalina“, 然后随着日期滚动,接下来我们就会通过 log4j 来模仿这种做法。

log4j 配置文件

#只输出 info 以上的,输出到连个自定义appender: stdout和R.
log4j.rootLogger=info, stdout, R
#输出到控制台,格式是 优先级,后面跟上 日期 类名称 方法名称 换行 优先级 消息 换行
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{MM dd, yyyy HH:mm:ss a} %c %M%n%-5p: %m%n
#输出到每日滚动文件, 文件名称是 logs/catalina. 如果发生滚动,日期格式是 yyyy-MM-dd, 格式同上。
log4j.appender.R=org.apache.log4j.DailyRollingFileAppender
log4j.appender.R.File=logs/catalina
log4j.appender.R.DatePattern='.'yyyy-MM-dd'.log'
log4j.appender.R.layout=org.apache.log4j.PatternLayout
log4j.appender.R.layout.ConversionPattern=%d{MM dd, yyyy HH:mm:ss a} %c %M%n%-5p: %m%n

Bootstrap改造:(1)像 tomcat 那样 一开始打印 jvm 信息。(2)把端口占用提示信息注释掉,当真正异常发生的时候,就会打印出来。

package com.ysy.diytomcat;

import cn.hutool.core.io.*;
import cn.hutool.core.util.*;
import cn.hutool.log.LogFactory;
import cn.hutool.system.*;
import com.ysy.diytomcat.http.*;
import com.ysy.diytomcat.util.*;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.*;

public class Bootstrap {

    public static void main(String[] args) {

        try {
            logJVM();
            int port = 18080;
            //把端口占用提示信息注释掉,当真正异常发生的时候,就会打印出来
//            if (!NetUtil.isUsableLocalPort(port)) {
//                System.out.println(port + " 端口已经被占用了!");
//                return;
//            }
            ServerSocket ss = new ServerSocket(port);

            while (true) {
                Socket s = ss.accept();
                Request request = new Request(s);
                System.out.println("浏览器的输入信息: \r\n" + request.getRequestString());
                System.out.println("uri: " + request.getUri());

                Response response = new Response();
                String uri = request.getUri();
                //首先判断 uri 是否为空,如果为空就不处理了。 什么情况为空呢? 在 TestTomcat 里的 NetUtil.isUsableLocalPort(port) 这段代码就会导致为空。
                if (null == uri)
                    continue;
                System.out.println(uri);
                //如果是 "/", 那么依然返回原字符串。
                if ("/".equals(uri)) {
                    String html = "Hello DIY Tomcat";
                    response.getWriter().println(html);
                } else {
                    //接着处理文件,首先取出文件名,比如访问的是 /a.html, 那么文件名就是 a.html
                    String fileName = StrUtil.removePrefix(uri, "/");
                    //然后获取对应的文件对象 file
                    File file = FileUtil.file(Constant.rootFolder, fileName);
                    if (file.exists()) {
                        //如果文件存在,那么获取内容并通过 response.getWriter 打印。
                        String fileContent = FileUtil.readUtf8String(file);
                        response.getWriter().println(fileContent);
                    } else {
                        //如果文件不存在,那么打印 File Not Found。
                        response.getWriter().println("File Not Found");
                    }
                }
                //把返回 200 响应重构到了一个独立的方法里,看上去更清爽了。
                handle200(s, response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private static void logJVM() {
        Map<String,String> infos = new LinkedHashMap<>();
        infos.put("Server version", "How2J DiyTomcat/1.0.1");
        infos.put("Server built", "2020-04-08 10:20:22");
        infos.put("Server number", "1.0.1");
        infos.put("OS Name\t", SystemUtil.get("os.name"));
        infos.put("OS Version", SystemUtil.get("os.version"));
        infos.put("Architecture", SystemUtil.get("os.arch"));
        infos.put("Java Home", SystemUtil.get("java.home"));
        infos.put("JVM Version", SystemUtil.get("java.runtime.version"));
        infos.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor"));

        Set<String> keys = infos.keySet();
        for (String key : keys) {
            LogFactory.get().info(key+":\t\t" + infos.get(key));
        }
    }
    private static void handle200(Socket s, Response response) throws IOException {
        String contentType = response.getContentType();
        String headText = Constant.response_head_202;
        headText = StrUtil.format(headText, contentType);
        byte[] head = headText.getBytes();
        //根据 response 对象上的 contentType ,组成返回的头信息,并且转换成字节数组。
        byte[] body = response.getBody();
        //获取主题信息部分,即 html 对应的 字节数组。
        byte[] responseBytes = new byte[head.length + body.length];
        //拼接头信息和主题信息,成为一个响应字节数组。
        ArrayUtil.copy(head, 0, responseBytes, 0, head.length);
        ArrayUtil.copy(body, 0, responseBytes, head.length, body.length);

        OutputStream os = s.getOutputStream();
        os.write(responseBytes);
        //close 自动 flush
        s.close();
    }
}

重复运行Bootstrap

08 08, 2022 19:25:54 下午 com.ysy.diytomcat.Bootstrap logJVM
INFO : Server version:		How2J DiyTomcat/1.0.1
08 08, 2022 19:25:54 下午 com.ysy.diytomcat.Bootstrap logJVM
INFO : Server built:		2020-04-08 10:20:22
08 08, 2022 19:25:54 下午 com.ysy.diytomcat.Bootstrap logJVM
INFO : Server number:		1.0.1
08 08, 2022 19:25:54 下午 com.ysy.diytomcat.Bootstrap logJVM
INFO : OS Name	:		Windows 10
08 08, 2022 19:25:54 下午 com.ysy.diytomcat.Bootstrap logJVM
INFO : OS Version:		10.0
08 08, 2022 19:25:54 下午 com.ysy.diytomcat.Bootstrap logJVM
INFO : Architecture:		amd64
08 08, 2022 19:25:54 下午 com.ysy.diytomcat.Bootstrap logJVM
INFO : Java Home:		E:\Java\jdk-1.8\jre
08 08, 2022 19:25:54 下午 com.ysy.diytomcat.Bootstrap logJVM
INFO : JVM Version:		1.8.0_151-b12
08 08, 2022 19:25:54 下午 com.ysy.diytomcat.Bootstrap logJVM
INFO : JVM Vendor:		Oracle Corporation
java.net.BindException: Address already in use: JVM_Bind
	at java.net.DualStackPlainSocketImpl.bind0(Native Method)
	at java.net.DualStackPlainSocketImpl.socketBind(DualStackPlainSocketImpl.java:106)
	at java.net.AbstractPlainSocketImpl.bind(AbstractPlainSocketImpl.java:387)
	at java.net.PlainSocketImpl.bind(PlainSocketImpl.java:190)
	at java.net.ServerSocket.bind(ServerSocket.java:375)
	at java.net.ServerSocket.<init>(ServerSocket.java:237)
	at java.net.ServerSocket.<init>(ServerSocket.java:128)
	at com.ysy.diytomcat.Bootstrap.main(Bootstrap.java:27)

2,多线程,多项目

2.1,耗时任务

耗时任务指的是访问某个页面,比较消耗时间,比如连接数据库什么的。 这里为了简化,故意设计成访问 timeConsume.html会花掉1秒钟。

Bootstrap改造:增加对 timeConsume.html 的判断。

package com.ysy.diytomcat;

import cn.hutool.core.io.*;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.*;
import cn.hutool.log.LogFactory;
import cn.hutool.system.*;
import com.ysy.diytomcat.http.*;
import com.ysy.diytomcat.util.*;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.*;

public class Bootstrap {

    public static void main(String[] args) {

        try {
            logJVM();
            int port = 18080;
            ServerSocket ss = new ServerSocket(port);

            while (true) {
                Socket s = ss.accept();
                Request request = new Request(s);
                System.out.println("浏览器的输入信息: \r\n" + request.getRequestString());
                System.out.println("uri: " + request.getUri());

                Response response = new Response();
                String uri = request.getUri();
                //首先判断 uri 是否为空,如果为空就不处理了。 什么情况为空呢? 在 TestTomcat 里的 NetUtil.isUsableLocalPort(port) 这段代码就会导致为空。
                if (null == uri)
                    continue;
                System.out.println(uri);
                //如果是 "/", 那么依然返回原字符串。
                if ("/".equals(uri)) {
                    String html = "Hello DIY Tomcat";
                    response.getWriter().println(html);
                } else {
                    //接着处理文件,首先取出文件名,比如访问的是 /a.html, 那么文件名就是 a.html
                    String fileName = StrUtil.removePrefix(uri, "/");
                    //然后获取对应的文件对象 file
                    File file = FileUtil.file(Constant.rootFolder, fileName);
                    if (file.exists()) {
                        //如果文件存在,那么获取内容并通过 response.getWriter 打印。
                        String fileContent = FileUtil.readUtf8String(file);
                        response.getWriter().println(fileContent);
                        if(fileName.equals("timeConsume.html")){
                            ThreadUtil.sleep(1000);
                        }
                    } else {
                        //如果文件不存在,那么打印 File Not Found。
                        response.getWriter().println("File Not Found");
                    }
                }
                //把返回 200 响应重构到了一个独立的方法里,看上去更清爽了。
                handle200(s, response);
            }
        } catch (IOException e) {
            LogFactory.get().error(e);
            e.printStackTrace();
        }
    }
    private static void logJVM() {
        Map<String,String> infos = new LinkedHashMap<>();
        infos.put("Server version", "How2J DiyTomcat/1.0.1");
        infos.put("Server built", "2020-04-08 10:20:22");
        infos.put("Server number", "1.0.1");
        infos.put("OS Name\t", SystemUtil.get("os.name"));
        infos.put("OS Version", SystemUtil.get("os.version"));
        infos.put("Architecture", SystemUtil.get("os.arch"));
        infos.put("Java Home", SystemUtil.get("java.home"));
        infos.put("JVM Version", SystemUtil.get("java.runtime.version"));
        infos.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor"));

        Set<String> keys = infos.keySet();
        for (String key : keys) {
            LogFactory.get().info(key+":\t\t" + infos.get(key));
        }
    }
    private static void handle200(Socket s, Response response) throws IOException {
        String contentType = response.getContentType();
        String headText = Constant.response_head_202;
        headText = StrUtil.format(headText, contentType);
        byte[] head = headText.getBytes();
        //根据 response 对象上的 contentType ,组成返回的头信息,并且转换成字节数组。
        byte[] body = response.getBody();
        //获取主题信息部分,即 html 对应的 字节数组。
        byte[] responseBytes = new byte[head.length + body.length];
        //拼接头信息和主题信息,成为一个响应字节数组。
        ArrayUtil.copy(head, 0, responseBytes, 0, head.length);
        ArrayUtil.copy(body, 0, responseBytes, head.length, body.length);

        OutputStream os = s.getOutputStream();
        os.write(responseBytes);
        //close 自动 flush
        s.close();
    }
}

2.2,线程池

当前的服务器是单线程的,处理一个请求之后,才能处理下一个,这样效率肯定是很低的,所以我们要把它改造成多线程的,可以同时处理多个请求。而每当有请求来的时候,就创建一个新的线程,这种方式开销非常大,所以我们要使用 线程池的方式对线程进行重复利用。

首先准备一个工具类 ThreadUtil, 里面创建了一个 ThreadPoolExecutor 对象。

  • 第一个参数 20 表示线程池初始有20个核心线程数。
  • 第二个参数 100 表示最多会有 100个线程。
  • 第三个和第四个参数 表示如果 新增加出来的线程如果空闲时间超过 60秒,那么就会被回收,最后保留 20个线程。
  • 第五个参数 new LinkedBlockingQueue<Runnable>(10), 表示当有很多请求短时间过来,使得20根核心线程都满了之后,并不会马上分配新的线程处理更多的请求, 而是把这些请求放过在 这个LinkedBlockingQueue里, 当核心线程忙过来了,就会来处理 这个队列里的请求。 只有当处理不过来的请求数目超过 了 10个之后,才会增加更多的线程来处理。
package com.ysy.diytomcat.util;

import java.util.concurrent.*;

public class ThreadPoolUtil {
    private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(20, 100, 60, TimeUnit.SECONDS,
            new LinkedBlockingQueue<Runnable>(10));

    public static void run(Runnable r) {
        threadPool.execute(r);
    }
}

改造Bootstrap:当有请求来的时候,就创建一个 Runnable 任务,并且把他丢进线程池运行即可,然后就再去准备接受下一个请求。

package com.ysy.diytomcat;

import cn.hutool.core.io.*;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.*;
import cn.hutool.log.LogFactory;
import cn.hutool.system.*;
import com.ysy.diytomcat.http.*;
import com.ysy.diytomcat.util.*;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.*;

public class Bootstrap {

    public static void main(String[] args) {

        try {
            logJVM();

            int port = 18080;

            ServerSocket ss = new ServerSocket(port);

            while (true) {
                Socket s = ss.accept();
                Runnable r = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Request request = new Request(s);
                            System.out.println("浏览器的输入信息: \r\n" + request.getRequestString());
                            System.out.println("uri: " + request.getUri());

                            Response response = new Response();
                            String uri = request.getUri();
                            //首先判断 uri 是否为空,如果为空就不处理了。 什么情况为空呢? 在 TestTomcat 里的 NetUtil.isUsableLocalPort(port) 这段代码就会导致为空。
                            if (null == uri)
                                return;
                            System.out.println(uri);
                            //如果是 "/", 那么依然返回原字符串。
                            if ("/".equals(uri)) {
                                String html = "Hello DIY Tomcat";
                                response.getWriter().println(html);
                            } else {
                                //接着处理文件,首先取出文件名,比如访问的是 /a.html, 那么文件名就是 a.html
                                String fileName = StrUtil.removePrefix(uri, "/");
                                //然后获取对应的文件对象 file
                                File file = FileUtil.file(Constant.rootFolder, fileName);
                                if (file.exists()) {
                                    //如果文件存在,那么获取内容并通过 response.getWriter 打印。
                                    String fileContent = FileUtil.readUtf8String(file);
                                    response.getWriter().println(fileContent);
                                    if (fileName.equals("timeConsume.html")) {
                                        ThreadUtil.sleep(1000);
                                    }
                                } else {
                                    //如果文件不存在,那么打印 File Not Found。
                                    response.getWriter().println("File Not Found");
                                }
                            }
                            //把返回 200 响应重构到了一个独立的方法里,看上去更清爽了。
                            handle200(s, response);
                        } catch (IOException e) {
                            LogFactory.get().error(e);
                            e.printStackTrace();
                        }
                    }
                };
                ThreadPoolUtil.run(r);
            }
        } catch (IOException e) {
            LogFactory.get().error(e);
            e.printStackTrace();
        }
    }

    private static void logJVM() {
        Map<String, String> infos = new LinkedHashMap<>();
        infos.put("Server version", "How2J DiyTomcat/1.0.1");
        infos.put("Server built", "2020-04-08 10:20:22");
        infos.put("Server number", "1.0.1");
        infos.put("OS Name\t", SystemUtil.get("os.name"));
        infos.put("OS Version", SystemUtil.get("os.version"));
        infos.put("Architecture", SystemUtil.get("os.arch"));
        infos.put("Java Home", SystemUtil.get("java.home"));
        infos.put("JVM Version", SystemUtil.get("java.runtime.version"));
        infos.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor"));

        Set<String> keys = infos.keySet();
        for (String key : keys) {
            LogFactory.get().info(key + ":\t\t" + infos.get(key));
        }
    }

    private static void handle200(Socket s, Response response) throws IOException {
        String contentType = response.getContentType();
        String headText = Constant.response_head_202;
        headText = StrUtil.format(headText, contentType);
        byte[] head = headText.getBytes();
        //根据 response 对象上的 contentType ,组成返回的头信息,并且转换成字节数组。
        byte[] body = response.getBody();
        //获取主题信息部分,即 html 对应的 字节数组。
        byte[] responseBytes = new byte[head.length + body.length];
        //拼接头信息和主题信息,成为一个响应字节数组。
        ArrayUtil.copy(head, 0, responseBytes, 0, head.length);
        ArrayUtil.copy(body, 0, responseBytes, head.length, body.length);

        OutputStream os = s.getOutputStream();
        os.write(responseBytes);
        //close 自动 flush
        s.close();
    }
}

2.3,多应用

目前的做法只支持 ROOT 这么一个应用,那么我们将继续进行改造使得服务器只是多个应用。

  • 首先在 webapps 下新建目录 a, 并在其中放入一个 index.html 。
Hello DIY Tomcat from index.html@a
  • 新建 Context 类,来代表一个应用,它有两个属性, path 和 docBase,以及对应的 getter, setter。path 表示访问的路径;docBase 表示对应在文件系统中的位置;
package com.ysy.diytomcat.catalina;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.TimeInterval;
import cn.hutool.log.LogFactory;

public class Context {
    private String path;
    private String docBase;

    public Context(String path, String docBase) {
        TimeInterval timeInterval = DateUtil.timer();
        this.path = path;
        this.docBase = docBase;
        LogFactory.get().info("Deploying web application directory {}", this.docBase);
        LogFactory.get().info("Deployment of web application directory {} has finished in {} ms", this.docBase, timeInterval.intervalMs());
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public String getDocBase() {
        return docBase;
    }

    public void setDocBase(String docBase) {
        this.docBase = docBase;
    }
}

对 Bootstrap 进行改造

package com.ysy.diytomcat;

import cn.hutool.core.io.*;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.*;
import cn.hutool.log.LogFactory;
import cn.hutool.system.*;
import com.ysy.diytomcat.catalina.Context;
import com.ysy.diytomcat.http.*;
import com.ysy.diytomcat.util.*;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.*;

public class Bootstrap {
    //声明一个 contextMap 用于存放路径和Context 的映射。
    public static Map<String, Context> contextMap = new HashMap<>();

    public static void main(String[] args) {

        try {
            logJVM();
            scanContextsOnWebAppsFolder();
            int port = 18080;
            ServerSocket ss = new ServerSocket(port);

            while (true) {
                Socket s = ss.accept();
                Runnable r = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Request request = new Request(s);
                            System.out.println("浏览器的输入信息: \r\n" + request.getRequestString());
                            System.out.println("uri: " + request.getUri());

                            Response response = new Response();
                            String uri = request.getUri();
                            //首先判断 uri 是否为空,如果为空就不处理了。 什么情况为空呢? 在 TestTomcat 里的 NetUtil.isUsableLocalPort(port) 这段代码就会导致为空。
                            if (null == uri)
                                return;
                            System.out.println(uri);
                            //如果是 "/", 那么依然返回原字符串。
                            if ("/".equals(uri)) {
                                String html = "Hello DIY Tomcat";
                                response.getWriter().println(html);
                            } else {
                                //接着处理文件,首先取出文件名,比如访问的是 /a.html, 那么文件名就是 a.html
                                String fileName = StrUtil.removePrefix(uri, "/");
                                //然后获取对应的文件对象 file
                                File file = FileUtil.file(Constant.rootFolder, fileName);
                                if (file.exists()) {
                                    //如果文件存在,那么获取内容并通过 response.getWriter 打印。
                                    String fileContent = FileUtil.readUtf8String(file);
                                    response.getWriter().println(fileContent);
                                    if (fileName.equals("timeConsume.html")) {
                                        ThreadUtil.sleep(1000);
                                    }
                                } else {
                                    //如果文件不存在,那么打印 File Not Found。
                                    response.getWriter().println("File Not Found");
                                }
                            }
                            //把返回 200 响应重构到了一个独立的方法里,看上去更清爽了。
                            handle200(s, response);
                        } catch (IOException e) {
                            LogFactory.get().error(e);
                            e.printStackTrace();
                        }
                    }
                };
                ThreadPoolUtil.run(r);
            }
        } catch (IOException e) {
            LogFactory.get().error(e);
            e.printStackTrace();
        }
    }

    //创建 scanContextsOnWebAppsFolder 方法,用于扫描 webapps 文件夹下的目录,对这些目录调用 loadContext 进行加载。
    private static void scanContextsOnWebAppsFolder() {
        File[] folders = Constant.webappsFolder.listFiles();
        for (File folder : folders) {
            if (!folder.isDirectory())
                continue;
            loadContext(folder);
        }
    }

    //加载这个目录成为 Context 对象。
    private static void loadContext(File folder) {
        String path = folder.getName();
        if ("ROOT".equals(path))
            path = "/";
        else
            path = "/" + path;

        String docBase = folder.getAbsolutePath();
        Context context = new Context(path, docBase);
        contextMap.put(context.getPath(), context);
    }

    private static void logJVM() {
        Map<String, String> infos = new LinkedHashMap<>();
        infos.put("Server version", "How2J DiyTomcat/1.0.1");
        infos.put("Server built", "2020-04-08 10:20:22");
        infos.put("Server number", "1.0.1");
        infos.put("OS Name\t", SystemUtil.get("os.name"));
        infos.put("OS Version", SystemUtil.get("os.version"));
        infos.put("Architecture", SystemUtil.get("os.arch"));
        infos.put("Java Home", SystemUtil.get("java.home"));
        infos.put("JVM Version", SystemUtil.get("java.runtime.version"));
        infos.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor"));

        Set<String> keys = infos.keySet();
        for (String key : keys) {
            LogFactory.get().info(key + ":\t\t" + infos.get(key));
        }
    }

    private static void handle200(Socket s, Response response) throws IOException {
        String contentType = response.getContentType();
        String headText = Constant.response_head_202;
        headText = StrUtil.format(headText, contentType);
        byte[] head = headText.getBytes();
        //根据 response 对象上的 contentType ,组成返回的头信息,并且转换成字节数组。
        byte[] body = response.getBody();
        //获取主题信息部分,即 html 对应的 字节数组。
        byte[] responseBytes = new byte[head.length + body.length];
        //拼接头信息和主题信息,成为一个响应字节数组。
        ArrayUtil.copy(head, 0, responseBytes, 0, head.length);
        ArrayUtil.copy(body, 0, responseBytes, head.length, body.length);

        OutputStream os = s.getOutputStream();
        os.write(responseBytes);
        //close 自动 flush
        s.close();
    }
}

改造 Request

package com.ysy.diytomcat.http;

import cn.hutool.core.util.StrUtil;
import com.ysy.diytomcat.Bootstrap;
import com.ysy.diytomcat.catalina.Context;
import com.ysy.diytomcat.util.MiniBrowser;

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;

public class Request {

    private String requestString;
    private String uri;
    private Socket socket;
    private Context context;

    public Request(Socket socket) throws IOException {
        this.socket = socket;
        parseHttpRequest();
        if (StrUtil.isEmpty(requestString) || requestString.equals(""))
            return;
        parseUri();
        //在构造方法中调用 parseContext(), 倘若当前 Context 的路径不是 "/", 那么要对 uri进行修正,比如 uri 是 /a/index.html, 获取出来的 Context路径不是 "/”, 那么要修正 uri 为 /index.html。
        parseContext();
        if (!"/".equals(context.getPath()))
            uri = StrUtil.removePrefix(uri, context.getPath());
    }
    //增加解析Context 的方法, 通过获取uri 中的信息来得到 path. 然后根据这个 path 来获取 Context 对象。 如果获取不到,比如 /b/a.html, 对应的 path 是 /b, 是没有对应 Context 的,那么就获取 "/” 对应的 ROOT Context。
    private void parseContext() {
        String path = StrUtil.subBetween(uri, "/", "/");
        if (null == path)
            path = "/";
        else
            path = "/" + path;

        context = Bootstrap.contextMap.get(path);
        if (null == context)
            context = Bootstrap.contextMap.get("/");
    }

    //解析 http请求字符串, 这里面就调用了 MiniBrowser里重构的 readBytes 方法。
    private void parseHttpRequest() throws IOException {
        InputStream is = this.socket.getInputStream();
        byte[] bytes = MiniBrowser.readBytes(is);
        requestString = new String(bytes, "utf-8");
    }

    //解析真实链接
    private void parseUri() {
        String temp = StrUtil.subBetween(requestString, " ", " ");
        //不包括?,链接即是真实链接
        if (!StrUtil.contains(temp, '?')) {
            uri = temp;
            return;
        }
        //包括?,截取?之前的字符串
        temp = StrUtil.subBefore(temp, '?', false);
        uri = temp;
    }

    public Context getContext() {
        return context;
    }

    public String getUri() {
        return uri;
    }

    public String getRequestString() {
        return requestString;
    }

}

2.4,配置型多应用

多应用开发里,是扫描 webapps 文件夹,这节实现像 tomcat 那样通过 配置 server.xml 来实现这个效果。

  • 在项目下新建个 conf 目录,然后新建 server.xml。
<?xml version="1.0" encoding="UTF-8"?>
<Server>
    <Context path="/b" docBase="d:/project/diytomcat/b" />
</Server>
  • 在 E:/IDEA/workspace/DiyTomcat/webapps/ 创建 b目录,并且在其中存放 index.html, 内容如下:Hello DIY Tomcat from index.html@b
  • 修改Constant,增加两个常量,用于定位 server.xml
public static final File confFolder = new File(SystemUtil.get("user.dir"),"conf");
public static final File serverXmlFile = new File(confFolder, "server.xml");
  • 准备工具类 ServerXMLUtil,这个工具类使用了第三方工具 jsoup。
package com.ysy.diytomcat.util;

import cn.hutool.core.io.FileUtil;
import com.ysy.diytomcat.catalina.Context;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import java.util.ArrayList;
import java.util.List;

public class ServerXMLUtil {
    public static List<Context> getContexts() {
        List<Context> result = new ArrayList<>();
        //获取 server.xml 的内容
        String xml = FileUtil.readUtf8String(Constant.serverXmlFile);
        //转换成 jsoup document
        Document d = Jsoup.parse(xml);
        //查询所有的 Context 节点
        Elements es = d.select("Context");
        //遍历这些节点,并获取对应的 path和docBase ,以生成 Context 对象, 然后放进 result 返回。
        for (Element e : es) {
            String path = e.attr("path");
            String docBase = e.attr("docBase");
            Context context = new Context(path, docBase);
            result.add(context);
        }
        return result;
    }
}
  • 修改Bootstrap
package com.ysy.diytomcat;

import cn.hutool.core.io.*;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.*;
import cn.hutool.log.LogFactory;
import cn.hutool.system.*;
import com.ysy.diytomcat.catalina.Context;
import com.ysy.diytomcat.http.*;
import com.ysy.diytomcat.util.*;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.*;

public class Bootstrap {
    //声明一个 contextMap 用于存放路径和Context 的映射。
    public static Map<String, Context> contextMap = new HashMap<>();

    public static void main(String[] args) {

        try {
            logJVM();
            scanContextsOnWebAppsFolder();
            scanContextsInServerXML();
            int port = 18080;
            ServerSocket ss = new ServerSocket(port);

            while (true) {
                Socket s = ss.accept();
                Runnable r = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Request request = new Request(s);
                            System.out.println("浏览器的输入信息: \r\n" + request.getRequestString());
                            System.out.println("uri: " + request.getUri());

                            Response response = new Response();
                            String uri = request.getUri();
                            //首先判断 uri 是否为空,如果为空就不处理了。 什么情况为空呢? 在 TestTomcat 里的 NetUtil.isUsableLocalPort(port) 这段代码就会导致为空。
                            if (null == uri)
                                return;
                            System.out.println(uri);
                            //如果是 "/", 那么依然返回原字符串。
                            Context context = request.getContext();
                            if ("/".equals(uri)) {
                                String html = "Hello DIY Tomcat";
                                response.getWriter().println(html);
                            } else {
                                //接着处理文件,首先取出文件名,比如访问的是 /a.html, 那么文件名就是 a.html
                                String fileName = StrUtil.removePrefix(uri, "/");
                                //然后获取对应的文件对象 file,在判断 uri 之前获取当前context对象
                                File file = FileUtil.file(context.getDocBase(), fileName);
                                if (file.exists()) {
                                    //如果文件存在,那么获取内容并通过 response.getWriter 打印。
                                    String fileContent = FileUtil.readUtf8String(file);
                                    response.getWriter().println(fileContent);
                                    if (fileName.equals("timeConsume.html")) {
                                        ThreadUtil.sleep(1000);
                                    }
                                } else {
                                    //如果文件不存在,那么打印 File Not Found。
                                    response.getWriter().println("File Not Found");
                                }
                            }
                            //把返回 200 响应重构到了一个独立的方法里,看上去更清爽了。
                            handle200(s, response);
                        } catch (IOException e) {
                            LogFactory.get().error(e);
                            e.printStackTrace();
                        }
                    }
                };
                ThreadPoolUtil.run(r);
            }
        } catch (IOException e) {
            LogFactory.get().error(e);
            e.printStackTrace();
        }
    }

    //创建scanContextsInServerXML, 通过 ServerXMLUtil 获取 context, 放进 contextMap里。
    private static void scanContextsInServerXML() {
        List<Context> contexts = ServerXMLUtil.getContexts();
        for (Context context : contexts) {
            contextMap.put(context.getPath(), context);
        }
    }

    //创建 scanContextsOnWebAppsFolder 方法,用于扫描 webapps 文件夹下的目录,对这些目录调用 loadContext 进行加载。
    private static void scanContextsOnWebAppsFolder() {
        File[] folders = Constant.webappsFolder.listFiles();
        for (File folder : folders) {
            if (!folder.isDirectory())
                continue;
            loadContext(folder);
        }
    }

    //加载这个目录成为 Context 对象。
    private static void loadContext(File folder) {
        String path = folder.getName();
        if ("ROOT".equals(path))
            path = "/";
        else
            path = "/" + path;

        String docBase = folder.getAbsolutePath();
        Context context = new Context(path, docBase);

        contextMap.put(context.getPath(), context);
    }

    private static void logJVM() {
        Map<String, String> infos = new LinkedHashMap<>();
        infos.put("Server version", "How2J DiyTomcat/1.0.1");
        infos.put("Server built", "2020-04-08 10:20:22");
        infos.put("Server number", "1.0.1");
        infos.put("OS Name\t", SystemUtil.get("os.name"));
        infos.put("OS Version", SystemUtil.get("os.version"));
        infos.put("Architecture", SystemUtil.get("os.arch"));
        infos.put("Java Home", SystemUtil.get("java.home"));
        infos.put("JVM Version", SystemUtil.get("java.runtime.version"));
        infos.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor"));

        Set<String> keys = infos.keySet();
        for (String key : keys) {
            LogFactory.get().info(key + ":\t\t" + infos.get(key));
        }
    }

    private static void handle200(Socket s, Response response) throws IOException {
        String contentType = response.getContentType();
        String headText = Constant.response_head_202;
        headText = StrUtil.format(headText, contentType);
        byte[] head = headText.getBytes();
        //根据 response 对象上的 contentType ,组成返回的头信息,并且转换成字节数组。
        byte[] body = response.getBody();
        //获取主题信息部分,即 html 对应的 字节数组。
        byte[] responseBytes = new byte[head.length + body.length];
        //拼接头信息和主题信息,成为一个响应字节数组。
        ArrayUtil.copy(head, 0, responseBytes, 0, head.length);
        ArrayUtil.copy(body, 0, responseBytes, head.length, body.length);

        OutputStream os = s.getOutputStream();
        os.write(responseBytes);
        //close 自动 flush
        s.close();
    }
}

3,Tomcat内置对象

3.1,Host

修改Server.xml:Host 的意思是虚拟主机,通常都是 localhost,即表示本机。

<?xml version="1.0" encoding="UTF-8"?>
<Server>
    <Host name="localhost">
        <Context path="/b" docBase="E:/IDEA/workspace/DiyTomcat/webapps/b"/>
    </Host>
</Server>

修改ServerXMLUtil:增加一个 getHostName 来解析 host 元素下的 name 属性。

package com.ysy.diytomcat.util;

import cn.hutool.core.io.FileUtil;
import com.ysy.diytomcat.catalina.Context;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import java.util.ArrayList;
import java.util.List;

public class ServerXMLUtil {
    public static List<Context> getContexts() {
        List<Context> result = new ArrayList<>();
        //获取 server.xml 的内容
        String xml = FileUtil.readUtf8String(Constant.serverXmlFile);
        //转换成 jsoup document
        Document d = Jsoup.parse(xml);
        //查询所有的 Context 节点
        Elements es = d.select("Context");
        //遍历这些节点,并获取对应的 path和docBase ,以生成 Context 对象, 然后放进 result 返回。
        for (Element e : es) {
            String path = e.attr("path");
            String docBase = e.attr("docBase");
            Context context = new Context(path, docBase);
            result.add(context);
        }
        return result;
    }

    public static String getHostName() {
        String xml = FileUtil.readUtf8String(Constant.serverXmlFile);
        Document d = Jsoup.parse(xml);

        Element host = d.select("Host").first();
        return host.attr("name");
    }
}

创建 host类:有 name 和 contextMap 属性。

package com.ysy.diytomcat.catalina;

import com.ysy.diytomcat.util.Constant;
import com.ysy.diytomcat.util.ServerXMLUtil;

import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Host {
    private String name;
    //contextMap 其实就是本来在 bootstrap 里的 contextMap , 只不过挪到这里来了。
    private Map<String, Context> contextMap;

    public Host() {
        this.contextMap = new HashMap<>();
        this.name = ServerXMLUtil.getHostName();

        scanContextsOnWebAppsFolder();
        scanContextsInServerXML();

    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    private void scanContextsInServerXML() {
        List<Context> contexts = ServerXMLUtil.getContexts();
        for (Context context : contexts) {
            contextMap.put(context.getPath(), context);
        }
    }

    private void scanContextsOnWebAppsFolder() {
        File[] folders = Constant.webappsFolder.listFiles();
        for (File folder : folders) {
            if (!folder.isDirectory())
                continue;
            loadContext(folder);
        }
    }

    private void loadContext(File folder) {
        String path = folder.getName();
        if ("ROOT".equals(path))
            path = "/";
        else
            path = "/" + path;

        String docBase = folder.getAbsolutePath();
        Context context = new Context(path, docBase);

        contextMap.put(context.getPath(), context);
    }

    public Context getContext(String path) {
        return contextMap.get(path);
    }
}

修改Request:因为 Request 以前是从 Bootstrap.contextMap 里获取 context 对象的,伴随 contextMap 移动到 Host ,为了能够实现原来的功能,需要在 Request 里增加个 host对象。

package com.ysy.diytomcat.http;

import cn.hutool.core.util.StrUtil;
import com.ysy.diytomcat.catalina.Context;
import com.ysy.diytomcat.catalina.Host;
import com.ysy.diytomcat.util.MiniBrowser;

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;

public class Request {

    private String requestString;
    private String uri;
    private Socket socket;
    private Context context;
    private Host host;

    public Request(Socket socket, Host host) throws IOException {
        this.socket = socket;
        this.host = host;
        parseHttpRequest();
        if (StrUtil.isEmpty(requestString) || requestString.equals(""))
            return;
        parseUri();
        //在构造方法中调用 parseContext(), 倘若当前 Context 的路径不是 "/", 那么要对 uri进行修正,比如 uri 是 /a/index.html, 获取出来的 Context路径不是 "/”, 那么要修正 uri 为 /index.html。
        parseContext();
        if (!"/".equals(context.getPath()))
            uri = StrUtil.removePrefix(uri, context.getPath());
    }

    //增加解析Context 的方法, 通过获取uri 中的信息来得到 path. 然后根据这个 path 来获取 Context 对象。 如果获取不到,比如 /b/a.html, 对应的 path 是 /b, 是没有对应 Context 的,那么就获取 "/” 对应的 ROOT Context。
    private void parseContext() {
        String path = StrUtil.subBetween(uri, "/", "/");
        if (null == path)
            path = "/";
        else
            path = "/" + path;

        context = host.getContext(path);
        if (null == context)
            context = host.getContext("/");
    }

    //解析 http请求字符串, 这里面就调用了 MiniBrowser里重构的 readBytes 方法。
    private void parseHttpRequest() throws IOException {
        InputStream is = this.socket.getInputStream();
        byte[] bytes = MiniBrowser.readBytes(is);
        requestString = new String(bytes, "utf-8");
    }

    //解析真实链接
    private void parseUri() {
        String temp = StrUtil.subBetween(requestString, " ", " ");
        //不包括?,链接即是真实链接
        if (!StrUtil.contains(temp, '?')) {
            uri = temp;
            return;
        }
        //包括?,截取?之前的字符串
        temp = StrUtil.subBefore(temp, '?', false);
        uri = temp;
    }

    public Context getContext() {
        return context;
    }

    public String getUri() {
        return uri;
    }

    public String getRequestString() {
        return requestString;
    }

}

修改Bootstrap:伴随着 contextMap 移动到host对象,Bootstrap 就把 contextMap 相关的代码都移动过去了。

package com.ysy.diytomcat;

import cn.hutool.core.io.*;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.*;
import cn.hutool.log.LogFactory;
import cn.hutool.system.*;
import com.ysy.diytomcat.catalina.Context;
import com.ysy.diytomcat.catalina.Host;
import com.ysy.diytomcat.http.*;
import com.ysy.diytomcat.util.*;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.*;

public class Bootstrap {
    //声明一个 contextMap 用于存放路径和Context 的映射。
    public static Map<String, Context> contextMap = new HashMap<>();

    public static void main(String[] args) {

        try {
            logJVM();
            int port = 18080;
            Host host = new Host();
            ServerSocket ss = new ServerSocket(port);

            while (true) {
                Socket s = ss.accept();
                Runnable r = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Request request = new Request(s, host);
                            System.out.println("浏览器的输入信息: \r\n" + request.getRequestString());
                            System.out.println("uri: " + request.getUri());

                            Response response = new Response();
                            String uri = request.getUri();
                            //首先判断 uri 是否为空,如果为空就不处理了。 什么情况为空呢? 在 TestTomcat 里的 NetUtil.isUsableLocalPort(port) 这段代码就会导致为空。
                            if (null == uri)
                                return;
                            System.out.println(uri);
                            //如果是 "/", 那么依然返回原字符串。
                            Context context = request.getContext();
                            if ("/".equals(uri)) {
                                String html = "Hello DIY Tomcat";
                                response.getWriter().println(html);
                            } else {
                                //接着处理文件,首先取出文件名,比如访问的是 /a.html, 那么文件名就是 a.html
                                String fileName = StrUtil.removePrefix(uri, "/");
                                //然后获取对应的文件对象 file,在判断 uri 之前获取当前context对象
                                File file = FileUtil.file(context.getDocBase(), fileName);
                                if (file.exists()) {
                                    //如果文件存在,那么获取内容并通过 response.getWriter 打印。
                                    String fileContent = FileUtil.readUtf8String(file);
                                    response.getWriter().println(fileContent);
                                    if (fileName.equals("timeConsume.html")) {
                                        ThreadUtil.sleep(1000);
                                    }
                                } else {
                                    //如果文件不存在,那么打印 File Not Found。
                                    response.getWriter().println("File Not Found");
                                }
                            }
                            //把返回 200 响应重构到了一个独立的方法里,看上去更清爽了。
                            handle200(s, response);
                        } catch (IOException e) {
                            LogFactory.get().error(e);
                            e.printStackTrace();
                        }
                    }
                };
                ThreadPoolUtil.run(r);
            }
        } catch (IOException e) {
            LogFactory.get().error(e);
            e.printStackTrace();
        }
    }

    private static void logJVM() {
        Map<String, String> infos = new LinkedHashMap<>();
        infos.put("Server version", "How2J DiyTomcat/1.0.1");
        infos.put("Server built", "2020-04-08 10:20:22");
        infos.put("Server number", "1.0.1");
        infos.put("OS Name\t", SystemUtil.get("os.name"));
        infos.put("OS Version", SystemUtil.get("os.version"));
        infos.put("Architecture", SystemUtil.get("os.arch"));
        infos.put("Java Home", SystemUtil.get("java.home"));
        infos.put("JVM Version", SystemUtil.get("java.runtime.version"));
        infos.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor"));

        Set<String> keys = infos.keySet();
        for (String key : keys) {
            LogFactory.get().info(key + ":\t\t" + infos.get(key));
        }
    }

    private static void handle200(Socket s, Response response) throws IOException {
        String contentType = response.getContentType();
        String headText = Constant.response_head_202;
        headText = StrUtil.format(headText, contentType);
        byte[] head = headText.getBytes();
        //根据 response 对象上的 contentType ,组成返回的头信息,并且转换成字节数组。
        byte[] body = response.getBody();
        //获取主题信息部分,即 html 对应的 字节数组。
        byte[] responseBytes = new byte[head.length + body.length];
        //拼接头信息和主题信息,成为一个响应字节数组。
        ArrayUtil.copy(head, 0, responseBytes, 0, head.length);
        ArrayUtil.copy(body, 0, responseBytes, head.length, body.length);

        OutputStream os = s.getOutputStream();
        os.write(responseBytes);
        //close 自动 flush
        s.close();
    }
}

3.2,Engine

在 tomcat 里 Engine表示 servlet 引擎,用来处理 servlet 的请求。在 Host 上增加 Engine 节点, 它有个defaultHost="localhost" 表示默认的 Host 是名称是 "localhost" 的 Host。

修改 server.xml

<?xml version="1.0" encoding="UTF-8"?>
<Server>
    <Engine defaultHost="localhost">
        <Host name = "localhost">
            <Context path="/b" docBase="E:/IDEA/workspace/DiyTomcat/webapps/b" />
        </Host>
    </Engine>
</Server>

创建Engine

package com.ysy.diytomcat.catalina;

import com.ysy.diytomcat.util.ServerXMLUtil;
import java.util.List;

public class Engine {
    private String defaultHost;
    private List<Host> hosts;

    public Engine() {
        this.defaultHost = ServerXMLUtil.getEngineDefaultHost();
        this.hosts = ServerXMLUtil.getHosts(this);
        checkDefault();
    }

    private void checkDefault() {
        if (null == getDefaultHost())
            throw new RuntimeException("the defaultHost" + defaultHost + " does not exist!");
    }

    public Host getDefaultHost() {
        for (Host host : hosts) {
            if (host.getName().equals(defaultHost))
                return host;
        }
        return null;
    }
}

修改Host

package com.ysy.diytomcat.catalina;

import com.ysy.diytomcat.util.Constant;
import com.ysy.diytomcat.util.ServerXMLUtil;

import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Host {
    private String name;
    //contextMap 其实就是本来在 bootstrap 里的 contextMap , 只不过挪到这里来了。
    private Map<String, Context> contextMap;
    //增加 Engine 属性
    private Engine engine;

    public Host() {
        this.contextMap = new HashMap<>();
        this.name = ServerXMLUtil.getHostName();
        //在构造方法中接受它
        this.engine = engine;
        scanContextsOnWebAppsFolder();
        scanContextsInServerXML();

    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    //创建scanContextsInServerXML, 通过 ServerXMLUtil 获取 context, 放进 contextMap里。
    private void scanContextsInServerXML() {
        List<Context> contexts = ServerXMLUtil.getContexts();
        for (Context context : contexts) {
            contextMap.put(context.getPath(), context);
        }
    }

    //创建 scanContextsOnWebAppsFolder 方法,用于扫描 webapps 文件夹下的目录,对这些目录调用 loadContext 进行加载。
    private void scanContextsOnWebAppsFolder() {
        File[] folders = Constant.webappsFolder.listFiles();
        for (File folder : folders) {
            if (!folder.isDirectory())
                continue;
            loadContext(folder);
        }
    }

    //加载这个目录成为 Context 对象。
    private void loadContext(File folder) {
        String path = folder.getName();
        if ("ROOT".equals(path))
            path = "/";
        else
            path = "/" + path;

        String docBase = folder.getAbsolutePath();
        Context context = new Context(path, docBase);

        contextMap.put(context.getPath(), context);
    }

    public Context getContext(String path) {
        return contextMap.get(path);
    }
}

修改 ServerXMLUtil

package com.ysy.diytomcat.util;

import cn.hutool.core.io.FileUtil;
import com.ysy.diytomcat.catalina.*;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import java.util.ArrayList;
import java.util.List;

public class ServerXMLUtil {
    public static List<Context> getContexts() {
        List<Context> result = new ArrayList<>();
        //获取 server.xml 的内容
        String xml = FileUtil.readUtf8String(Constant.serverXmlFile);
        //转换成 jsoup document
        Document d = Jsoup.parse(xml);
        //查询所有的 Context 节点
        Elements es = d.select("Context");
        //遍历这些节点,并获取对应的 path和docBase ,以生成 Context 对象, 然后放进 result 返回。
        for (Element e : es) {
            String path = e.attr("path");
            String docBase = e.attr("docBase");
            Context context = new Context(path, docBase);
            result.add(context);
        }
        return result;
    }

    public static String getEngineDefaultHost() {
        String xml = FileUtil.readUtf8String(Constant.serverXmlFile);
        Document d = Jsoup.parse(xml);

        Element host = d.select("Engine").first();
        return host.attr("defaultHost");
    }

    public static List<Host> getHosts(Engine engine) {
        List<Host> result = new ArrayList<>();
        String xml = FileUtil.readUtf8String(Constant.serverXmlFile);
        Document d = Jsoup.parse(xml);

        Elements es = d.select("Host");
        for (Element e : es) {
            String name = e.attr("name");
            Host host = new Host(name, engine);
            result.add(host);
        }
        return result;
    }

    public static String getHostName() {
        String xml = FileUtil.readUtf8String(Constant.serverXmlFile);
        Document d = Jsoup.parse(xml);

        Element host = d.select("Host").first();
        return host.attr("name");
    }
}

修改Request

package com.ysy.diytomcat.http;

import cn.hutool.core.util.StrUtil;
import com.ysy.diytomcat.catalina.Context;
import com.ysy.diytomcat.catalina.Engine;
import com.ysy.diytomcat.catalina.Host;
import com.ysy.diytomcat.util.MiniBrowser;

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;

public class Request {

    private String requestString;
    private String uri;
    private Socket socket;
    private Context context;

    private Engine engine;

    public Request(Socket socket, Engine engine) throws IOException {
        this.socket = socket;
        this.engine = engine;
        parseHttpRequest();
        if (StrUtil.isEmpty(requestString) || requestString.equals(""))
            return;
        parseUri();
        //在构造方法中调用 parseContext(), 倘若当前 Context 的路径不是 "/", 那么要对 uri进行修正,比如 uri 是 /a/index.html, 获取出来的 Context路径不是 "/”, 那么要修正 uri 为 /index.html。
        parseContext();
        if (!"/".equals(context.getPath()))
            uri = StrUtil.removePrefix(uri, context.getPath());
    }

    //增加解析Context 的方法, 通过获取uri 中的信息来得到 path. 然后根据这个 path 来获取 Context 对象。 如果获取不到,比如 /b/a.html, 对应的 path 是 /b, 是没有对应 Context 的,那么就获取 "/” 对应的 ROOT Context。
    private void parseContext() {
        String path = StrUtil.subBetween(uri, "/", "/");
        if (null == path)
            path = "/";
        else
            path = "/" + path;

        context = engine.getDefaultHost().getContext(path);
        if (null == context)
            context = engine.getDefaultHost().getContext("/");
    }

    //解析 http请求字符串, 这里面就调用了 MiniBrowser里重构的 readBytes 方法。
    private void parseHttpRequest() throws IOException {
        InputStream is = this.socket.getInputStream();
        byte[] bytes = MiniBrowser.readBytes(is);
        requestString = new String(bytes, "utf-8");
    }

    //解析真实链接
    private void parseUri() {
        String temp = StrUtil.subBetween(requestString, " ", " ");
        //不包括?,链接即是真实链接
        if (!StrUtil.contains(temp, '?')) {
            uri = temp;
            return;
        }
        //包括?,截取?之前的字符串
        temp = StrUtil.subBefore(temp, '?', false);
        uri = temp;
    }

    public Context getContext() {
        return context;
    }

    public String getUri() {
        return uri;
    }

    public String getRequestString() {
        return requestString;
    }

}

修改Bootstrap

package com.ysy.diytomcat;

import cn.hutool.core.io.*;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.*;
import cn.hutool.log.LogFactory;
import cn.hutool.system.*;
import com.ysy.diytomcat.catalina.Context;
import com.ysy.diytomcat.catalina.Engine;
import com.ysy.diytomcat.catalina.Host;
import com.ysy.diytomcat.http.*;
import com.ysy.diytomcat.util.*;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.*;

public class Bootstrap {
    //声明一个 contextMap 用于存放路径和Context 的映射。
    public static Map<String, Context> contextMap = new HashMap<>();

    public static void main(String[] args) {

        try {
            logJVM();
            int port = 18080;
            //由开始创建 Host 对象变成现在的 创建 Engine 对象
            Engine engine = new Engine();
            ServerSocket ss = new ServerSocket(port);

            while (true) {
                Socket s = ss.accept();
                Runnable r = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Request request = new Request(s, engine);
                            System.out.println("浏览器的输入信息: \r\n" + request.getRequestString());
                            System.out.println("uri: " + request.getUri());

                            Response response = new Response();
                            String uri = request.getUri();
                            //首先判断 uri 是否为空,如果为空就不处理了。 什么情况为空呢? 在 TestTomcat 里的 NetUtil.isUsableLocalPort(port) 这段代码就会导致为空。
                            if (null == uri)
                                return;
                            System.out.println(uri);
                            //如果是 "/", 那么依然返回原字符串。
                            Context context = request.getContext();
                            if ("/".equals(uri)) {
                                String html = "Hello DIY Tomcat";
                                response.getWriter().println(html);
                            } else {
                                //接着处理文件,首先取出文件名,比如访问的是 /a.html, 那么文件名就是 a.html
                                String fileName = StrUtil.removePrefix(uri, "/");
                                //然后获取对应的文件对象 file,在判断 uri 之前获取当前context对象
                                File file = FileUtil.file(context.getDocBase(), fileName);
                                if (file.exists()) {
                                    //如果文件存在,那么获取内容并通过 response.getWriter 打印。
                                    String fileContent = FileUtil.readUtf8String(file);
                                    response.getWriter().println(fileContent);
                                    if (fileName.equals("timeConsume.html")) {
                                        ThreadUtil.sleep(1000);
                                    }
                                } else {
                                    //如果文件不存在,那么打印 File Not Found。
                                    response.getWriter().println("File Not Found");
                                }
                            }
                            //把返回 200 响应重构到了一个独立的方法里,看上去更清爽了。
                            handle200(s, response);
                        } catch (IOException e) {
                            LogFactory.get().error(e);
                            e.printStackTrace();
                        }
                    }
                };
                ThreadPoolUtil.run(r);
            }
        } catch (IOException e) {
            LogFactory.get().error(e);
            e.printStackTrace();
        }
    }

    private static void logJVM() {
        Map<String, String> infos = new LinkedHashMap<>();
        infos.put("Server version", "How2J DiyTomcat/1.0.1");
        infos.put("Server built", "2020-04-08 10:20:22");
        infos.put("Server number", "1.0.1");
        infos.put("OS Name\t", SystemUtil.get("os.name"));
        infos.put("OS Version", SystemUtil.get("os.version"));
        infos.put("Architecture", SystemUtil.get("os.arch"));
        infos.put("Java Home", SystemUtil.get("java.home"));
        infos.put("JVM Version", SystemUtil.get("java.runtime.version"));
        infos.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor"));

        Set<String> keys = infos.keySet();
        for (String key : keys) {
            LogFactory.get().info(key + ":\t\t" + infos.get(key));
        }
    }
    private static void handle200(Socket s, Response response) throws IOException {
        String contentType = response.getContentType();
        String headText = Constant.response_head_202;
        headText = StrUtil.format(headText, contentType);
        byte[] head = headText.getBytes();
        //根据 response 对象上的 contentType ,组成返回的头信息,并且转换成字节数组。
        byte[] body = response.getBody();
        //获取主题信息部分,即 html 对应的 字节数组。
        byte[] responseBytes = new byte[head.length + body.length];
        //拼接头信息和主题信息,成为一个响应字节数组。
        ArrayUtil.copy(head, 0, responseBytes, 0, head.length);
        ArrayUtil.copy(body, 0, responseBytes, head.length, body.length);

        OutputStream os = s.getOutputStream();
        os.write(responseBytes);
        //close 自动 flush
        s.close();
    }
}

3.3,Service

Service 是 Engine 的父节点,用于代表 tomcat 提供的服务。 

修改server.xml:在Engine 上面增加个 Service 节点。

<?xml version="1.0" encoding="UTF-8"?>
<Server>
    <Service name="Catalina">
        <Engine defaultHost="localhost">
            <Host name="localhost">
                <Context path="/b" docBase="E:/IDEA/workspace/DiyTomcat/webapps/b"/>
            </Host>
        </Engine>
    </Service>
</Server>

ServerXMLUtil中添加getServiceName():正好之前的 getHostName 不用了,改造成 getServiceName。

public static String getServiceName() {
    String xml = FileUtil.readUtf8String(Constant.serverXmlFile);
    Document d = Jsoup.parse(xml);
    Element host = d.select("Service").first();
    return host.attr("name");
}

新增Service类:有name 和Engine 属性。一个 Service 下通常只有一个 Engine, 就不做成 List<Engine> 集合了。

package com.ysy.diytomcat.catalina;
import com.ysy.diytomcat.util.ServerXMLUtil;

public class Service {
    private String name;
    private Engine engine;

    public Service() {
        this.name = ServerXMLUtil.getServiceName();
        this.engine = new Engine(this);
    }

    public Engine getEngine() {
        return engine;
    }
}

修改Engine:改造一下,增加 Service 属性。

package com.ysy.diytomcat.catalina;

import com.ysy.diytomcat.util.ServerXMLUtil;

import java.util.List;

public class Engine {
    private String defaultHost;
    private List<Host> hosts;
    private Service service;

    public Engine(Service service) {
        this.defaultHost = ServerXMLUtil.getEngineDefaultHost();
        this.hosts = ServerXMLUtil.getHosts(this);
        this.service = service;
        checkDefault();
    }

    private void checkDefault() {
        if (null == getDefaultHost())
            throw new RuntimeException("the defaultHost" + defaultHost + " does not exist!");
    }

    public Service getService() {
        return service;
    }

    public Host getDefaultHost() {
        for (Host host : hosts) {
            if (host.getName().equals(defaultHost))
                return host;
        }
        return null;
    }
}

修改Request:就像当初抛弃从 Host 切换到 Engine 一样,现在从 Engine 切换到 Service。

package com.ysy.diytomcat.http;

import cn.hutool.core.util.StrUtil;
import com.ysy.diytomcat.catalina.Context;
import com.ysy.diytomcat.catalina.Engine;
import com.ysy.diytomcat.catalina.Host;
import com.ysy.diytomcat.catalina.Service;
import com.ysy.diytomcat.util.MiniBrowser;

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;

public class Request {

    private String requestString;
    private String uri;
    private Socket socket;
    private Context context;

    private Service service;

    public Request(Socket socket, Service service) throws IOException {
        this.socket = socket;
        this.service = service;
        parseHttpRequest();
        if (StrUtil.isEmpty(requestString) || requestString.equals(""))
            return;
        parseUri();
        //在构造方法中调用 parseContext(), 倘若当前 Context 的路径不是 "/", 那么要对 uri进行修正,比如 uri 是 /a/index.html, 获取出来的 Context路径不是 "/”, 那么要修正 uri 为 /index.html。
        parseContext();
        if (!"/".equals(context.getPath()))
            uri = StrUtil.removePrefix(uri, context.getPath());
    }

    //增加解析Context 的方法, 通过获取uri 中的信息来得到 path. 然后根据这个 path 来获取 Context 对象。 如果获取不到,比如 /b/a.html, 对应的 path 是 /b, 是没有对应 Context 的,那么就获取 "/” 对应的 ROOT Context。
    private void parseContext() {
        String path = StrUtil.subBetween(uri, "/", "/");
        if (null == path)
            path = "/";
        else
            path = "/" + path;

        context = service.getEngine().getDefaultHost().getContext(path);
        if (null == context)
            context = service.getEngine().getDefaultHost().getContext("/");
    }

    //解析 http请求字符串, 这里面就调用了 MiniBrowser里重构的 readBytes 方法。
    private void parseHttpRequest() throws IOException {
        InputStream is = this.socket.getInputStream();
        byte[] bytes = MiniBrowser.readBytes(is);
        requestString = new String(bytes, "utf-8");
    }

    //解析真实链接
    private void parseUri() {
        String temp = StrUtil.subBetween(requestString, " ", " ");
        //不包括?,链接即是真实链接
        if (!StrUtil.contains(temp, '?')) {
            uri = temp;
            return;
        }
        //包括?,截取?之前的字符串
        temp = StrUtil.subBefore(temp, '?', false);
        uri = temp;
    }

    public Context getContext() {
        return context;
    }

    public String getUri() {
        return uri;
    }

    public String getRequestString() {
        return requestString;
    }

}

修改 Bootstrap

package com.ysy.diytomcat;

import cn.hutool.core.io.*;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.*;
import cn.hutool.log.LogFactory;
import cn.hutool.system.*;
import com.ysy.diytomcat.catalina.Context;
import com.ysy.diytomcat.catalina.Engine;
import com.ysy.diytomcat.catalina.Host;
import com.ysy.diytomcat.catalina.Service;
import com.ysy.diytomcat.http.*;
import com.ysy.diytomcat.util.*;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.*;

public class Bootstrap {
    //声明一个 contextMap 用于存放路径和Context 的映射。
    public static Map<String, Context> contextMap = new HashMap<>();

    public static void main(String[] args) {

        try {
            logJVM();
            int port = 18080;
            Service service = new Service();
            ServerSocket ss = new ServerSocket(port);

            while (true) {
                Socket s = ss.accept();
                Runnable r = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Request request = new Request(s, service);
                            System.out.println("浏览器的输入信息: \r\n" + request.getRequestString());
                            System.out.println("uri: " + request.getUri());

                            Response response = new Response();
                            String uri = request.getUri();
                            //首先判断 uri 是否为空,如果为空就不处理了。 什么情况为空呢? 在 TestTomcat 里的 NetUtil.isUsableLocalPort(port) 这段代码就会导致为空。
                            if (null == uri)
                                return;
                            System.out.println(uri);
                            //如果是 "/", 那么依然返回原字符串。
                            Context context = request.getContext();
                            if ("/".equals(uri)) {
                                String html = "Hello DIY Tomcat";
                                response.getWriter().println(html);
                            } else {
                                //接着处理文件,首先取出文件名,比如访问的是 /a.html, 那么文件名就是 a.html
                                String fileName = StrUtil.removePrefix(uri, "/");
                                //然后获取对应的文件对象 file,在判断 uri 之前获取当前context对象
                                File file = FileUtil.file(context.getDocBase(), fileName);
                                if (file.exists()) {
                                    //如果文件存在,那么获取内容并通过 response.getWriter 打印。
                                    String fileContent = FileUtil.readUtf8String(file);
                                    response.getWriter().println(fileContent);
                                    if (fileName.equals("timeConsume.html")) {
                                        ThreadUtil.sleep(1000);
                                    }
                                } else {
                                    //如果文件不存在,那么打印 File Not Found。
                                    response.getWriter().println("File Not Found");
                                }
                            }
                            //把返回 200 响应重构到了一个独立的方法里,看上去更清爽了。
                            handle200(s, response);
                        } catch (IOException e) {
                            LogFactory.get().error(e);
                            e.printStackTrace();
                        }
                    }
                };
                ThreadPoolUtil.run(r);
            }
        } catch (IOException e) {
            LogFactory.get().error(e);
            e.printStackTrace();
        }
    }

    private static void logJVM() {
        Map<String, String> infos = new LinkedHashMap<>();
        infos.put("Server version", "How2J DiyTomcat/1.0.1");
        infos.put("Server built", "2020-04-08 10:20:22");
        infos.put("Server number", "1.0.1");
        infos.put("OS Name\t", SystemUtil.get("os.name"));
        infos.put("OS Version", SystemUtil.get("os.version"));
        infos.put("Architecture", SystemUtil.get("os.arch"));
        infos.put("Java Home", SystemUtil.get("java.home"));
        infos.put("JVM Version", SystemUtil.get("java.runtime.version"));
        infos.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor"));

        Set<String> keys = infos.keySet();
        for (String key : keys) {
            LogFactory.get().info(key + ":\t\t" + infos.get(key));
        }
    }

    private static void handle200(Socket s, Response response) throws IOException {
        String contentType = response.getContentType();
        String headText = Constant.response_head_202;
        headText = StrUtil.format(headText, contentType);
        byte[] head = headText.getBytes();
        //根据 response 对象上的 contentType ,组成返回的头信息,并且转换成字节数组。
        byte[] body = response.getBody();
        //获取主题信息部分,即 html 对应的 字节数组。
        byte[] responseBytes = new byte[head.length + body.length];
        //拼接头信息和主题信息,成为一个响应字节数组。
        ArrayUtil.copy(head, 0, responseBytes, 0, head.length);
        ArrayUtil.copy(body, 0, responseBytes, head.length, body.length);

        OutputStream os = s.getOutputStream();
        os.write(responseBytes);
        //close 自动 flush
        s.close();
    }
}

3.4,Server

Server 就代表最外层的 Server 元素,即服务器本身。

创建Server类:添加service属性,然后就把 Bootstrap 的内容搬过来了~

package com.ysy.diytomcat.catalina;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.TimeInterval;
import com.ysy.diytomcat.util.*
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.log.LogFactory;
import cn.hutool.system.SystemUtil;
import com.ysy.diytomcat.http.*;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

public class Server {
    private Service service;
    public Server(){
        this.service = new Service(this);
    }

    public void start(){
        logJVM();
        init();
    }

    private void init() {
        try {
            int port = 18080;
            ServerSocket ss = new ServerSocket(port);

            while(true) {
                Socket s =  ss.accept();
                Runnable r = new Runnable(){
                    @Override
                    public void run() {
                        try {
                            Request request = new Request(s,service);
                            Response response = new Response();
                            String uri = request.getUri();
                            if(null==uri)
                                return;
                            System.out.println("uri:"+uri);

                            Context context = request.getContext();

                            if("/".equals(uri)){
                                String html = "Hello DIY Tomcat from how2j.cn";
                                response.getWriter().println(html);
                            }
                            else{
                                String fileName = StrUtil.removePrefix(uri, "/");
                                File file = FileUtil.file(context.getDocBase(),fileName);
                                if(file.exists()){
                                    String fileContent = FileUtil.readUtf8String(file);
                                    response.getWriter().println(fileContent);

                                    if(fileName.equals("timeConsume.html")){
                                        ThreadUtil.sleep(1000);
                                    }

                                }
                                else{
                                    response.getWriter().println("File Not Found");
                                }
                            }
                            handle200(s, response);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                };

                ThreadPoolUtil.run(r);
            }
        } catch (IOException e) {
            LogFactory.get().error(e);
            e.printStackTrace();
        }
    }

    private static void logJVM() {
        Map<String,String> infos = new LinkedHashMap<>();
        infos.put("Server version", "How2J DiyTomcat/1.0.1");
        infos.put("Server built", "2020-04-08 10:20:22");
        infos.put("Server number", "1.0.1");
        infos.put("OS Name\t", SystemUtil.get("os.name"));
        infos.put("OS Version", SystemUtil.get("os.version"));
        infos.put("Architecture", SystemUtil.get("os.arch"));
        infos.put("Java Home", SystemUtil.get("java.home"));
        infos.put("JVM Version", SystemUtil.get("java.runtime.version"));
        infos.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor"));

        Set<String> keys = infos.keySet();
        for (String key : keys) {
            LogFactory.get().info(key+":\t\t" + infos.get(key));
        }
    }

    private static void handle200(Socket s, Response response) throws IOException {
        String contentType = response.getContentType();
        String headText = Constant.response_head_202;
        headText = StrUtil.format(headText, contentType);
        byte[] head = headText.getBytes();

        byte[] body = response.getBody();

        byte[] responseBytes = new byte[head.length + body.length];
        ArrayUtil.copy(head, 0, responseBytes, 0, head.length);
        ArrayUtil.copy(body, 0, responseBytes, head.length, body.length);

        OutputStream os = s.getOutputStream();
        os.write(responseBytes);
        s.close();
    }
}

修改Service:提供 server 属性和构造方法支持。

package com.ysy.diytomcat.catalina;

import com.ysy.diytomcat.util.ServerXMLUtil;

public class Service {
    private String name;
    private Engine engine;
    private Server server;

    public Service() {
        this.server = server;
        this.name = ServerXMLUtil.getServiceName();
        this.engine = new Engine(this);
    }

    public Engine getEngine() {
        return engine;
    }

    public Server getServer() {
        return server;
    }
}

修改Bootstrap:现在 Bootstrap 就变成了一个壳子了,直接调用 Server 实例的 start 方法。

package com.ysy.diytomcat;

import com.ysy.diytomcat.catalina.*;

public class Bootstrap {
    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }
}

4,其他页面

4.1,404页面

当访问的文件不存在的时候,目前仅仅是返回字符串 File Not Found, 并且返回代码还是 200的,这样不符合 http 协议的规范,应当返回 404, 所以我们接下来就要进行改造, 使得它呈现 tomcat 的 404 这种页面风格。

Constant新增

public static final String response_head_404 = "HTTP/1.1 404 Not Found\r\n Content-Type: text/html\r\n\r\n";
public static final String textFormat_404 =
            "<html><head><title>DIY Tomcat/1.0.1 - Error report</title><style>" +
                    "<!--H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} " +
                    "H2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} " +
                    "H3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} " +
                    "BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} " +
                    "B {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} " +
                    "P {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;}" +
                    "A {color : black;}A.name {color : black;}HR {color : #525D76;}--></style> " +
                    "</head><body><h1>HTTP Status 404 - {}</h1>" +
                    "<HR size='1' noshade='noshade'><p><b>type</b> Status report</p><p><b>message</b> <u>{}</u></p><p><b>description</b> " +
                    "<u>The requested resource is not available.</u></p><HR size='1' noshade='noshade'><h3>DiyTocmat 1.0.1</h3>" +
                    "</body></html>";

修改Server

package com.ysy.diytomcat.catalina;

import com.ysy.diytomcat.util.*;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.log.LogFactory;
import cn.hutool.system.SystemUtil;
import com.ysy.diytomcat.http.*;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

public class Server {
    private Service service;

    public Server() {
        this.service = new Service(this);
    }

    public void start() {
        logJVM();
        init();
    }

    private void init() {
        try {
            int port = 18080;
            ServerSocket ss = new ServerSocket(port);

            while (true) {
                Socket s = ss.accept();
                Runnable r = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Request request = new Request(s, service);
                            Response response = new Response();
                            String uri = request.getUri();
                            if (null == uri)
                                return;
                            System.out.println("uri:" + uri);

                            Context context = request.getContext();

                            if ("/".equals(uri)) {
                                String html = "Hello DIY Tomcat";
                                response.getWriter().println(html);
                            } else {
                                String fileName = StrUtil.removePrefix(uri, "/");
                                File file = FileUtil.file(context.getDocBase(), fileName);
                                if (file.exists()) {
                                    String fileContent = FileUtil.readUtf8String(file);
                                    response.getWriter().println(fileContent);

                                    if (fileName.equals("timeConsume.html")) {
                                        ThreadUtil.sleep(1000);
                                    }

                                } else {
                                    handle404(s, uri);
                                    return;
                                }
                            }
                            handle200(s, response);
                        } catch (IOException e) {
                            e.printStackTrace();
                        } finally {
                            try {
                                if (!s.isClosed())
                                    s.close();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                };

                ThreadPoolUtil.run(r);
            }
        } catch (IOException e) {
            LogFactory.get().error(e);
            e.printStackTrace();
        }
    }

    private static void logJVM() {
        Map<String, String> infos = new LinkedHashMap<>();
        infos.put("Server version", "How2J DiyTomcat/1.0.1");
        infos.put("Server built", "2020-04-08 10:20:22");
        infos.put("Server number", "1.0.1");
        infos.put("OS Name\t", SystemUtil.get("os.name"));
        infos.put("OS Version", SystemUtil.get("os.version"));
        infos.put("Architecture", SystemUtil.get("os.arch"));
        infos.put("Java Home", SystemUtil.get("java.home"));
        infos.put("JVM Version", SystemUtil.get("java.runtime.version"));
        infos.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor"));

        Set<String> keys = infos.keySet();
        for (String key : keys) {
            LogFactory.get().info(key + ":\t\t" + infos.get(key));
        }
    }

    protected void handle404(Socket s, String uri) throws IOException {
        OutputStream os = s.getOutputStream();
        String responseText = StrUtil.format(Constant.textFormat_404, uri, uri);
        responseText = Constant.response_head_404 + responseText;
        byte[] responseByte = responseText.getBytes("utf-8");
        os.write(responseByte);
    }

    private static void handle200(Socket s, Response response) throws IOException {
        String contentType = response.getContentType();
        String headText = Constant.response_head_202;
        headText = StrUtil.format(headText, contentType);
        byte[] head = headText.getBytes();

        byte[] body = response.getBody();

        byte[] responseBytes = new byte[head.length + body.length];
        ArrayUtil.copy(head, 0, responseBytes, 0, head.length);
        ArrayUtil.copy(body, 0, responseBytes, head.length, body.length);

        OutputStream os = s.getOutputStream();
        os.write(responseBytes);
        s.close();
    }
}

4.2,500页面

Constant:

public static final String response_head_500 = "HTTP/1.1 500 Internal Server Error\r\n Content-Type: text/html\r\n\r\n";
public static final String textFormat_500 = "<html><head><title>DIY Tomcat/1.0.1 - Error report</title><style>"
            + "<!--H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} "
            + "H2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} "
            + "H3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} "
            + "BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} "
            + "B {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} "
            + "P {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;}"
            + "A {color : black;}A.name {color : black;}HR {color : #525D76;}--></style> "
            + "</head><body><h1>HTTP Status 500 - An exception occurred processing {}</h1>"
            + "<HR size='1' noshade='noshade'><p><b>type</b> Exception report</p><p><b>message</b> <u>An exception occurred processing {}</u></p><p><b>description</b> "
            + "<u>The server encountered an internal error that prevented it from fulfilling this request.</u></p>"
            + "<p>Stacktrace:</p>" + "<pre>{}</pre>" + "<HR size='1' noshade='noshade'><h3>DiyTocmat 1.0.1</h3>"
            + "</body></html>";

Server

package com.ysy.diytomcat.catalina;

import com.ysy.diytomcat.util.*;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.log.LogFactory;
import cn.hutool.system.SystemUtil;
import com.ysy.diytomcat.http.*;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

public class Server {
    private Service service;

    public Server() {
        this.service = new Service(this);
    }

    public void start() {
        logJVM();
        init();
    }

    private void init() {
        try {
            int port = 18080;
            ServerSocket ss = new ServerSocket(port);

            while (true) {
                Socket s = ss.accept();
                Runnable r = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Request request = new Request(s, service);
                            Response response = new Response();
                            String uri = request.getUri();
                            if (null == uri)
                                return;
                            System.out.println("uri:" + uri);

                            Context context = request.getContext();
                            if ("/500.html".equals(uri)) {
                                throw new Exception("this is a deliberately created exception");
                            }
                            if ("/".equals(uri)) {
                                String html = "Hello DIY Tomcat";
                                response.getWriter().println(html);
                            } else {
                                String fileName = StrUtil.removePrefix(uri, "/");
                                File file = FileUtil.file(context.getDocBase(), fileName);
                                if (file.exists()) {
                                    String fileContent = FileUtil.readUtf8String(file);
                                    response.getWriter().println(fileContent);

                                    if (fileName.equals("timeConsume.html")) {
                                        ThreadUtil.sleep(1000);
                                    }

                                } else {
                                    handle404(s, uri);
                                    return;
                                }
                            }
                            handle200(s, response);
                        } catch (Exception e) {
                            LogFactory.get().error(e);
                            handle500(s, e);
                        } finally {
                            try {
                                if (!s.isClosed())
                                    s.close();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                };

                ThreadPoolUtil.run(r);
            }
        } catch (IOException e) {
            LogFactory.get().error(e);
            e.printStackTrace();
        }
    }

    private static void logJVM() {
        Map<String, String> infos = new LinkedHashMap<>();
        infos.put("Server version", "How2J DiyTomcat/1.0.1");
        infos.put("Server built", "2020-04-08 10:20:22");
        infos.put("Server number", "1.0.1");
        infos.put("OS Name\t", SystemUtil.get("os.name"));
        infos.put("OS Version", SystemUtil.get("os.version"));
        infos.put("Architecture", SystemUtil.get("os.arch"));
        infos.put("Java Home", SystemUtil.get("java.home"));
        infos.put("JVM Version", SystemUtil.get("java.runtime.version"));
        infos.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor"));

        Set<String> keys = infos.keySet();
        for (String key : keys) {
            LogFactory.get().info(key + ":\t\t" + infos.get(key));
        }
    }

    protected void handle500(Socket s, Exception e) {
        try {
            OutputStream os = s.getOutputStream();
            StackTraceElement stes[] = e.getStackTrace();
            StringBuffer sb = new StringBuffer();
            sb.append(e.toString());
            sb.append("\r\n");
            for (StackTraceElement ste : stes) {
                sb.append("\t");
                sb.append(ste.toString());
                sb.append("\r\n");
            }

            String msg = e.getMessage();

            if (null != msg && msg.length() > 20)
                msg = msg.substring(0, 19);

            String text = StrUtil.format(Constant.textFormat_500, msg, e.toString(), sb.toString());
            text = Constant.response_head_500 + text;
            byte[] responseBytes = text.getBytes("utf-8");
            os.write(responseBytes);
        } catch (IOException e1) {
            e1.printStackTrace();
        }
    }

    protected void handle404(Socket s, String uri) throws IOException {
        OutputStream os = s.getOutputStream();
        String responseText = StrUtil.format(Constant.textFormat_404, uri, uri);
        responseText = Constant.response_head_404 + responseText;
        byte[] responseByte = responseText.getBytes("utf-8");
        os.write(responseByte);
    }

    private static void handle200(Socket s, Response response) throws IOException {
        String contentType = response.getContentType();
        String headText = Constant.response_head_202;
        headText = StrUtil.format(headText, contentType);
        byte[] head = headText.getBytes();

        byte[] body = response.getBody();

        byte[] responseBytes = new byte[head.length + body.length];
        ArrayUtil.copy(head, 0, responseBytes, 0, head.length);
        ArrayUtil.copy(body, 0, responseBytes, head.length, body.length);

        OutputStream os = s.getOutputStream();
        os.write(responseBytes);
        s.close();
    }
}

4.3,欢迎文件

所谓的欢迎文件就是访问某个 context 的时候,如果没有指明文件,那么就默认访问 index.html 或者 index.jsp 这种功能。

Constant:增加 webXmlFile 文件,指向 conf/web.xml 这里。

public static final File webXmlFile = new File(confFolder, "web.xml");

conf/web.xml:在 conf 目录下新建 web.xml 文件,里面保存如下信息。这个 web.xml 是在 tomcat/conf/web.xml 这个位置的, 和大家开发 j2ee 的时候 webContent/WEB-INF/web.xml 那个 web.xml 文件是不同的文件。tomcat 关于文件的默认配置其实都是放在这里的。

<?xml version="1.0" encoding="UTF-8"?>
<web-app>
    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
        <welcome-file>index.htm</welcome-file>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>

WebXMLUtil:准备个工具类,用来获取 某个 Context 下的欢迎文件名称。根据 Context的 docBase 去匹配 web.xml 中的3个文件,找到哪个,就是哪个,如果都没有找到,默认就返回 index.html 文件。

package com.ysy.diytomcat.util;

import java.io.File;
import static com.ysy.diytomcat.util.Constant.webXmlFile;
import cn.hutool.core.io.FileUtil;
import com.ysy.diytomcat.catalina.Context;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

public class WebXMLUtil {
    public static String getWelcomeFile(Context context) {
        String xml = FileUtil.readUtf8String(webXmlFile);
        Document d = Jsoup.parse(xml);
        Elements es = d.select("welcome-file");
        for (Element e : es) {
            String welcomeFileName = e.text();
            File f = new File(context.getDocBase(), welcomeFileName);
            if (f.exists())
                return f.getName();
        }
        return "index.html";
    }
}

Request:以前访问 /a/index 的时候会匹配 /a 这个context,但是访问呢 /a 的时候,匹配的是 / 这个 context,因为现在有了 欢迎文件了,所以 /a 也要匹配 context了,所以Request有几个地方要做一下调整。

  • parseContext 里,先通过 uri 进行匹配,这样 /a 就可以匹配到 context了。
private void parseContext() {
    Engine engine = service.getEngine();
    context = engine.getDefaultHost().getContext(uri);
    if (null != context)
        return;
    String path = StrUtil.subBetween(uri, "/", "/");
    if (null == path)
        path = "/";
    else
        path = "/" + path;

    context = service.getEngine().getDefaultHost().getContext(path);
    if (null == context)
        context = service.getEngine().getDefaultHost().getContext("/");
}
  • 在构造方法里,比如 访问的地址是 /a, 那么 uri就变成 ""了,所以考虑这种情况, 让 uri 等于 "/"。
public Request(Socket socket, Service service) throws IOException {
    this.socket = socket;
    this.service = service;
    parseHttpRequest();
    if (StrUtil.isEmpty(requestString) || requestString.equals(""))
        return;
    parseUri();
    //在构造方法中调用 parseContext(), 倘若当前 Context 的路径不是 "/", 那么要对 uri进行修正,比如 uri 是 /a/index.html, 获取出来的 Context路径不是 "/”, 那么要修正 uri 为 /index.html。
    parseContext();
    if (!"/".equals(context.getPath())) {
        uri = StrUtil.removePrefix(uri, context.getPath());
        if (StrUtil.isEmpty(uri))
            uri = "/";
    }
}

Server:当 uri 等于 "/" 的时候,uri 就修改成欢迎文件,后面就当作普通文件来处理了。

package com.ysy.diytomcat.catalina;

import com.ysy.diytomcat.util.*;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.log.LogFactory;
import cn.hutool.system.SystemUtil;
import com.ysy.diytomcat.http.*;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

public class Server {
    private Service service;

    public Server() {
        this.service = new Service(this);
    }

    public void start() {
        logJVM();
        init();
    }

    private void init() {
        try {
            int port = 18080;
            ServerSocket ss = new ServerSocket(port);

            while (true) {
                Socket s = ss.accept();
                Runnable r = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Request request = new Request(s, service);
                            Response response = new Response();
                            String uri = request.getUri();
                            if (null == uri)
                                return;
                            System.out.println("uri:" + uri);

                            Context context = request.getContext();
                            if ("/500.html".equals(uri)) {
                                throw new Exception("this is a deliberately created exception");
                            }
                            if ("/".equals(uri))
                                uri = WebXMLUtil.getWelcomeFile(request.getContext());
                            String fileName = StrUtil.removePrefix(uri, "/");
                            File file = FileUtil.file(context.getDocBase(), fileName);
                            if (file.exists()) {
                                String fileContent = FileUtil.readUtf8String(file);
                                response.getWriter().println(fileContent);

                                if (fileName.equals("timeConsume.html")) {
                                    ThreadUtil.sleep(1000);
                                }

                            } else {
                                handle404(s, uri);
                                return;
                            }
                            handle200(s, response);
                        } catch (Exception e) {
                            LogFactory.get().error(e);
                            handle500(s, e);
                        } finally {
                            try {
                                if (!s.isClosed())
                                    s.close();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                };

                ThreadPoolUtil.run(r);
            }
        } catch (IOException e) {
            LogFactory.get().error(e);
            e.printStackTrace();
        }
    }

    private static void logJVM() {
        Map<String, String> infos = new LinkedHashMap<>();
        infos.put("Server version", "How2J DiyTomcat/1.0.1");
        infos.put("Server built", "2020-04-08 10:20:22");
        infos.put("Server number", "1.0.1");
        infos.put("OS Name\t", SystemUtil.get("os.name"));
        infos.put("OS Version", SystemUtil.get("os.version"));
        infos.put("Architecture", SystemUtil.get("os.arch"));
        infos.put("Java Home", SystemUtil.get("java.home"));
        infos.put("JVM Version", SystemUtil.get("java.runtime.version"));
        infos.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor"));

        Set<String> keys = infos.keySet();
        for (String key : keys) {
            LogFactory.get().info(key + ":\t\t" + infos.get(key));
        }
    }

    protected void handle500(Socket s, Exception e) {
        try {
            OutputStream os = s.getOutputStream();
            StackTraceElement stes[] = e.getStackTrace();
            StringBuffer sb = new StringBuffer();
            sb.append(e.toString());
            sb.append("\r\n");
            for (StackTraceElement ste : stes) {
                sb.append("\t");
                sb.append(ste.toString());
                sb.append("\r\n");
            }

            String msg = e.getMessage();

            if (null != msg && msg.length() > 20)
                msg = msg.substring(0, 19);

            String text = StrUtil.format(Constant.textFormat_500, msg, e.toString(), sb.toString());
            text = Constant.response_head_500 + text;
            byte[] responseBytes = text.getBytes("utf-8");
            os.write(responseBytes);
        } catch (IOException e1) {
            e1.printStackTrace();
        }
    }

    protected void handle404(Socket s, String uri) throws IOException {
        OutputStream os = s.getOutputStream();
        String responseText = StrUtil.format(Constant.textFormat_404, uri, uri);
        responseText = Constant.response_head_404 + responseText;
        byte[] responseByte = responseText.getBytes("utf-8");
        os.write(responseByte);
    }

    private static void handle200(Socket s, Response response) throws IOException {
        String contentType = response.getContentType();
        String headText = Constant.response_head_202;
        headText = StrUtil.format(headText, contentType);
        byte[] head = headText.getBytes();

        byte[] body = response.getBody();

        byte[] responseBytes = new byte[head.length + body.length];
        ArrayUtil.copy(head, 0, responseBytes, 0, head.length);
        ArrayUtil.copy(body, 0, responseBytes, head.length, body.length);

        OutputStream os = s.getOutputStream();
        os.write(responseBytes);
        s.close();
    }
}

以前访问 "/" 的时候,是直接在内存里写字符串,现在在 ROOT 下新建一个 index.html ,内容是一样的。

4.4,mime-type

文件有各种格式,比如 png, jpg, txt, html , exe 等等,但是浏览器却不能理解这些后缀名,它能理解的是 mime-type,比如 png 对应的 mime-type 是 image/png。当它拿到的响应告诉它 mime-type 是 image/png 的时候,它就会按照对应的格式去理解和解析这个图片。

目前我们默认返回的是 text/html 就是告诉浏览器把拿到的数据要当作 html 来解析,但是我们还会访问其他格式的文件,比如 .txt, .png, .pdf,就不能都统一返回成 text/html了。
所以我们要把不同后缀名的文件,翻译成对应的 mime-type 然后在 http 响应中,用 Content-type 这个头信息告诉浏览器,这样浏览器就可以更好地进行解析工作了。

web.xml:tomcat 把 mime-type 相关的信息都存放在这个 web.xml里。

<?xml version="1.0" encoding="UTF-8"?>
<web-app>
    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
        <welcome-file>index.htm</welcome-file>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
    <mime-mapping>
        <extension>pdf</extension>
        <mime-type>application/pdf</mime-type>
    </mime-mapping>
</web-app>

WebXMLUtil

package com.ysy.diytomcat.util;

import java.io.File;
import java.util.*;
import static com.ysy.diytomcat.util.Constant.webXmlFile;
import cn.hutool.core.io.FileUtil;
import com.ysy.diytomcat.catalina.Context;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

public class WebXMLUtil {
    private static Map<String, String> mimeTypeMapping = new HashMap<>();

    public static synchronized String getMimeType(String extName) {
        if (mimeTypeMapping.isEmpty())
            initMimeType();

        String mimeType = mimeTypeMapping.get(extName);
        if (null == mimeType)
            return "text/html";

        return mimeType;

    }

    private static void initMimeType() {
        String xml = FileUtil.readUtf8String(webXmlFile);
        Document d = Jsoup.parse(xml);

        Elements es = d.select("mime-mapping");
        for (Element e : es) {
            String extName = e.select("extension").first().text();
            String mimeType = e.select("mime-type").first().text();
            mimeTypeMapping.put(extName, mimeType);
        }

    }

    public static String getWelcomeFile(Context context) {
        String xml = FileUtil.readUtf8String(webXmlFile);
        Document d = Jsoup.parse(xml);
        Elements es = d.select("welcome-file");
        for (Element e : es) {
            String welcomeFileName = e.text();
            File f = new File(context.getDocBase(), welcomeFileName);
            if (f.exists())
                return f.getName();
        }
        return "index.html";
    }
}

Response:给contentType增加一个 setter。

public void setContentType(String contentType) {
    this.contentType = contentType;
}

Server:文件存在的时候,获取mimeType,并且设置在 response 的 contentType 属性上。

String extName = FileUtil.extName(file);
String mimeType = WebXMLUtil.getMimeType(extName);
response.setContentType(mimeType);
String fileContent = FileUtil.readUtf8String(file);
response.getWriter().println(fileContent);

a.txt:在ROOT下准备个 a.txt。

4.5,二进制文件

前我们能处理的文件都是文本文件,如 html 和 txt. 但是业务上总是需要处理二进制文件的,比如图片和 pdf这样的。

Response:为 Response 准备一个 body[] 来存放二进制文件,提供setter,修改 getter, 当body 不为空的时候,直接返回 body。

private byte[] body;
public void setBody(byte[] body) {
    this.body = body;
}
 public byte[] getBody() throws UnsupportedEncodingException {
    if (null == body) {
        String content = stringWriter.toString();
        body = content.getBytes("utf-8");
    }
    return body;
}

Server:以前处理文本文件的方式,会先读取成字符串,然后交给 response 。现在改成直接读取成二进制文件,交给 response 的 body。

package com.ysy.diytomcat.catalina;

import com.ysy.diytomcat.util.*;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.log.LogFactory;
import cn.hutool.system.SystemUtil;
import com.ysy.diytomcat.http.*;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

public class Server {
    private Service service;

    public Server() {
        this.service = new Service(this);
    }

    public void start() {
        logJVM();
        init();
    }

    private void init() {
        try {
            int port = 18080;
            ServerSocket ss = new ServerSocket(port);

            while (true) {
                Socket s = ss.accept();
                Runnable r = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Request request = new Request(s, service);
                            Response response = new Response();
                            String uri = request.getUri();
                            if (null == uri)
                                return;
                            System.out.println("uri:" + uri);

                            Context context = request.getContext();
                            if ("/500.html".equals(uri)) {
                                throw new Exception("this is a deliberately created exception");
                            }
                            if ("/".equals(uri))
                                uri = WebXMLUtil.getWelcomeFile(request.getContext());
                            String fileName = StrUtil.removePrefix(uri, "/");
                            File file = FileUtil.file(context.getDocBase(), fileName);
                            if (file.exists()) {
                                String extName = FileUtil.extName(file);
                                String mimeType = WebXMLUtil.getMimeType(extName);
                                response.setContentType(mimeType);
//                                String fileContent = FileUtil.readUtf8String(file);
//                                response.getWriter().println(fileContent);
                                byte body[] = FileUtil.readBytes(file);
                                response.setBody(body);
                                if (fileName.equals("timeConsume.html")) {
                                    ThreadUtil.sleep(1000);
                                }

                            } else {
                                handle404(s, uri);
                                return;
                            }
                            handle200(s, response);
                        } catch (Exception e) {
                            LogFactory.get().error(e);
                            handle500(s, e);
                        } finally {
                            try {
                                if (!s.isClosed())
                                    s.close();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                };

                ThreadPoolUtil.run(r);
            }
        } catch (IOException e) {
            LogFactory.get().error(e);
            e.printStackTrace();
        }
    }

    private static void logJVM() {
        Map<String, String> infos = new LinkedHashMap<>();
        infos.put("Server version", "How2J DiyTomcat/1.0.1");
        infos.put("Server built", "2020-04-08 10:20:22");
        infos.put("Server number", "1.0.1");
        infos.put("OS Name\t", SystemUtil.get("os.name"));
        infos.put("OS Version", SystemUtil.get("os.version"));
        infos.put("Architecture", SystemUtil.get("os.arch"));
        infos.put("Java Home", SystemUtil.get("java.home"));
        infos.put("JVM Version", SystemUtil.get("java.runtime.version"));
        infos.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor"));

        Set<String> keys = infos.keySet();
        for (String key : keys) {
            LogFactory.get().info(key + ":\t\t" + infos.get(key));
        }
    }

    protected void handle500(Socket s, Exception e) {
        try {
            OutputStream os = s.getOutputStream();
            StackTraceElement stes[] = e.getStackTrace();
            StringBuffer sb = new StringBuffer();
            sb.append(e.toString());
            sb.append("\r\n");
            for (StackTraceElement ste : stes) {
                sb.append("\t");
                sb.append(ste.toString());
                sb.append("\r\n");
            }

            String msg = e.getMessage();

            if (null != msg && msg.length() > 20)
                msg = msg.substring(0, 19);

            String text = StrUtil.format(Constant.textFormat_500, msg, e.toString(), sb.toString());
            text = Constant.response_head_500 + text;
            byte[] responseBytes = text.getBytes("utf-8");
            os.write(responseBytes);
        } catch (IOException e1) {
            e1.printStackTrace();
        }
    }

    protected void handle404(Socket s, String uri) throws IOException {
        OutputStream os = s.getOutputStream();
        String responseText = StrUtil.format(Constant.textFormat_404, uri, uri);
        responseText = Constant.response_head_404 + responseText;
        byte[] responseByte = responseText.getBytes("utf-8");
        os.write(responseByte);
    }

    private static void handle200(Socket s, Response response) throws IOException {
        String contentType = response.getContentType();
        String headText = Constant.response_head_202;
        headText = StrUtil.format(headText, contentType);
        byte[] head = headText.getBytes();

        byte[] body = response.getBody();

        byte[] responseBytes = new byte[head.length + body.length];
        ArrayUtil.copy(head, 0, responseBytes, 0, head.length);
        ArrayUtil.copy(body, 0, responseBytes, head.length, body.length);

        OutputStream os = s.getOutputStream();
        os.write(responseBytes);
        s.close();
    }
}

MiniBrowser:改造一下 MiniBrowser,给 readBytes 方法增加一个 fully 参数,表示是否完全读取。当 fully 等于 true的时候,即便读取到的数据没有buffer_size 那么长,也会继续读取。为什么要这么改动呢? 主要是为了测试 etf.pdf 这个文件, 这个文件比较大 (其实也没多大), 那么在传输过程中,可能就不会一次传输 1024个字节,有时候会小于这个字节数,如果读取到的数据小于这个字节就结束的话,那么读取到的文件就是不完整的。

public static byte[] readBytes(InputStream is, boolean fully) throws IOException {
    int buffer_size = 1024;
    byte buffer[] = new byte[buffer_size];
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    while (true) {
        int length = is.read(buffer);
        if (-1 == length)
            break;
        baos.write(buffer, 0, length);
        if (!fully && length != buffer_size)
            break;
    }
    byte[] result = baos.toByteArray();
    return result;
}

Request:修改 Request 的 parseHttpRequest 方法。

byte[] bytes = MiniBrowser.readBytes(is,false);

这个要带个 false 参数,表示还像原来那样,如果读取到的数据不够 bufferSize ,那么就不继续读取了。为什么这里不能用过 true 呢? 因为浏览器默认使用长连接,发出的连接不会主动关闭,那么 Request 读取数据的时候 就会卡在那里了~

Logo

开源、云原生的融合云平台

更多推荐