深入核心:一步步手撕Tomcat搭建自己的Web服务器

介绍:

servlet:处理 http 请求

tomcat:服务器

Servlet

servlet 接口:
  1. 定义 Servlet 声明周期
  2. 初始化:init
  3. 服务:service
  4. 销毁:destory
  5. 继承链:

Tomcat

Tomcat 和 servlet 原理:
  1. 浏览器向服务器发送 http 请求
  2. socket 接收请求,发送给请求解析器
  3. 请求解析器再解析自己想要的信息
    1. 请求地址
    2. 请求方式
    3. 浏览器类型
    4. Cookie
    5. ··········
  4. 解析器解析到的信息发送给映射器
  5. 映射器中存放:
    1. Web 地址
    2. 内存地址
  6. 根据请求解析器中解析的信息,找到映射器中相对应的网络地址和内存地址,根据请求方式去访问对应的程序
Socket 交互以及解析阶段:
java 复制代码
package com.Tomcat;


import com.sun.corba.se.spi.activation.Server;

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

public class myTomcat {
    Request request = new Request();    //创建解析请求的对象

    public void startUP() throws IOException {
        //监听端口号
        ServerSocket serverSocket = new ServerSocket(7421);

        while(true){
            Socket socket = serverSocket.accept();      //阻塞监听
            System.out.println("有请求!!!!!!!");

            //将每个请求开启一个线程
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        handler(socket);    //调用处理信息方法
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }

    //处理信息
    public void handler(Socket socket) throws IOException {
        InputStream inputStream = socket.getInputStream();

        //将bit流转换为文字信息
        int count = 0;
        while(count == 0){
            count = inputStream.available();        //统计输入流的长度
        }

        //打印数据
        byte[] bytes = new byte[count];
        inputStream.read(bytes);    //将bit信息写入到byte数组
        String Context = new String(bytes);     //将 byte 数组转换为字符串
        System.out.println(Context);    //输出信息

        //拆解字符串,获取想要的信息
        String[] list = Context.split("\\n");   //根据换行切割字符串
        String Methed = list[0].split(" ")[0];  //在拆分的第一行中以空格再次拆分,获取第一个数据
        String Path = list[0].split(" ")[1];    //在拆分的第一行中以空格再次拆分,获取第二个数据

        //把截取的数据传给 Request 类
        request.setMethod(Methed);
        request.setPath(Path);
        
    }
}
Request 存储解析信息 ==> 继承 HttpservletRequest,为方便访问同一变量
java 复制代码
public class Request implements HttpservletRequast {
    private String Method;
    private String Path;

    public String getMethod() {
        return Method;
    }

    public void setMethod(String method) {
        Method = method;
    }

    public String getPath() {
        return Path;
    }

    public void setPath(String path) {
        Path = path;
    }

    @Override
    public String toString() {
        return "Request{" +
                "Method='" + Method + '\'' +
                ", Path='" + Path + '\'' +
                '}';
    }


}
扫包:
java 复制代码
/**
 * 扫描指定包,获取该包下所有的类的全路径信息
 */
public class SearchClassUtil {
    //存放文件的绝对路径
    public static  List<String> classPaths = new ArrayList<String>();

    /**
     * 扫描固定包下面的类
     * @return
     */
    public static List<String> searchClass(){
        //需要扫描的包名
        String basePack = "com.servlet";      //写需要获取包名的路径

        //将获取到的包名转换为路径

        //getResource():是获取类对象的方法, "/" :表示在根目录开始
        //getPath():是将对象的路径转为字符串
        String classPath = SearchClassUtil.class.getResource("/").getPath();

         //将包名转换为文件系统路径  --->  把 "." 替换成 系统的路径分隔符(系统不一样,分隔符也不一样)
        basePack =  basePack.replace(".", File.separator);
        
        //把两个路径合并为文件的 绝对路径
        String searchPath = classPath + basePack;
        
        //调用doPath()方法,把路径写入路径数组中
        doPath(new File(searchPath),classPath);
        //这个时候我们已经得到了指定包下所有的类的绝对路径了。我们现在利用这些绝对路径和java的反射机制得到他们的类对象
        return classPaths;
    }

