我的架构梦:(十六)手写Tomcat服务器

经过前面两篇的分析,我们对Tomcat的系统架构与原理有了一定的认识与理解了,回顾请戳:

我的架构梦:(十四)Tomcat 系统架构与原理剖析
我的架构梦:(十五)Tomcat 服务器核心配置详解

一、需求分析

名称:tomcat-customize

tomcat-customize要做的事情:作为一个服务器软件提供服务的,也即我们可以通过浏览器客户端发送http请求, tomcat-customize可以接收到请求进行处理,处理之后的结果可以返回浏览器客户端。

1、提供服务,接收请求(Socket通信);
2、请求信息封装成Request对象(Response对象);
3、客户端请求资源,资源分为静态资源(html)和动态资源(Servlet);
4、资源返回给客户端浏览器。

我们递进式完成以上需求,提出V1.0、V2.0、V3.0、V4.0、V5.0版本的需求。

  • V1.0需求:浏览器请求http://localhost:8080,返回一个固定的字符串到⻚面Hello Tomcat Customize!
  • V2.0需求:封装RequestResponse对象,返回html静态资源文件;
  • V3.0需求:可以请求动态资源(Servlet);
  • V4.0需求:多线程改造(其他请求不受请求阻塞影响);
  • V5.0需求:线程池改造(高并发场景);

完成上述五个版本后,我们的代码如下:

二、代码详情

1、Bootstrap 启动类

/**
 * 自定义Tomcat的主类
 */
@Data
public class Bootstrap {

    /**
     * 定义socket监听的端口号
     */
    private int port = 8080;

    /**
     * 自定义Tomcat启动需要初始化展开的一些操作
     */
    public void start() throws Exception {

        // 加载解析相关的配置,web.xml
        loadServlet();

        // 定义一个线程池
        int corePoolSize = 10;
        int maximumPoolSize =50;
        long keepAliveTime = 100L;
        TimeUnit unit = TimeUnit.SECONDS;
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(50);
        ThreadFactory threadFactory = Executors.defaultThreadFactory();
        RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                unit,
                workQueue,
                threadFactory,
                handler
        );

        /**
         * 自定义Tomcat 1.0版本
         * 需求:浏览器请求http://localhost:8080,返回一个固定的字符串到页面"Hello Tomcat Customize!"
         */
        ServerSocket serverSocket = new ServerSocket(port);
        System.out.println("Tomcat Customize start on port: " + port);

        /*while (true) {
            Socket socket = serverSocket.accept();
            // 有了socket,接收到请求,获取输出流
            OutputStream outputStream = socket.getOutputStream();
            String data = "Hello Tomcat Customize!";
            String responseText = HttpProtocolUtil.getHttpHeader200(data.getBytes().length) + data;
            outputStream.write(responseText.getBytes());
            socket.close();
        }*/

        /**
         * 自定义Tomcat 2.0版本
         * 需求:封装Request和Response对象,返回html静态资源文件
         */
        /*while (true) {
            Socket socket = serverSocket.accept();
            InputStream inputStream = socket.getInputStream();

            // 封装Request对象和Response对象
            Request request = new Request(inputStream);
            Response response = new Response(socket.getOutputStream());

            response.outputHtml(request.getUrl());
            socket.close();
        }*/

        /**
         * 自定义Tomcat 3.0版本
         * 需求:可以请求动态资源(Servlet)
         */
        /*while (true) {
            Socket socket = serverSocket.accept();
            InputStream inputStream = socket.getInputStream();

            // 封装Request对象和Response对象
            Request request = new Request(inputStream);
            Response response = new Response(socket.getOutputStream());

            HttpServlet httpServlet = servletMap.get(request.getUrl());
            // 静态资源处理
            if (httpServlet == null) {
                response.outputHtml(request.getUrl());
            } else {
                // 动态资源servlet请求
                httpServlet.service(request, response);
            }

            socket.close();
        }*/

        /**
         * 自定义Tomcat 4.0版本
         * 需求:多线程改造(其他请求不受请求阻塞影响)
         */
        /*while (true) {
            Socket socket = serverSocket.accept();
            RequestProcessor requestProcessor = new RequestProcessor(socket, servletMap);
            requestProcessor.start();
        }*/

