从0开始,手搓Tomcat

一、什么是Tomcat

Tomcat 是一款开源的、轻量级的 Web 服务器,它不仅能够提供 HTTP 服务,还能够运行 Java Servlet 和 JavaServer Pages(JSP)。对于许多开发者来说,理解 Tomcat 的目录结构以及如何在该结构中组织应用,往往是入门的第一步。

1、Tomcat目录结构

Tomcat 的目录结构相对简单,但每个目录和文件都有其明确的用途。

1. bin 目录

bin 目录包含启动和停止 Tomcat 所需的脚本。它的内容包括:

startup.sh/startup.bat:启动 Tomcat。

shutdown.sh/shutdown.bat:停止 Tomcat。

catalina.sh/catalina.bat:Tomcat 启动时的核心脚本,包含了一些启动参数的设置。

setenv.sh/setenv.bat:用于设置 Tomcat 环境变量,通常用于配置 Java 堆大小、日志设置等。

2. conf 目录

conf 目录包含 Tomcat 的核心配置文件,这些文件直接影响 Tomcat 的行为。常见的配置文件有:

server.xml:Tomcat 的主要配置文件,定义了服务器的端口、连接器、虚拟主机等。

web.xml:Web 应用的默认部署描述符。

context.xml:为每个 Web 应用提供额外的配置。

3. lib 目录

lib 目录存放 Tomcat 运行时所需要的 Java 类库和 JAR 包,包括:

servlet-api.jar:Java Servlet API 的实现。

jsp-api.jar:Java Server Pages(JSP)API 的实现。

其他第三方库和 Tomcat 特有的库文件。

4. logs 目录

logs 目录用于存储 Tomcat 启动、运行过程中的日志文件。常见的日志文件有:

catalina.out:Tomcat 启动和运行时的主要日志文件。

localhost_access_log.*.txt:记录每个请求的访问日志。

5. webapps 目录

webapps 目录是 Tomcat 的应用目录,所有部署的 Web 应用都应该放在此目录下。每个应用通常以一个文件夹的形式存在,目录中包含了应用的 WEB-INF 和 META-INF 等文件夹。

6. work 目录

work 目录用于存放 Tomcat 编译 JSP 文件后的中间文件。每当 Tomcat 运行一个 JSP 文件时,都会将其编译成 Java 类文件并存放在这个目录中。

7. temp 目录

temp 目录是 Tomcat 用来存放临时文件的地方。例如,Tomcat 会将文件上传的内容存放在该目录中,处理过程中生成的临时数据也会保存在这里。

2、Tomcat的工作目录

在apache的Tomcat中,工作目录可以简化成上图,即在webapps中进行资源的访问。资源的访问可以简单的理解为对网页的操作。

二、Tomcat工作原理简介

1、Tomcat中Servlet的生命周期

在一个实现具体操作的Servlet类中,具有如下图的继承关系

javax.servlet.Servlet是所有 Servlet 的基本接口。任何自定义的 Servlet 都必须实现这个接口或者继承一个实现了它的类。该接口定义了 Servlet 的生命周期方法,例如 init(), service(), 和 destroy(),这些方法体现了Servlet的生命周期

init(..):当servlet第一次被请求时,Servlet容器就会开始调用这个方法来初始化一个Servlet对象出来

service(...):每当请求Servlet时,Servlet容器就会调用这个方法service(...)

destroy(...):当要销毁Servlet时,Servlet容器就会调用这个方法

getservletInfo(...):这个方法会返回Servlet的一段描述,可以返回一段字符串。

getServletconfig(...):这个方法会返回由Servlet容器传给init()方法的Servletconfig对象。

javax.servlet.GenericServlet 是 Servlet 接口的一个抽象实现类。它实现了 Servlet 接口,但提供了空实现的 service() 方法。通常,自定义的 Servlet 会继承 GenericServlet,并重写 service() 方法来处理客户端请求。

javax.servlet.http.HttpServlet 是处理 HTTP 请求的抽象类,继承自 GenericServlet。大多数 Web 应用中的 Servlet 会继承 HttpServlet,因为它提供了简化的 HTTP 请求处理方法,如 doGet(), doPost(), doPut() 和 doDelete(),这些方法可以根据请求类型进行重写。同时 HttpServlet 中重写了service() 方法,对于不同的请求进行判断并划分到不同的操作中,将其拆分成doGet()方法和doPost()等七种方法,其目的是为了更好的匹配http请求。

