使用 NIO 模式简单模拟一个 Tomcat 运行原理

问题:一直对 Tomcat 的运行原理充满着好奇,今天终于自己通过 NIO 模拟了一个简约版 Tomcat ,直接看下面代码:

package main.tomcat;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
 * 定义同一规范的接口用来处理客户端请求
 */
interface Servlet {
    void service(HttpServletRequest request, HttpServletResponse response) throws Exception;
}

/**
 * 处理客户单请求登录的 Servlet
 * request 接受客户端发送的请求参数
 * response 把处理结果发送给客户端
 */
class LoginServlet implements Servlet {

    @Override
    public void service(HttpServletRequest request, HttpServletResponse response) throws Exception {
        String userName = request.getParameterValue("userName");
        String password = request.getParameterValue("password");
        String uri = request.getParameterValue("uri");

        String allParameterValue = request.getAllParameterValue();

        System.out.println(LocalDateTime.now() + " 【" + Thread.currentThread().getName() + "】线程===>正在处理客户端请求,需耗时3s...");
        System.out.println("开始倒计时:");
        TimeUnit.SECONDS.sleep(1);
        System.out.println("3");
        TimeUnit.SECONDS.sleep(1);
        System.out.println("2");
        TimeUnit.SECONDS.sleep(1);
        System.out.println("1");
        TimeUnit.SECONDS.sleep(1);

        SocketChannel printWriter = response.getPrintWriter();
        StringBuffer result = ResponsePage.build(request.getParameterValue("uri"));
        result.append("<h1><font color='green'>接受到你的请求参数是:" + allParameterValue + "</font></h1>");
        // 处理完成,开始会写给客户端数据
        System.out.println(LocalDateTime.now() + " 【" + Thread.currentThread().getName() + "】线程===>处理成功");
        printWriter.write(ByteBuffer.wrap(result.toString().getBytes(StandardCharsets.UTF_8)));
        System.out.println("\n\n\n\n");
    }
}

/**
 * 定义一些返回结果页面,用于展示、方便浏览器查看
 */
class ResponsePage {

    public static StringBuffer build(String pageIndex) {
        StringBuffer buffer = new StringBuffer();
        buffer.append("HTTP/1.1 200 ok\n");
        buffer.append("Content-Type: text/html; Charset=utf-8\n\n");
        try {
            FileInputStream fileInputStream = null;
            if (pageIndex.contains("login")) {
                fileInputStream = new FileInputStream("/Users/gongweiming/IdeaProjects/NettyProDemo/src/main/tomcat/login.html");
            } else {
                fileInputStream = new FileInputStream("/Users/gongweiming/IdeaProjects/NettyProDemo/src/main/tomcat/404.html");
            }

            FileChannel channel = fileInputStream.getChannel();
            ByteBuffer byteBuf = ByteBuffer.allocate(1024);
            int len = 0;
            while ((len = channel.read(byteBuf)) != -1) {

            }
            String content = new String(byteBuf.array(), 0, 1024 - byteBuf.remaining());
            // System.out.println(">>>>>>>>>"+content);
            buffer.append(content);

           /* BufferedReader reader = new BufferedReader(new InputStreamReader(fileInputStream));
            String line = "";

            while ((line = reader.readLine()) != null) {
                buffer.append(line);
            }*/
        } catch (Exception e) {
            e.printStackTrace();
        }
        return buffer;
    }
}

/**
 * =============================启动 Tomcat 服务器=============================
 * 1、首先加载并解析 web.xml 内容,每一个客户端访问都会有一个对应的 Servlet 进行相应处理
 * 2、真正开始处理客户端请求,也就是调用 Servlet 接口中定义的 service(request,response) 方法
 * 3、service(request,response) 可以从 request 中获取客户端发送的数据,可以使用 response 进行响应客户端请求
 */
public class TomcatBootStrap {
    /**
     * 用于存放从 web.xml 中解析到的内容,基本就是 </login,LoginServlet> 一个访问路径对应一个处理器 Servlet
     */
    private static final ConcurrentHashMap<String, Servlet> servletMap = new ConcurrentHashMap<>();

    /**
     * Tomcat 启动入口
     */
    public static void main(String[] args) throws Exception {
        /** 1、解析 web.xml 中的内容,将请求 uri 和 servlet 解析到一个 Map 中存放好 */
        initWebXML();

        /** 2、使用 NIO 中的 Channel 通道进行通信,调用对应的 Servlet 处理业务 */
        startTomcat(8080);
    }

    /**
     * 只是一个做了一个简单的解析,局限性严重,推荐使用 Dom4j 树相关工具类解析
     */
    private static void initWebXML() throws Exception {
        FileInputStream fileInputStream = new FileInputStream("/Users/gongweiming/IdeaProjects/NettyProDemo/src/main/tomcat/web.xml");
        BufferedReader reader = new BufferedReader(new InputStreamReader(fileInputStream));
        String line = "";

        String key = null;
        String value = null;
        while ((line = reader.readLine()) != null) {
            if (line.contains("pattern")) {
                String[] split = line.split("/");
                key = "/" + split[1].substring(0, split[1].length() - 1);
            }
            if (line.contains("class")) {
                value = line.split(">")[1].split("<")[0];
            }
        }
        if (Objects.nonNull(key) && Objects.nonNull(value)) {
            servletMap.put(key, new LoginServlet());
        }
    }

    /**
     * 启动 Tomcat 服务,这里使用的是 NIO 异步非阻塞模式,也可以使用 Selector、Netty(Reactor 模式)
     */
    private static void startTomcat(int port) throws Exception {
        // 开启服务通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        // 监听端口
        serverSocketChannel.bind(new InetSocketAddress(port));

        // 设置成异步非阻塞模式,如果没有事件发生就返回 null
        serverSocketChannel.configureBlocking(false);

        // 开启线程处理业务
        doExecute(serverSocketChannel);

        // 关闭资源
        // 如果将 serverSocketChannel.accept() 方法放到另一个线程中执行的话这里不要关闭,否则拿不到 SocketChannel,
        // 如果使用 Completable 接口调用 serverSocketChannel.accept() 方法,注意它使用的是 ForkJoinPool 是个守护线程
        // 所以启动主线程,如果使用这个 ForkJoinPool 框架开启新线程,可以在主线程上面阻塞住,不要关闭了,因为 ForkJoinPool 是守护线程
        serverSocketChannel.close();
    }


    private static final int BUFFER_SIZE = 1024;

    /**
     * accept() 方法是非阻塞的,没有监听到事件返回 null,注意此处会存在空转问题
     * 采用多线程处理客户端请求
     * <p>
     * 分成三部分处理:
     * 1、通过 Socket 获取到客户端发送的数据包,然后解析封装成 HttpServletRequest 对象
     * 2、根据 web.xml 中的配置,找到对应的处理器 handler 或者说是 Servlet
     * 3、开始真正处理业务,处理完之后通过 HttpServletResponse 进行响应客户端
     */
    private static void doExecute(ServerSocketChannel serverSocketChannel) throws Exception {
        System.out.println("Tomcat is started,port is 8080...");
        while (true) {
            // 如果有 accept 事件发生,就不会返回 null
            SocketChannel socketChannel = serverSocketChannel.accept();
            if (socketChannel != null) {
                CompletableFuture.runAsync(() -> {
                    try {
                        HttpServletResponse response = new HttpServletResponse();
                        response.setPrintWriter(socketChannel);
                        /** 1、读取数据,解析客户端发送过来的请求 */
                        HttpServletRequest request = requestResolver(socketChannel);
                        if (Objects.isNull(request)) {
                            return;
                        }

                        /** 2、根据 web.xml 中配置的路由映射中找到对应的处理 handler */
                        Servlet handler = dispatcherServlet(request);

                        if (Objects.isNull(handler)) {
                            // 跳转到 404 页面,终结此次请求
                            response.getPrintWriter().write(ByteBuffer.wrap(ResponsePage.build("404").toString().getBytes(StandardCharsets.UTF_8)));
                            socketChannel.close();
                            return;
                        }

                        /** 3、对应的 handler 开始处理客户端请求 */
                        handler.service(request, response);

                        response.getPrintWriter().close();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                });
            }
        }
    }

    /**
     * 将请求路径分发到对的 Servlet 处理器上
     */
    private static Servlet dispatcherServlet(HttpServletRequest request) throws InterruptedException {
        return servletMap.get(request.getParameterValue("uri"));
    }

    /**
     * 解析客户端发送的数据包,将其封装成 HttpServletRequest
     *
     * @param socketChannel
     * @return
     * @throws Exception
     */
    private static HttpServletRequest requestResolver(SocketChannel socketChannel) throws Exception {
        HttpServletRequest request = new HttpServletRequest();
        ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
        socketChannel.read(buffer);
        byte[] bytes = buffer.array();
        String content = new String(bytes, 0, BUFFER_SIZE - buffer.remaining());
        System.out.println(LocalDateTime.now() + "【" + Thread.currentThread().getName() + "】线程===>接收到客户端请求,解析后,数据如下所示:\n");
        if (content.contains("favicon")) {
            return null;
        }
        System.out.println("=============================================================================================================");
        System.out.println(content);
        System.out.println("=================================================++++++【开始处理客户单请求】++++================================");

        if (!content.contains("favicon")) {
            String firstLine = content.split("\n")[0];
            if (firstLine.contains("HTTP")) {
                request.setParameterValue("uri", "/" + (firstLine.contains("?") ? firstLine.split("/")[1].split("\\?")[0] : firstLine.split("/")[1].split(" ")[0]));
                ;

                if (firstLine.contains("?") && firstLine.contains("=")) {
                    String s1 = firstLine.split("\\?")[1].split(" ")[0];
                    String[] split = s1.split("&");
                    Arrays.stream(split).forEach(e -> {
                        String[] split1 = e.split("=");
                        request.setParameterValue(split1[0], split1[1]);
                    });
                }
            }
        }
        return request;
    }

}

/**
 * 特别注意 Response 类中封装了可以给客户端会写内容的对象 Socket/SocketChannel
 * 这里也可以封装 PrintWriter 类,怎么方便怎么来
 */
class HttpServletResponse {
    private SocketChannel printWriter;

    public static HttpServletResponse SUCCESS() {
        HttpServletResponse response = new HttpServletResponse();
        return response;
    }

    public SocketChannel getPrintWriter() {
        return printWriter;
    }

    public void setPrintWriter(SocketChannel printWriter) {
        this.printWriter = printWriter;
    }
}

/**
 * 定义用于封装客户端请求参数实体 HttpServletRequest
 */
class HttpServletRequest {

    private Map<String, String> parameterValue = new ConcurrentHashMap<>();

    public String getAllParameterValue() {
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("<br/>");
        parameterValue.forEach((k, v) -> {
            stringBuffer.append(k + "=" + v + "<br/>");
        });
        return stringBuffer.toString();
    }

    public String getParameterValue(String key) {
        return parameterValue.get(key);
    }

    public void setParameterValue(String key, String value) {
        parameterValue.put(key, value);
    }
}

然后开启多个浏览器窗口,地址栏输入

http://localhost:8080/login?userName=10&password=1
http://localhost:8080/login
http://localhost:8080/abcxxx?userName=10&password=1
http://localhost:8080/abcxxx

控制台输出日志,如下:

Tomcat is started,port is 8080...
2022-08-09T17:29:39.679ForkJoinPool.commonPool-worker-9】线程===>接收到客户端请求,解析后,数据如下所示:

=============================================================================================================
GET /login HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: ".Not/A)Brand";v="99", "Google Chrome";v="103", "Chromium";v="103"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: XXL_JOB_LOGIN_IDENTITY=7b226964223a312c22757365726e616d65223a2261646d696e222c2270617373776f7264223a226531306164633339343962613539616262653536653035376632306638383365222c22726f6c65223a312c227065726d697373696f6e223a6e756c6c7d; Idea-159af402=a56b3ddf-5c52-4ba7-a513-b6bc69bef0f1


=================================================++++++【开始处理客户单请求】++++================================
2022-08-09T17:29:39.681ForkJoinPool.commonPool-worker-9】线程===>正在处理客户端请求,需耗时3s...
开始倒计时:
3
2
1
2022-08-09T17:29:43.701ForkJoinPool.commonPool-worker-9】线程===>处理成功

页面展示,如下:

在这里插入图片描述

总结

Tomcat 服务就是一个 Server,具备两点核心功能: Start 启动服务器,Stop 停止服务器;底层通过监听 Socket、接受请求、处理请求、响应请求等主要功能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

魔道不误砍柴功

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值