    /**
     * 该方法会得到所有的类,将类的绝对路径写入到classPaths中
     * @param file
     */
    private static void doPath(File file,String classpath) {
        if (file.isDirectory()) {//当前为文件夹
            //文件夹我们就递归  --->  筛出文件夹
            File[] files = file.listFiles();
            for (File f1 : files) {
                doPath(f1,classpath);
            }
        } else {//标准文件
            //标准文件我们就判断是否是class文件
            if (file.getName().endsWith(".class")) {
                
                //各级拆解字符串,替换分隔符
                String path = file.getPath().replace(classpath.replace("/","\\").
                                replaceFirst("\\\\",""),"").replace("\\",".").
                        replace(".class","");
                //如果是class文件我们就放入我们的集合中。
                classPaths.add(path);
            }
        }
    }

    public static void main(String[] args) {
        List<String> classes = SearchClassUtil.searchClass();
        for (String s: classes) {
            //System.out.println(s);
        }
    }
}
注解:设置文件的访问地址 ==> HashMap 中的 key 值
java 复制代码
@Retention(RetentionPolicy.RUNTIME)     //在运行期间保留
@Target(ElementType.TYPE)       //作用于类上面
public @interface Webservlet {
    String url() default "";
}
创建 Httpservlet 实现 Service 服务:
java 复制代码
public abstract class HttpServlet {   //HttpServerlet只实现了父类的service服务,其他方法没有实现,此时该类为抽象类

    //子类需要使用doGet或doPost方法,在这里直接让子类去实现两个发给发
    //这里需要获取Request中被解析出来的数据,
    //要想访问的是同一个Request对象,这里用到接口,让Request实现这个接口,传参时就会向上转型,此时request对象为同一个对象
    public abstract void doGet(HttpServletRequast requast, HttpServletResponse response) throws Exception;
    public abstract void doPost(HttpServletRequast requast,HttpServletResponse response);

    //在服务中判断用户的请求方式,让子类实现向对应的方法
    public void service(HttpServletRequast requast,HttpServletResponse response) throws Exception {
        if(requast.getMethod().equals("GET")){
            doGet(requast,response);
        }else if(requast.getMethod().equals("POST")){
            doPost(requast,response);
        }
    }
}
HttpservletRequast:为 Httpservlet 访问对象为统一对象,让 Request 实现这个接口
java 复制代码
public interface HttpservletRequast {

    String getMethod();

    void setMethod(String method);

    String getPath();

    void setPath(String path);
}
自己创建 servlet,继承 Httpservlet 实现 service 服务 ==> 实现相关的访问方式
java 复制代码
@WebServerlet(url = "OneServerlet")
public class FirstServlet extends HttpServlet {

    @Override
    public void doGet(HttpServletRequast requast, HttpServletResponse response) throws Exception {

    }

    @Override
    public void doPost(HttpServletRequast requast, HttpServletResponse response) {

    }
}
获取访问地址:HashMap 中的 key 值
java 复制代码
public class getMessageUtil {
    
    public static String fund(String path) throws Exception {
        //创建类对象
        Class clazz = Class.forName(path);

        //根据类对象调用 getDeclaredAnnotation() 方法找到该类的访问地址(@Webservlet()中的内容)
        Webservlet webservlet = (Webservlet) clazz.getDeclaredAnnotation(Webservlet.class);
        return webservlet.url();
    }

    public static void main(String[] args) throws Exception {
        //fund();
    }
}
  1. 映射器:底层由 HashMap 容器存储

    java 复制代码
    public class ServletConfigMapping {
        //将执行逻辑写入static代码块中,以便更好加载
        
        //定义Servlet容器
        public static Map<String,Class<HttpServlet>> classMap = new HashMap<>();
        
        //该静态代码块应放在启动tomcat前运行
        static {
            List<String> classPaths = SearchClassUtil.searchClass();
        
            for (String classPath : classPaths){
                try {
                    InitClassMap(classPath);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
        
        //将key val 值插入到HashMap中
        public static void InitClassMap(String classPath) throws Exception {
            //获取类对象
            Class clazz = Class.forName(classPath);
        
            //获取访问地址
            String url = getMessageUtil.fundUrl(classPath);
        
            //将值插入HashMap树当中
            classMap.put(url,clazz);
        }
    }

Response 返回数据:

设置返回头工具类:
java 复制代码
/**
 * 设置返回头
 */
public class ResponseUtil {
    public  static  final String responseHeader200 = "HTTP/1.1 200 \r\n"+
            "Content-Type:text/html \r\n"+"\r\n";

    public  static  final String responseHeader200JSON = "HTTP/1.1 200 \r\n"+
            "Content-Type:application/json \r\n"+"\r\n";

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

    public static String getResponseHeader200(String context){
        return "HTTP/1.1 200 \r\n"+
                "Content-Type:text/html \r\n"+"\r\n" + context;
    }
}
读取文件:根据提供的地址转化为文件完整地址
java 复制代码
/**
 * 该类的主要作用是进行读取文件
 */
public class FileUtil {

    public  static  boolean witeFile(InputStream inputStream, OutputStream outputStream){
        boolean success = false ;
        BufferedInputStream bufferedInputStream ;
        BufferedOutputStream bufferedOutputStream;

        try {
            bufferedInputStream = new BufferedInputStream(inputStream);
            bufferedOutputStream = new BufferedOutputStream(outputStream);
            bufferedOutputStream.write(ResponseUtil.responseHeader200.getBytes());

            int count = 0;
            while (count == 0){
                count = inputStream.available();
            }
            int fileSize = inputStream.available();
            long written = 0;
            int beteSize = 1024;
            byte[] bytes = new byte[beteSize];
            while (written < fileSize){
                if(written + beteSize > fileSize){
                    beteSize = (int)(fileSize - written);
                    bytes = new byte[beteSize];
                }
                bufferedInputStream.read(bytes);
                bufferedOutputStream.write(bytes);
                bufferedOutputStream.flush();
                written += beteSize;
            }
            success = true;

        } catch (IOException e) {
            e.printStackTrace();
        }
        return success;
    }

    public static boolean writeFile(File file,OutputStream outputStream) throws Exception{
        return witeFile(new FileInputStream(file),outputStream);
    }

    /**
     * 根据提供的地址转换为文件完整地址
     * @param path
     * @return
     */
    public static String getResoucePath(String path){
        String resource = FileUtil.class.getResource("/").getPath();
        return resource + "\\" + path;
    }

}
response 返回数据:
java 复制代码
public class Response implements HttpServletResponse {
    //输出流
    private OutputStream outputStream;

    public Response(OutputStream outputStream){
        this.outputStream = outputStream;
    }
    
     /***
     * 返回动态文字信息
     * @param context
     * @throws IOException
     */
    public void write(String context) throws IOException {
        outputStream.write(context.getBytes());     //将文字信息转换为 byte流 形式
    }

    public void WriteHtml(String Path) throws Exception {
        //得到文件全路径
        String resoucePath = FileUtil.getResoucePath(Path);
        //创建文件
        File file = new File(resoucePath);

        if(file.exists()){
            System.out.println("静态资源存在");
            //输出静态资源
            FileUtil.writeFile(file,outputStream);
        }else {
            System.out.println("静态资源不存在");
        }
    }
}
HttpServletResponse 接口:
java 复制代码
public interface HttpServletResponse {

    void write(String context) throws IOException;
}
输出资源:
java 复制代码
Response response = new Response(socket.getOutputStream());
    if(request.getPath().equals("") || request.getPath().equals("/")){      //空访问
        response.WriteHtml("404.html");     //抛出404页面
        response.write(ResponseUtil.getResponseHeader404());    //抛出404文字信息
    } else if (ServerletConfigMapping.classMap.get(request.getPath()) == null) {        //静态资源
        response.WriteHtml(request.getPath());
    }else {     //动态资源
        Class<HttpServlet> httpServletClass = ServerletConfigMapping.classMap.get(request.getPath());   //获取类对象
        if(httpServletClass != null){       //有类对象
            HttpServlet httpServlet = httpServletClass.newInstance();   //多态创建对象
            httpServlet.service(request,response);      //启动service服务
        }else{      //没有动态资源
            response.WriteHtml("404.html");     //抛出 404页面
        }
}
整合后 Tomcat:
java 复制代码
public class myTomcat {

    Request request = new Request();    //创建解析请求的对象

    //提前加载容器(HashMap)
    static {
        List<String> classPaths = SearchClassUtil.searchClass();

        for (String classPath : classPaths){
            try {
                ServerletConfigMapping.InitClassMap(classPath);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws IOException {
        myTomcat myTomcat = new myTomcat();
        myTomcat.startUP();
        
    }

    public void startUP() throws IOException {
        //监听端口号
        ServerSocket serverSocket = new ServerSocket(8080 );

        while(true){
            Socket socket = serverSocket.accept();      //阻塞监听
            System.out.println("有请求!!!!!!!");

            //将每个请求开启一个线程
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        handler(socket);    //调用处理信息方法
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }

    //处理信息
    public void handler(Socket socket) throws Exception {
        InputStream inputStream = socket.getInputStream();

        //将bit流转换为文字信息
        int count = 0;
        while(count == 0){
            count = inputStream.available();        //统计输入流的长度
        }

        //打印数据
        byte[] bytes = new byte[count];
        inputStream.read(bytes);    //将bit信息写入到byte数组
        String Context = new String(bytes);     //将 byte 数组转换为字符串
        System.out.println(Context);    //输出信息

        //拆解字符串,获取想要的信息
        String[] list = Context.split("\\n");   //根据换行切割字符串
        String Methed = list[0].split(" ")[0];  //在拆分的第一行中以空格再次拆分,获取第一个数据
        String Path = list[0].split(" ")[1];    //在拆分的第一行中以空格再次拆分,获取第二个数据

        //把截取的数据传给 Request 类
        request.setMethod(Methed);
        request.setPath(Path);





       //判断资源类型
        Response response = new Response(socket.getOutputStream());
        if(request.getPath().equals("") || request.getPath().equals("/")){      //空访问
            response.WriteHtml("404.html");     //抛出404页面
            response.write(ResponseUtil.getResponseHeader404());    //抛出404文字信息
        } else if (ServerletConfigMapping.classMap.get(request.getPath()) == null) {        //静态资源
            response.WriteHtml(request.getPath());
        }else {     //动态资源
            Class<HttpServlet> httpServletClass = ServerletConfigMapping.classMap.get(request.getPath());   //获取类对象
            if(httpServletClass != null){       //有类对象
                HttpServlet httpServlet = httpServletClass.newInstance();   //多态创建对象
                httpServlet.service(request,response);      //启动service服务
            }else{      //没有动态资源
                response.WriteHtml("404.html");     //抛出 404页面
            }
        }

    }
}

Tomcat 运行原理:

原理:
  1. 浏览器发起请求
  2. Socket 解析输入流,获取请求头信息
  3. 分析请求的地址是动态资源还是静态资源
    1. 首先判断 HashMap 中有没有这个 Key 值
    2. 如果有就去访问动态资源,如果没有就去查看静态资源
    3. 如果也不是静态资源就返回 404
  4. Servlet 容器(HashMap):
    1. 将 @WebServlet 中的值作为 key 值,将对象作为 value 值,存入 HashMap 中
Servlet 容器加载时期:
  1. 在 Socket 启动之前启动 Servlet 容器
    1. 缺点:程序启动时间变长
    2. 优点:不易出现空指针
  2. 在 Socket 启动之后启动 Servlet 容器
  3. 在浏览器访问的同时启动 Servlet 容器
相关推荐
向着开发进攻17 分钟前
Linux 常用命令与实战教程
linux·运维·服务器
狂爱代码的码农25 分钟前
Ubuntu22.04如何设置linux-lowlatency核心
服务器
领秀585827 分钟前
我问了DeepSeek和ChatGPT关于vue中包含几种watch的问题,它们是这么回答的……
前端·javascript·vue.js
16年上任的CTO42 分钟前
vue2-mixin的定义与和使用
前端·javascript·vue.js·mixin
呦呦鹿鸣Rzh1 小时前
HTML-表格,表单标签
java·前端·html
柠檬豆腐脑1 小时前
从前端到全栈:搭建Gitlab并实现自动化部署全栈项目
前端·gitlab
九州~空城2 小时前
Linux中系统相关指令(一)
linux·运维·服务器
轻口味2 小时前
Vue.js 使用 `teleport` 实现全局挂载
前端·javascript·vue.js
我码玄黄2 小时前
高效 MyBatis SQL 写法一
后端·sql·tomcat·mybatis
吴小花的博客2 小时前
日期选择控件,时间跨度最大一年。
前端·vue.js·elementui