        /**
         * 自定义Tomcat 5.0版本
         * 需求:线程池改造
         */
        while (true) {
            Socket socket = serverSocket.accept();
            RequestProcessor requestProcessor = new RequestProcessor(socket, servletMap);
            threadPoolExecutor.execute(requestProcessor);
        }

    }

    private Map<String, HttpServlet> servletMap = new HashMap<>();

    /**
     * 加载解析web.xml,初始化Servlet
     */
    private void loadServlet() {
        InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("web.xml");
        SAXReader saxReader = new SAXReader();

        try {
            Document document = saxReader.read(resourceAsStream);
            Element rootElement = document.getRootElement();
            List<Element> selectNodes = rootElement.selectNodes("//servlet");
            for (int i = 0; i < selectNodes.size(); i++) {
                Element element = selectNodes.get(i);
                // <servlet-name>riemann</servlet-name>
                Element servletnameElement = (Element) element.selectSingleNode("servlet-name");
                String servletName = servletnameElement.getStringValue();

                // <servlet-class>com.riemann.server.servlet.RiemannServlet</servlet-class>
                Element servletclassElement = (Element) element.selectSingleNode("servlet-class");
                String servletClass = servletclassElement.getStringValue();

                // 根据servlet-name的值找到url-pattern
                Element servletMapping = (Element) rootElement.selectSingleNode("/web-app/servlet-mapping[servlet-name='" + servletName + "']");
                // /riemann
                String urlPattern = servletMapping.selectSingleNode("url-pattern").getStringValue();
                servletMap.put(urlPattern, (HttpServlet) Class.forName(servletClass).newInstance());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 自定义Tomcat的程序启动入口
     * @param args
     */
    public static void main(String[] args) {
        Bootstrap bootstrap = new Bootstrap();
        try {
            // 启动自定义Tomcat
            bootstrap.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

2、Http协议工具类

/**
 * http协议工具类,主要是提供响应头信息,这里我们只提供200和404的情况
 */
public class HttpProtocolUtil {

    /**
     * 为响应码200提供请求头信息
     * @param contentLength
     * @return
     */
    public static String getHttpHeader200(long contentLength) {
        return "HTTP/1.1 200 OK \n" +
               "Content-Type: text/html \n" +
               "Content-Length: " + contentLength + " \n" +
               "\r\n";
    }

    /**
     * 为响应码404提供请求头信息(此处也包含了数据内容)
     * @return
     */
    public static String getHttpHeader404() {
        String str404 = "<h1>404 not found</h1>";
        return "HTTP/1.1 404 NOT Found \n" +
                "Content-Type: text/html \n" +
                "Content-Length: " + str404.getBytes().length + " \n" +
                "\r\n" + str404;
    }

}

3、Request、 Response封装类

/**
 * 把请求信息封装为Request对象(根据InputSteam输入流封装)
 */
@Data
public class Request {

    private String method; // 请求方式,比如GET/POST

    private String url;  // 例如 /,/index.html

    private InputStream inputStream;  // 输入流,其他属性从输入流中解析出来

    /**
     * 构造器,输入流传入
     * @param inputStream
     */
    public Request(InputStream inputStream) throws IOException {
        this.inputStream = inputStream;

        // 从输入流中获取请求信息
        // 从输入流中获取请求信息
        int count = 0;
        while (count == 0) {
            count = inputStream.available();
        }

        byte[] bytes = new byte[count];
        inputStream.read(bytes);

        String inputStr = new String(bytes);
        // 获取第一行请求头信息
        String firstLine = inputStr.split("\\n")[0]; // GET / HTTP/1.1
        String[] strings = firstLine.split(" ");
        this.method = strings[0];
        this.url = strings[1];

        System.out.println("method: " + method);
        System.out.println("url: " + url);

    }
    
}
/**
 * 封装Response对象,需要依赖于OutputStream
 *
 * 该对象需要提供核心方法,输出html
 */
@Data
@AllArgsConstructor
public class Response {

    private OutputStream outputStream;

    /**
     * 使用输出流输出指定字符串
     * @param content
     * @throws IOException
     */
    public void output(String content) throws IOException {
        outputStream.write(content.getBytes());
    }

    /**
     * 输出html格式
     * @param path url 随后要根据url来获取到静态资源的绝对路径,进一步根据绝对路径读取该静态资源文件,最终通过输出流输出。
     *                 / -----> classes
     * @throws IOException
     */
    public void outputHtml(String path) throws IOException {
        // 获取静态资源文件的绝对路径
        String absoluteResourcePath = StaticResourceUtil.getAbsolutePath(path);

        // 输入静态资源文件
        File file = new File(absoluteResourcePath);
        if (file.exists() && file.isFile()) {
            // 读取静态资源文件,输出静态资源
            StaticResourceUtil.outputStaticResource(new FileInputStream(file), outputStream);
        } else {
            // 输出404
            output(HttpProtocolUtil.getHttpHeader404());
        }
    }

}

4、静态资源请求处理工具类

/**
 * 静态资源工具类
 */
public class StaticResourceUtil {

    /**
     * 获取静态资源文件的绝对路径
     * @param path
     * @return
     */
    public static String getAbsolutePath(String path) {
        String absolutePath = StaticResourceUtil.class.getResource("/").getPath();
        return absolutePath.replaceAll("\\\\", "/") + path;
    }

    /**
     * 读取静态资源文件输入流,通过输出流输出
     * @param inputStream
     * @param outputStream
     * @throws IOException
     */
    public static void outputStaticResource(InputStream inputStream, OutputStream outputStream) throws IOException {
        int count = 0;
        while (count == 0) {
            count = inputStream.available();
        }

        int resourceSize = count;
        // 输出http请求头,然后再输出具体内容
        outputStream.write(HttpProtocolUtil.getHttpHeader200(resourceSize).getBytes());

        // 读取内容输出
        long written = 0 ; // 已经读取的内容长度
        int byteSize = 1024; // 计划每次缓冲的长度
        byte[] bytes = new byte[byteSize];

        while (written < resourceSize) {
            if (written + byteSize > resourceSize) { // 说明剩余未读取大小不足一个1024长度,那就按真实长度处理
                byteSize = (int) (resourceSize - written); // 剩余的文件内容长度
                bytes = new byte[byteSize];
            }
            inputStream.read(bytes);
            outputStream.write(bytes);

            outputStream.flush();

            written += byteSize;
        }
    }

}

5、动态资源请求

5.1 Servlet接口定义

public interface Servlet {

    void init() throws Exception;

    void destory() throws Exception;

    void service(Request request, Response response) throws Exception;

}

5.2 HttpServlet抽象类定义

public abstract class HttpServlet implements Servlet {

    public abstract void doGet(Request request, Response response);

    public abstract void doPost(Request request, Response response);

    @Override
    public void service(Request request, Response response) throws Exception {
        if ("GET".equalsIgnoreCase(request.getMethod())) {
            doGet(request, response);
        } else {
            doPost(request, response);
        }
    }

}

5.3 业务类Servlet定义RiemannServlet

public class RiemannServlet extends HttpServlet {

    @Override
    public void doGet(Request request, Response response) {
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        String content = "<h1>RiemannServlet get</h1>";
        try {
            response.output((HttpProtocolUtil.getHttpHeader200(content.getBytes().length) + content));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void doPost(Request request, Response response) {
        String content = "<h1>RiemannServlet post</h1>";
        try {
            response.output((HttpProtocolUtil.getHttpHeader200(content.getBytes().length) + content));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void init() throws Exception {

    }

    @Override
    public void destory() throws Exception {

    }

}

6、多线程改造封装的RequestProcessor类

@Data
@AllArgsConstructor
public class RequestProcessor extends Thread {

    private Socket socket;

    private Map<String, HttpServlet> servletMap;

    @Override
    public void run() {
        try {
            InputStream inputStream = socket.getInputStream();

            // 封装Request对象和Response对象
            Request request = new Request(inputStream);
            Response response = new Response(socket.getOutputStream());

            HttpServlet httpServlet = servletMap.get(request.getUrl());
            // 静态资源处理
            if (httpServlet == null) {
                response.outputHtml(request.getUrl());
            } else {
                // 动态资源servlet请求
                httpServlet.service(request, response);
            }

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

}

三、测试结果

1、访问静态资源

在这里插入图片描述

2、访问动态资源

在这里插入图片描述
3、找不到资源错误页面访问

在这里插入图片描述

4、多线程访问,其中演示动态资源阻塞,不影响其他资源的访问。

在这里插入图片描述
动态资源由于给了休眠10秒,访问的时候请求被阻塞。

在这里插入图片描述

但是不影响同时访问其他资源,这里是静态资源。

在这里插入图片描述

四、代码仓库

https://github.com/riemannChow/perseverance/tree/master/handwriting-framework/tomcat/tomcat-customize

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章