2、Tomcat对待资源

Tomcat将资源分为动态资源和静态资源。

1.静态资源

指内容在服务器上不会发生变化的资源,通常这些资源由服务器直接提供给客户端,不需要任何动态处理。常见的静态资源有:

  • HTML 文件:静态的网页内容,直接通过浏览器访问即可展示。
  • CSS 文件:样式表文件,用于定义网页的外观。
  • JavaScript 文件:脚本文件,用于网页的交互行为。
  • 图片文件:如 PNG、JPEG、GIF、SVG 等图片文件。
  • 字体文件:例如 .woff, .ttf 等字体文件。
  • 视频文件:如 .mp4, .avi 等文件。

Tomcat 处理静态资源的方式:

Tomcat 会直接通过文件系统(例如 /webapps/ROOT 目录)读取静态资源,然后返回给客户端。静态资源通常会被配置在 Web 应用的 webapps 目录下。

默认情况下,Tomcat 会直接将这些资源返回给客户端,除非配置了 URL 映射或者过滤器,否则不涉及任何额外的处理逻辑。

2.动态资源

动态资源 是指在客户端请求时,服务器需要根据请求的不同,动态生成或者处理后才返回的内容。这类资源的内容是可变的,通常与用户的输入、数据库等外部数据源有关。动态资源的常见形式包括:

  • Servlet:通过 Java 编写的后端代码,接收客户端请求并生成响应内容。
  • JSP (JavaServer Pages):与 Servlet 类似,但通过模板方式生成 HTML 内容。JSP 是动态生成 HTML 页面的一种方法。
  • RESTful API:通常由 Servlet 或其他 Java 后端框架实现,用于为前端应用提供数据接口。
  • WebSocket:允许在客户端与服务器之间进行双向通信的技术。

Tomcat 处理动态资源的方式

Tomcat 会将动态请求交给相应的 Servlet 或 JSP 进行处理。Servlet/JSP 会根据请求生成动态内容。请求 URL 会映射到特定的 Servlet 或 JSP,Tomcat 通过 web.xml 或注解来配置这些映射。

动态资源通常通过 Servlet 或 JSP 来生成 HTTP 响应,这些响应可以是 HTML、JSON、XML 等格式,具体取决于请求和后端代码的逻辑。

3.两种资源的区别

|------|---------|-----|----------------------------------|
| 资源类型 | 需要服务器处理 | 变化性 | 典型示例 |
| 静态资源 | 无 | 不变 | HTML、CSS、JavaScript、图片、视频等 |
| 动态资源 | 需要处理 | 可变 | Servlet、JSP、REST API、WebSocket 等 |

静态资源 直接由 Tomcat 提供,客户端请求时无需任何额外的动态处理。

动态资源 需要通过 Servlet 或 JSP 等方式在服务器端进行处理,然后生成响应内容返回给客户端。

3、Tomcat的核心------Servlet容器

Tomcat的核心是servlet容器,本质是一个hashmap,Key值是servlet的访问路径,Value值是一般为servlet对象。在启动tomcat时,动态资源加载到servlet容器中

http请求打到servlet容器中,进行key值对比,如果访问动态资源,则调用servlet对象

需要注意的一点是,在Tomcat启动时,就需要把动态资源加载到Servlet容器中。

4、Tomcat处理http请求

Tomcat 生成一个socket对象,用于接收和处理http请求,从请求中解析出请求类型和需要的资源,交给Servlet容器中的Servlet对象去处理

三、简易Tomcat实现

1、目录结构

这样设计为区分不同功能的Java文件。

需要指出的是,这里创建项目不是普通的Java项目,需要选择Maven项目,只需设置项目名称即可,其他默认即可,项目名不要有中文。

2、实现继承关系

Servlet接口

java 复制代码
package com.tom2.servlet;

import com.tom2.servlet.Re.HttpRequest;
import com.tom2.servlet.Re.HttpResponse;

public interface Servlet {
    public abstract void init();
    public abstract void destory();
    public abstract void service(HttpRequest request, HttpResponse response) throws Exception;
}

GenericServlet抽象类

java 复制代码
package com.tom2.servlet;

import com.tom2.servlet.Re.HttpRequest;
import com.tom2.servlet.Re.HttpResponse;

