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

最近想学一下 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://i-blog.csdnimg.cn/blog_migrate/5947b72e0ec57f19ce9c3a9bbd3aed38.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://i-blog.csdnimg.cn/blog_migrate/844de3b0fb81953909aff16c19eab1a9.png


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

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值