java 手写一个简易的 tomcat 服务器

2023-11-10

最近想学一下 tomcat 到底是怎么运行的,于是手写一个简易的 tomcat 服务器,加深一下印象。


涉及到的知识:

  • 计算机网络
    • tcp/ip 协议 , 网络编程 ,socket
    • 浏览器请求,响应报文规范
  • java 反射,注解

实现的功能:

  • 可打包成独立应用
  • 能实现一个简单的接口
  • 能重定向

其实一个人是没那么多时间搞完tomcat那么多事情的,只能做些简单的实现,还是有很多漏洞bug的,所以仅供参考学习,切勿拿来使用。

Tomcat 运行大致流程:

  1. Socket连接
  2. 读取请求网络中的字节流
  3. 根据相应的协议(Http/AJP)解析字节流,生成统一的Tomcat Request对象
  4. 将Tomcat Reques传给容器
  5. 容器返回Tomcat Response对象
  6. 将Tomcat Response对象转换为字节流
  7. 将字节流返回给客户端

目录结构

在这里插入图片描述

  • entity : java实体类,一些pojo,和常量类
  • handler : 实现步骤 3 ,封装 requestresponse
  • request : request 对象,以及拓展类
  • response : response 对象, 拓展类,和一些常量类
  • server : 主要的代码
    • RequestRunnable.java : 负责处理浏览器的请求,然后通过请求路径,分发给指定的 servlet 处理,实现步骤 2,3,4
    • Server.java : 读取配置,开启 socket ,接收浏览器请求,并开启线程交给 RequestRunnable.java , 实现步骤 1,2
    • ThreadPool.java : 线程池工厂
  • service : 业务层 , 通过 RequestRunnable 分发,交给 指定 web 应用处理。
  • utils : 工具包
    • ServletClassLoader.java : 加载指定配置路径下的 class 文件,并通过反射加载 servlet 实现类到 classloder 中, 最后交给 RequestRunnable 处理。

成果预览

先看一下成果:
在这里插入图片描述
在这里插入图片描述
startup.bat

chcp 65001
java -jar  tomcat.jar  webAppsPath="D:/桌面/tomcat/webapps/net"   port=9999
pause  

在这里插入图片描述

我创建了一个新的 java 项目,并实现了一个 Servlet 。
在这里插入图片描述
MyServlet.java

package tomcat;

import cn.enncy.tomcat.request.HttpRequest;
import cn.enncy.tomcat.response.HttpResponse;
import cn.enncy.tomcat.service.HttpServlet;
import cn.enncy.tomcat.service.WebService;

import java.io.IOException;

@WebService(path = "/hello/tomcat")
public class MyServlet extends HttpServlet {
    @Override
    public void doGet(HttpRequest req, HttpResponse res) {
        try {
            res.write("hello tomcat");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void doPost(HttpRequest httpRequest, HttpResponse httpResponse) {
        doGet(httpRequest,httpResponse);
    }
}

最后运行 startup.bat
在这里插入图片描述

在这里插入图片描述

源码

1. 加载 Servlet

首先最重要的就是,当你打包成一个 jar 包之后,怎么样加载 指定文件下的项目,以及 class 文件。这里我们用到了 URLClassLoader ,通过反射加载

package cn.enncy.tomcat.utils;


import cn.enncy.tomcat.service.WebService;

import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashSet;
import java.util.Set;
import java.util.Stack;

public class ServletClassLoader {

    public static Class[] loadClasses(String rootClassPath) throws Exception {
        Set<Class<?>> classSet = new HashSet<>();
        // 设置class文件所在根路径
        File clazzPath = new File(rootClassPath);

        // 记录加载.class文件的数量
        int clazzCount = 0;

        if (clazzPath.exists() && clazzPath.isDirectory()) {
            // 获取路径长度
            int clazzPathLen = clazzPath.getAbsolutePath().length() + 1;

            Stack<File> stack = new Stack<>();
            stack.push(clazzPath);

            // 遍历类路径
            while (!stack.isEmpty()) {
                File path = stack.pop();
                File[] classFiles = path.listFiles(pathname -> {
                    //只加载class文件
                    return pathname.isDirectory() || pathname.getName().endsWith(".class");
                });
                if (classFiles == null) {
                    break;
                }
                for (File subFile : classFiles) {
                    if (subFile.isDirectory()) {
                        stack.push(subFile);
                    } else {
                        if (clazzCount++ == 0) {
                            Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
                            if (! method.isAccessible()) {
                                method.setAccessible(true);
                            }
                            // 设置类加载器
                            URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
                            // 将当前类路径加入到类加载器中
                            method.invoke(classLoader, clazzPath.toURI().toURL());
                        }
                        // 文件名称
                        String className = subFile.getAbsolutePath();
                        //去掉后缀名
                        className = className.substring(clazzPathLen, className.length() - 6);
                        //将/替换成. 得到全路径类名
                        className = className.replace(File.separatorChar, '.');
                        // 加载 Servlet 类
                        Class<?> clazz = Class.forName(className);
                        if(clazz.isAnnotationPresent(WebService.class)){
                            classSet.add(clazz);
                        }


                    }
                }
            }
        }
        return classSet.toArray(new Class[0]);
    }
}

2. 创建 socket 链接

package cn.enncy.tomcat.server;

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

public class Server {

    public static  void startup(int port,Class[] classes,Runnable callback) throws IOException {
        //创建线程池	最大线程1000个
        ThreadPool threadPool = new ThreadPool(1000,1000,1000);
        //开启服务器
        ServerSocket serverSocket = new ServerSocket(port);
        //回调
        callback.run();
        while(true){
            //获取请求
            Socket accept = serverSocket.accept();
            //创建新的任务  Runnable
            RequestRunnable requestTaskRunnable = new RequestRunnable(accept,classes);
            //开启新的线程去执行
            threadPool.execute(requestTaskRunnable);
        }
    }
}

创建线程池

使用线程池建立线程,去处理请求,每一个请求对应一个线程。切勿手动创建线程,防止内存溢出。

package cn.enncy.tomcat.server;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadPool extends  ThreadPoolExecutor{

    public ThreadPool(int corePoolSize, int maximumPoolSize,  long keepAliveTime) {
        super(  corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(1024),
                new WorkThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
    }

}
class WorkThreadFactory implements ThreadFactory {
    private final AtomicInteger atomicInteger = new AtomicInteger(0);
    @Override
    public Thread newThread(Runnable r) {
        int c = atomicInteger.incrementAndGet();
        //通过计数器,可以更好的管理线程
        return new Thread(r,"pool-1-thread-"+c);
    }
}

3. 处理请求

最核心的地方也就是在这里了,

  1. 根据相应的协议解析字节流,生成统一的Tomcat Request对象
  2. 将Tomcat Reques 传给 web应用
package cn.enncy.tomcat.server;


import cn.enncy.tomcat.handler.RequestHandler;
import cn.enncy.tomcat.handler.ResponseHandler;
import cn.enncy.tomcat.request.HttpRequest;
import cn.enncy.tomcat.response.HttpResponse;
import cn.enncy.tomcat.service.HttpServlet;
import cn.enncy.tomcat.service.WebService;

import java.lang.reflect.Method;
import java.net.Socket;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RequestRunnable implements  Runnable{

    private Socket socket;
    private final Class[] classes;

    public RequestRunnable(Socket socket,Class[] classes) {
        this.socket = socket;
        this.classes = classes;
    }

    @Override
    public void run() {
        try{
            //封装请求
            HttpRequest request = RequestHandler.createRequest (socket);
            assert request != null;
            System.out.println("[Tomcat-"+Thread.currentThread().getName()+"]  "+request.getMethod()+" "+request.getPath()+" "+request.getProtocol());
            //封装响应
            HttpResponse response =  ResponseHandler.createResponse(socket);
            //遍历资源类,反射获取 servlet
            for (Class clazz : classes) {
                WebService webService = (WebService) clazz.getAnnotation(WebService.class);
                String path = webService.path();
                //如果当前请求的文件路径和 servlet 的path路径匹配,则吧请求交给这个类处理
                Matcher matcher = Pattern.compile(path).matcher(request.getFilePath());
                if(matcher.find()){
                    //实例类,然后执行方法
                    Object object = clazz.getConstructor().newInstance();
                    HttpServlet httpServlet = (HttpServlet) object;
                    httpServlet.doGet(request, response);
                    break;
                }
            }

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

}


剩下的各种 requestresponse 的封装其实也就是跟 字符串浏览器报文规范,打交道,这里就不细说了。大概封装流程如下:


  1. 通过 socket 获取 InputStream ,你会读取到类似以下的大致内容
Host: localhost:9999
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: image
Referer: http://localhost:9999/hello/tomcat
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: Webstorm-59be97a3=8d34a989-fd21-411a-9c0f-29b1cefcc5b7; _ga=GA1.1.1609156561.1611760673; AMCV_71FF20B3534568190A490D45%40AdobeOrg=-1124106680%7CMCIDTS%7C18685%7CMCMID%7C23713747909870943654360722308276803315%7CMCAID%7CNONE%7CMCOPTOUT-1614324543s%7CNONE%7CvVersion%7C5.2.0

格式如下
https://imgconvert.csdnimg.cn/aHR0cDovL3MyLjUxY3RvLmNvbS93eWZzMDIvTTAyLzU5LzE2L3dLaW9MMVRITmZhaEdBa0RBQUZ1LS01OVMzTTE3My5qcGc?x-oss-process=image/format,png
2. 然后你自己再封装一个 response,通过 socketOutputStream - write 写出去给浏览器,类似如下内容

HTTP/1.1 200 OK
Server: Tomcat
Date: Tue, 4 May 2021 11:08:13 GMT
Content-Type: text/html; charset=utf-8

这里是你要传给浏览器的内容

格式如下
https://img-blog.csdn.net/20150126110634828?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvY2hlbmhhbnpodW4=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center


代码以及实例,我已经上传到 github ,源码大家自行查阅
https://github.com/enncy/tomcat

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

java 手写一个简易的 tomcat 服务器 的相关文章

  • 构造函数中的变量?

    我目前是 Java 编程的初学者 任务是 编码并测试 Hangman 游戏的一个版本 您的解决方案将涉及一个 Hangman 类 其构造函数选择一个单词 其猜测方法处理每个猜测的字符 不过 我有一个小问题 我的整个代码可以工作并编译 并且我
  • 编译错误:表达式非法开始

    我正在学习 Java 游戏方面 我买了一本书 里面有一些代码 我尝试复制并测试它 唯一的问题是 当我尝试编译它时 它会出现错误 C Users James Desktop Java gt Javac GamePanel java GameP
  • 使用 mvel 检查 List 中是否存在元素

    我随身带着一份清单 清单就像 List
  • 从java程序调用SVN命令

    我想从 java 程序调用 SVN 命令 update commit 有什么帮助吗 SVN 乌龟SVN 环境 java程序将在jBoss服务器内运行 从应用程序服务器内使用 GUI SVN 客户端是一个非常非常糟糕的主意 而Tortoise
  • 用简单的英语来说,什么是单例?

    我已经在谷歌上搜索了大约一个小时 但我仍然不清楚什么是单例 谁能让我更清楚一点 也许可以发布一个代码示例 我所知道的是 一个给定类只能有一个实例 但是你不能为此使用静态类吗 提前致谢 The simple plain English1 ve
  • 使用 Google Guava 进行不区分大小写的排序

    目前 我在两个不同的地方使用以下两段代码来创建一个排序的 不可变的列表 return Ordering natural immutableSortedCopy iterable and return Ordering usingToStri
  • 为什么 Eclipse 要求我在 java 代码中设置(任意)括号?

    我目前正在尝试弄清楚如何使用 Eclipse 在 java 中对 Escape 模型进行编程 我对 Escape 和 Eclipse 很陌生 自从我用 java 编程以来已经有一段时间了 所以如果这是一个愚蠢的问题 请原谅 基本上 我一直被
  • SQL状态[99999];错误代码[17004];无效的列类型:1111 使用 Spring SimpleJdbcCall

    大家好 我正在使用 spring 简单的 JDBC 模板来调用 oracle 过程 下面是我的代码 步骤 create or replace PROCEDURE get all system users pi client code IN
  • @Service 中带有 Kotlin 的 Spring Boot @Autowired 始终为 null

    目前 我尝试使用 Kotlin 重写我的 Java Spring Boot 应用程序 我遇到了一个问题 在我所有的类中都用 Service依赖注入无法正常工作 所有实例都null 这是一个例子 Service Transactional o
  • 如何保护 Hibernate QBE 查询的安全

    目前 我知道四种使用 hibernate 进行事务处理的方式 使用对象 使用HQL 使用特定于数据库的 SQL 使用标准 QBE 好吧 关于它们对注射的抵抗力有多强 我认为是这些 如果我错了 请纠正我 安全 因为内部 SQL 调用是参数化的
  • 没有找到适合 jdbc.sqlite 的驱动程序

    所以首先我之前看到过这个问题 我查看了以前的答案并尝试用它来解决我的问题 但是我做不到 我正在创建一个图书馆系统 并为注册类和登录类编写了代码 并为数据库创建了一个类 当我尝试运行该程序时 我收到一条消息 指出没有找到合适的驱动程序 后跟数
  • Java 支持多行字符串吗?

    来自 Perl 我肯定缺少在源代码中创建多行字符串的 here document 方法 string lt lt EOF create a three line string text text text EOF 在 Java 中 当我从头
  • 如何关闭 Grizzly 日志记录?

    如何关闭 Grizzly 的日志记录 我想关闭以下日志记录 Okt 18 2018 8 42 24 AM org glassfish grizzly http server NetworkListener start INFORMATION
  • 无法解析符号“servlet”

    我有一个新手大问题 当我尝试以下操作时 servlet 变成红色并指示 无法解析符号 servlet import javax servlet http import javax servlet ServletException 我的 ap
  • DateTimeFormatter 中的通配符

    我需要将一个字符串解析为LocalDate 该字符串看起来像31 03 2016用正则表达式术语 即 表示日期数字后可能有 0 个或多个未知字符 输入 输出示例 31xy 03 2016 gt 2016 03 31 我希望在 DateTim
  • 在Fragment中第一次调用时SharedPreferences为空

    我有一个示例 Android 应用程序 根据位置 邮政编码 和设置 SharedPreference 中设置的温度单位 该应用程序显示 7 天的天气 当应用程序第一次获取温度并检查 SharedPreference 中设置的温度单位时 它似
  • 如何将 try-with-resources 与 if 语句一起使用?

    我有简单的代码 try FileReader file new FileReader messageFilePath BufferedReader reader new BufferedReader file String line whi
  • 如何从 Ant 构建文件设置 Eclipse 构建路径和类路径?

    关于 Ant 和 Eclipse 有很多讨论 但之前的答案似乎对我没有帮助 事情是这样的 我正在尝试构建一个可以从命令行使用 Ant 成功编译的 Java 程序 更令人困惑的是 我尝试编译的程序是 Ant 本身 我真正想做的是将这个项目引入
  • Spring MVC 中拦截器和过滤器的区别

    我有点困惑Filter and Interceptor目的 据我从文档中了解到 Interceptor在请求之间运行 另一方面Filter在渲染视图之前运行 但在控制器渲染响应之后运行 那么两者的区别在哪里postHandle 在拦截器和d
  • 有Commons AnnotationUtils之类的库吗? (爪哇)

    除了直接使用注释 api 并编写自己的注释或使用 Springs 之外 我找不到用于查询注释的通用实用程序 静态方法 库 Springs 注释实用程序 http static springsource org spring docs 2 5

随机推荐

  • 【python 多线程存数据lock(锁)】

    多线程存数据不会数据丢失 案例一 这里只是简单的线程池 import os from concurrent futures import ThreadPoolExecutor from time import perf counter im
  • PTA-计算工资

    计算工资 某公司员工的工资计算方法如下 一周内工作时间不超过40小时 按正常工作时间计酬 超出40小时的工作时间部分 按正常工作时间报酬的1 5倍计酬 员工按进公司时间分为新职工和老职工 进公司不少于5年的员工为老职工 5年以下的为新职工
  • TypeScript 之类型判断

    在使用 Angular 做项目的时候 对 TypeScript 的类型判断不太熟练 为了方便查找 特意对 TypeScript 的类型判断做了简单梳理 文章只是 TS 官网的内容摘要 没有高深的知识 想要深入学习 TS 还要看官网文档 基础
  • 18M 超轻量系统开源

    图像识别作为深度学习算法的主流实践应用方向 早已在生活的各个领域发挥作用 如安全检查和身份核验时的人脸识别 无人货架和智能零售柜中的商品识别 这些任务背后的关键技术都在于此 图1 PP ShiTu应用于商品识别效果示意 开发者应用展示 然而
  • JMeter压测原则之独立部署监控

    无论是用哪种压测工具 我们都会比较关心压测工具所在机器的的系统资源占用情况 毕竟很多人压着压着 压力机出现性能瓶颈了还不知道 并且还错误的评估成是被测系统的性能问题 很多初学者好像都犯过这种尴尬的错误 文章分成三个部分说明 为什么说Perf
  • a &a &a[0]之间的区别和联系

    数组中 a为数组的首地址 a 0 为数组第一个元素的地址 所以 a a 0 但是 a又是什么东西呢 我们来做下面的代码测试 include
  • Binary operator ‘==‘ cannot be applied to operands of type ‘Int‘ and ‘[Int]‘

    等号两边数据类型不一致进行比较报错 Binary operator cannot be applied to operands of type Int and Int 这个提示也挺明确 二元运算符 两边不能使用 Int 和 Int 写代码时
  • c++ socket、 listen、accept、recv 、send、 connect函数记录

    文章目录 socket bind 和connect 函数 listen 和accept 函数 send recv read 和write 函数 TCP客户端 Tcp服务端 socket int socket int domain int t
  • 如何进行代码审查?

    如何review开发人员的代码 前置的一些概念 review级别 参与人身份和方式不同划分 相关开发自己看代码 非正式会议 开发人员组内 相关开发 直接上级 相关开发 直接上级 总监 1 团队review制度 团队内根据实际情况规定流程 在
  • 与机器学习相关的数学家,你认识几个?

    机器学习 需要一定的数学基础 也需要一定的代码能力 我们发布了一篇 机器学习的数学基础 里面有很多数学公式是数学家的名字命名的 然而 好多人不知道那些数学家长什么样子 于是 我们搜集了十位数学家的资料 排名不分先后 看看大家能从图片中叫出几
  • Vue_test

    文章目录 vue test笔记 1 脚手架文件结构 2 关于不同版本的Vue 3 vue config js配置文件 4 ref属性 5 配置项props 6 mixin 混入 7 插件 8 scoped样式 9 总结TodoList案例
  • Neo4js安装报错:未能加载指定的模块“\Neo4j-Management.psd1”

    情形如下 解决方法 更改bin neo4j ps1文件里Import Module PSScriptRoot Neo4j Management psd1 为绝对路径
  • Mybatis-generator代码自动生成(包含swagger注解,bean中文注释,service接口,serviceImpl实现类)

    Mybatis generator代码自动生成 包含swagger注解 bean中文注释 service接口 serviceImpl实现类 Dao接口是继承tkmybatis 简介 项目地址 代码生成步骤 代码生成图示 简介 由于平时老是需
  • 公众号上传临时素材获取media_id

    公众号上传临时素材获取media id java语言 更新于2018 01 15 public class Util private static final String UPLOAD URL https api weixin qq co
  • Linux驱动系列-PWM驱动

    转自 嵌入式系统研发 1 概述 本文主要讲述了Linux的PWM驱动框架 实现方法 驱动添加方法和调试方法 示例Linux内核版本 6 2 8 2 原理 PWM是Pulse Width Modulation的简称 中文译作脉冲宽度调制 作为
  • Vue实现底部对话框

    效果 手机上的效果 电脑上的效果 代码 App vue
  • 手写一个简化版 vuepress 需要知道什么?

    自实现 vuepress 效果图如下 首先我们来看看 vuepress 是怎么工作的 1 全局安装 vuepress npm install g vuepress 2 运行编写好的 docs 文件 编译后的浏览器显示文档网页 vuepres
  • 谭浩强C++课后习题20——找二维数组的鞍点

    谭浩强C 课后习题20 找二维数组的鞍点 题目描述 找出一个二维数组中的鞍点 即该位置上的元素在该行上最大 在该列上最小 也有可能没有鞍点 一个二维数组最多只有一个鞍点 也有可能没有 算法思路 先找出一行中值最大的元素 然后检查它是否是该列
  • 一、Vite css处理(模块module、sass less预处理、获取CSS代码字符串)

    文章目录 一 参考 二 CSS 自动导入 三 import 内联和变基 四 PostCSS 五 CSS 模块化 module css 5 1 自定义 CSS module 导出命名规则 5 2 为什么要有 CSS module 5 3 CS
  • java 手写一个简易的 tomcat 服务器

    最近想学一下 tomcat 到底是怎么运行的 于是手写一个简易的 tomcat 服务器 加深一下印象 涉及到的知识 计算机网络 tcp ip 协议 网络编程 socket 浏览器请求 响应报文规范 java 反射 注解 实现的功能 可打包成