public abstract class GenericServlet implements Servlet{

    @Override
    public void init() {
        System.out.println("genservlet 的init");
    }

    @Override
    public void destory() {
        System.out.println("genservlet 的destory");
    }
}

HttpServlet 抽象类

java 复制代码
package com.tom2.servlet;

import com.tom2.servlet.Re.HttpRequest;
import com.tom2.servlet.Re.HttpResponse;

public abstract class HttpServlet extends GenericServlet{
    @Override
    public void service(HttpRequest request, HttpResponse response) throws Exception {
        if(request.getMethod().equals("GET")){
            doGet(request,response);
        }else if(request.getMethod().equals("POST")){
            doPost(request,response);
        }
    }

    public abstract void doGet(HttpRequest request, HttpResponse response) throws Exception;
    public abstract void doPost(HttpRequest request, HttpResponse response) throws Exception;

}

HttpRequest 和 HttpResponse

java 复制代码
package com.tom2.servlet.Re;

public class HttpRequest {
    private String path;
    private String method;

    public String getPath() {
        return path;
    }

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

    public String getMethod() {
        return method;
    }

    public void setMethod(String method) {
        this.method = method;
    }

    @Override
    public String toString() {
        return "HttpRequest{" +
                "path='" + path + '\'' +
                ", method='" + method + '\'' +
                '}';
    }
}

package com.tom2.servlet.Re;

import java.io.IOException;
import java.io.OutputStream;

public class HttpResponse {
    private OutputStream outputStream;

    public  HttpResponse(OutputStream outputStream){
        this.outputStream=outputStream;
    }

    public void writeResponse(String context) throws IOException {
        outputStream.write(context.getBytes());
    }

}

3、工具类

SearchClassUtil :利用反射进行类信息的查找。扫描指定的 Java 包目录,查找所有 .class 文件,并提取每个 .class 文件的全类名,将这些类名存储在一个 List 列表中。

java 复制代码
package com.tom2.Util;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

/**
 * 扫描 com.qcby.webapps 目录下的 Java 文件并获取每个类的全名
 */
public class SearchClassUtil {
    public static List<String> classPaths = new ArrayList<String>();
    public static List<String> searchClass() {
        // 需要扫描的包名
        String basePack = "com.tom2.webapps";
        // 将包名转换为路径格式
        String classPath = SearchClassUtil.class.getClassLoader().getResource("").getPath();

        basePack = basePack.replace(".", File.separator);

        if(classPath.startsWith("file:")){
            classPath.substring(5);
        }
        String searchPath = classPath + basePack;

        // 确保路径在开发环境和打包后的环境下都能正确处理
        File searchDir = new File(searchPath);
        if (!searchDir.exists()) {
            // 如果路径不存在,尝试扫描 target/classes 目录
            searchPath = "target/classes"+File.separator+basePack;
            searchDir = new File(searchPath);
        }

        // 调用 doPath 方法递归扫描文件
        doPath(searchDir, classPath);

        // 返回扫描到的类路径列表
        return classPaths;
    }

    /**
     * 递归处理文件,找到 .class 文件并将其全类名添加到 classPaths 列表中
     */
    private static void doPath(File file, String classpath) {
        if (file.isDirectory()) {
            // 如果是目录,递归处理子目录
            File[] files = file.listFiles();
            if (files != null) {
                for (File f1 : files) {
                    doPath(f1, classpath);
                }
            }
        } else {
            // 如果是 .class 文件,提取类名
            if (file.getName().endsWith(".class")) {
                // 修正路径替换:不使用 replaceFirst,而直接使用 replace
                String path = file.getPath()
                        .replace(classpath, "") // 直接使用 classpath,不再替换 "/" 为 File.separator
                        .replace(File.separator, ".")
                        .replace(".class", "");
                if(path.startsWith("targetes.")){
                    path = path.substring("targetes.".length());
                }

                // 将类名添加到 classPaths 列表
                classPaths.add(path);
            }
        }
    }

    public static void main(String[] args) {
        List<String> classes = SearchClassUtil.searchClass();
        for (String s : classes) {
            System.out.println(s);
        }
    }
}

WebServlet:注解

java 复制代码
package com.tom2.Util;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface WebServlet {
    String urlMapping() default "";
}

ResponseUtil

java 复制代码
package com.tom2.Util;

public class ResponseUtil {
    public  static  final String responseHeader200 = "HTTP/1.1 200 \r\n"+
            "Content-Type:text/html; charset=utf-8 \r\n"+"\r\n";

    public static String getResponseHeader404(){
        return "HTTP/1.1 404 \r\n"+
                "Content-Type:text/html; charset=utf-8 \r\n"+"\r\n" + "404";
    }

    public static String getResponseHeader200(String context){
        return "HTTP/1.1 200 \r\n"+
                "Content-Type:text/html; charset=utf-8 \r\n"+"\r\n" + context;
    }
}

5、模拟Servlet容器

SearchServlet

java 复制代码
package com.tom2;

import com.tom2.Util.SearchClassUtil;
import com.tom2.Util.WebServlet;
import com.tom2.servlet.HttpServlet;
import java.util.*;

public class SearchServlet {
    public static Map<String, HttpServlet> servletmap = new HashMap<>();

    static {
        List<String> classpath = SearchClassUtil.searchClass();
        for(String path:classpath){
            System.out.println(path);
        }
        for(String path:classpath){
            try{
                Class clazz = Class.forName(path);
                WebServlet webServlet = (WebServlet) clazz.getDeclaredAnnotation(WebServlet.class);
                HttpServlet httpServlet = (HttpServlet) clazz.getDeclaredConstructor().newInstance();
                servletmap.put(webServlet.urlMapping(),httpServlet);
            }catch(Exception e){
                e.printStackTrace();
            }

        }
    }

}

6、动态资源

Login

java 复制代码
package com.tom2.webapps.myweb;

import com.tom2.Util.WebServlet;
import com.tom2.servlet.HttpServlet;
import com.tom2.servlet.Re.HttpRequest;
import com.tom2.servlet.Re.HttpResponse;
@WebServlet(urlMapping = "/login")
public class Login extends HttpServlet {
    @Override
    public void doGet(HttpRequest request, HttpResponse response) throws Exception {
        System.out.println("俺是login的doGet");
    }

    @Override
    public void doPost(HttpRequest request, HttpResponse response) throws Exception {

    }
}

Show

java 复制代码
package com.tom2.webapps.myweb;
import com.tom2.Util.ResponseUtil;
import com.tom2.Util.WebServlet;
import com.tom2.servlet.HttpServlet;
import com.tom2.servlet.Re.HttpRequest;
import com.tom2.servlet.Re.HttpResponse;
@WebServlet(urlMapping = "/show")
public class Show extends HttpServlet {
    @Override
    public void doGet(HttpRequest request, HttpResponse response) throws Exception {
        response.writeResponse(ResponseUtil.getResponseHeader200("俺是Show的doGet"));
    }

    @Override
    public void doPost(HttpRequest request, HttpResponse response) throws Exception {

    }
}

7、主方法

MyTomcat

java 复制代码
package com.tom2;

import com.tom2.servlet.HttpServlet;
import com.tom2.servlet.Re.HttpRequest;
import com.tom2.servlet.Re.HttpResponse;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class MyTomcat {
    private static HttpRequest httpRequest = new HttpRequest();

    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(8080);
        while(true){
            Socket socket = serverSocket.accept();
            InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream();
            HttpResponse httpResponse = new HttpResponse(outputStream);

            int len = 0;
            while(len==0){
                len = inputStream.available();
            }
            byte[] bytes = new byte[len];
            inputStream.read(bytes);
            String context = new String(bytes);
            System.out.println(context);
            if(context.equals("")){
                System.out.println("空请求");
            }else{
                String line1 = context.split("\\n")[0];
                String method = line1.split("\\s")[0];
                String path = line1.split("\\s")[1];
                httpRequest.setMethod(method);
                httpRequest.setPath(path);
            }
            System.out.println(SearchServlet.servletmap.containsKey(httpRequest.getPath()));
            if(SearchServlet.servletmap.containsKey(httpRequest.getPath())){
                HttpServlet httpServlet = SearchServlet.servletmap.get(httpRequest.getPath());
                httpServlet.service(httpRequest,httpResponse);
            }
        }
    }
}

8、模拟效果

在浏览器地址栏中输入:localhost:8080/login

在浏览器地址栏中输入:localhost:8080/show

相关推荐
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax