欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

手写简易版tomcat

程序员文章站 2024-03-22 23:53:22
...

功能

  • Minicat要做的事情:作为一个服务器软件提供服务的,也即我们可以通过浏览器客户端发送http请求, Minicat可以接收到请求进行处理,处理之后的结果可以返回浏览器客户端。

目标

  1. 提供服务,接收请求(Socket通信)
  2. 请求信息封装成Request对象(Response对象)
  3. 客户端请求资源,资源分为静态资源(html)和动态资源(Servlet)
  4. 资源返回给客户端浏览器

改进

  • V1.0需求:浏览器请求http://localhost:8080,返回一个固定的字符串到⻚面"Hello Minicat!"
  • V2.0需求:封装Request和Response对象,返回html静态资源文件
  • V3.0需求:可以请求动态资源(Servlet)
V1.0 返回一个固定的字符串到⻚面"Hello Minicat!"

启动类,程序入口

public class Bootstrap {

    /**
     * 定义socket监听的端口号
     */
    private int port = 8080;

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }

    /**
     * 启动方法
     *
     * @throws IOException
     */
    private void start() throws IOException {

        ServerSocket serverSocket = new ServerSocket(port);
        System.out.println("=====>>>Minicat start on port:" + port);

        /**
         * 完成Minicat 1.0版本
         * 需求:浏览器请求http://localhost:8080,返回一个固定的字符串到页面"Hello Minicat!"
         */
        while (true) {
            Socket socket = serverSocket.accept();
            // 有了socket,接收到请求,获取输出流
            OutputStream outputStream = socket.getOutputStream();
            String data = "Hello Minicat!";
            String responseText = HttpProtocolUtil.getHttpHeader200(data.getBytes().length) + data;
            outputStream.write(responseText.getBytes());
            socket.close();
        }
    }

    /**
     * Minicat 的程序启动入口
     *
     * @param args
     */
    public static void main(String[] args) {
        Bootstrap bootstrap = new Bootstrap();
        try {
            // 启动Minicat
            bootstrap.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

工具类

/**
 * @Description http协议工具类,主要是提供响应头信息,这里我们只提供200和404的情况
 * @Author rpp
 * @Date 2020/7/18 2:59 下午
 */
public class HttpProtocolUtil {

    /**
     * 为响应码200提供请求头信息
     *
     * @return
     */
    public static String getHttpHeader200(long contentLength) {
        return "HTTP/1.1 200 OK \n" +
                "Content-Type: text/html \n" +
                "Content-Length: " + contentLength + " \n" +
                "\r\n";
    }

    /**
     * 为响应码404提供请求头信息(此处也包含了数据内容)
     *
     * @return
     */
    public static String getHttpHeader404() {
        String str404 = "<h1>404 not found</h1>";
        return "HTTP/1.1 404 NOT Found \n" +
                "Content-Type: text/html \n" +
                "Content-Length: " + str404.getBytes().length + " \n" +
                "\r\n" + str404;
    }
}

效果验证:启动后浏览器输入http://localhost:8080/
手写简易版tomcat

V2.0 封装Request和Response对象,返回html静态资源文件
  1. 封装request,根据输入字节流解析出请求方式及url,代码如下
/**
 * 把请求信息封装为Request对象(根据InputSteam输入流封装)
 */
public class Request {

    /**
     * 请求方式,比如GET/POST
     */
    private String method;

    /**
     * 例如url /,/index.html
     */
    private String url;

    /**
     * 输入流,其他属性从输入流中解析出来
     */
    private InputStream inputStream;


    public String getMethod() {
        return method;
    }

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

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public InputStream getInputStream() {
        return inputStream;
    }

    public void setInputStream(InputStream inputStream) {
        this.inputStream = inputStream;
    }

    public Request() {
    }

    /**
     * 构造器,输入流传入
     *
     * @param inputStream
     * @throws IOException
     */
    public Request(InputStream inputStream) throws IOException {
        this.inputStream = inputStream;

        // 从输入流中获取请求信息
        int count = 0;
        while (count == 0) {
            count = inputStream.available();
        }

        byte[] bytes = new byte[count];
        inputStream.read(bytes);

        String inputStr = new String(bytes);
        // 获取第一行请求头信息
        // GET / HTTP/1.1
        String firstLineStr = inputStr.split("\\n")[0];

        String[] strings = firstLineStr.split(" ");

        this.method = strings[0];
        this.url = strings[1];

        System.out.println("=====>>method:" + method);
        System.out.println("=====>>url:" + url);
    }
}
  1. 使用输出流输出静态资源文件,可根据url找出静态资源的绝对路径,然后读取并输出
  • 根据ur解析读取静态资源文件工具类
public class StaticResourceUtil {

    /**
     * 获取静态资源文件的绝对路径
     *
     * @param path
     * @return
     */
    public static String getAbsolutePath(String path) {
        String absolutePath = StaticResourceUtil.class.getResource("/").getPath();
        return absolutePath.replaceAll("\\\\", "/") + path;
    }


    /**
     * 读取静态资源文件输入流,通过输出流输出
     */
    public static void outputStaticResource(InputStream inputStream, OutputStream outputStream) throws IOException {

        int count = 0;
        while (count == 0) {
            count = inputStream.available();
        }

        int resourceSize = count;
        // 输出http请求头,然后再输出具体内容
        outputStream.write(HttpProtocolUtil.getHttpHeader200(resourceSize).getBytes());

        // 读取内容输出
        // 已经读取的内容长度
        long written = 0;
        // 计划每次缓冲的长度
        int byteSize = 1024;
        byte[] bytes = new byte[byteSize];

        while (written < resourceSize) {
            // 说明剩余未读取大小不足一个1024长度,那就按真实长度处理
            if (written + byteSize > resourceSize) {
                // 剩余的文件内容长度
                byteSize = (int) (resourceSize - written);
                bytes = new byte[byteSize];
            }

            inputStream.read(bytes);
            outputStream.write(bytes);

            outputStream.flush();
            written += byteSize;
        }
    }
}
  • response
/**
 * 封装Response对象,需要依赖于OutputStream
 * <p>
 * 该对象需要提供核心方法,输出html
 */
public class Response {

    private OutputStream outputStream;

    public Response() {
    }

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

    /**
     * 使用输出流输出指定字符串
     *
     * @param content
     * @throws IOException
     */
    public void output(String content) throws IOException {
        outputStream.write(content.getBytes());
    }

    /**
     * @param path url,随后要根据url来获取到静态资源的绝对路径,进一步根据绝对路径读取该静态资源文件,最终通过输出流输出      
     */
    public void outputHtml(String path) throws IOException {
        // 获取静态资源文件的绝对路径
        String absoluteResourcePath = StaticResourceUtil.getAbsolutePath(path);
        // 输入静态资源文件
        File file = new File(absoluteResourcePath);
        if (file.exists() && file.isFile()) {
            // 读取静态资源文件,输出静态资源
            StaticResourceUtil.outputStaticResource(new FileInputStream(file), outputStream);
        } else {
            // 输出404
            output(HttpProtocolUtil.getHttpHeader404());
        }
    }
}
  1. 启动方法修改
/**
     * 启动方法
     *
     * @throws IOException
     */
    private void start() throws IOException {

        ServerSocket serverSocket = new ServerSocket(port);
        System.out.println("=====>>>Minicat start on port:" + port);

        /**
         * 完成Minicat 2.0版本
         * 需求:封装Request和Response对象,返回html静态资源文件
         */
        while(true) {
            Socket socket = serverSocket.accept();
            InputStream inputStream = socket.getInputStream();

            // 封装Request对象和Response对象
            Request request = new Request(inputStream);
            Response response = new Response(socket.getOutputStream());

            response.outputHtml(request.getUrl());
            socket.close();
        }
    }
  1. resource目录下创建静态资源文件index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>static resouce</title>
</head>
<body>
Hello Minicat-static resouce!
</body>
</html>
  1. 验证结果
    手写简易版tomcat
    手写简易版tomcat
V3.0 可以请求动态资源(Servlet)
  1. 自定义Servlet接口及实现类
public interface Servlet {

    void init() throws Exception;

    void destory() throws Exception;

    void service(Request request,Response response) throws Exception;
}
public abstract class HttpServlet implements Servlet {

    public abstract void doGet(Request request, Response response);

    public abstract void doPost(Request request, Response response);
    
    @Override
    public void service(Request request, Response response) throws Exception {
        if ("GET".equalsIgnoreCase(request.getMethod())) {
            doGet(request, response);
        } else {
            doPost(request, response);
        }
    }
}
public class MyServlet extends HttpServlet {
    @Override
    public void doGet(Request request, Response response) {
        
        String content = "<h1>MyServlet get</h1>";
        try {
            response.output((HttpProtocolUtil.getHttpHeader200(content.getBytes().length) + content));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void doPost(Request request, Response response) {
        String content = "<h1>MyServlet post</h1>";
        try {
            response.output((HttpProtocolUtil.getHttpHeader200(content.getBytes().length) + content));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void init() throws Exception {

    }

    @Override
    public void destory() throws Exception {

    }
}
  1. 在resources目录下创建配置文件web.xml
<?xml version="1.0" encoding="UTF-8" ?>
<web-app>
    <servlet>
        <servlet-name>rpp</servlet-name>
        <servlet-class>com.rpp.MyServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>rpp</servlet-name>
        <url-pattern>/rpp</url-pattern>
    </servlet-mapping>
</web-app>
  1. 修改启动类,启动时加载配置文件web.xml,然后根据请求动态调用方法
public class Bootstrap {

    /**
     * 定义socket监听的端口号
     */
    private int port = 8080;

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }

    /**
     * 启动方法
     *
     * @throws IOException
     */
    private void start() throws Exception {

        // 加载解析相关的配置,web.xml
        loadServlet();

        ServerSocket serverSocket = new ServerSocket(port);
        System.out.println("=====>>>Minicat start on port:" + port);

        /**
         * 完成Minicat 3.0版本
         * 需求:可以请求动态资源(Servlet)
         */
        while (true) {
            Socket socket = serverSocket.accept();
            InputStream inputStream = socket.getInputStream();

            // 封装Request对象和Response对象
            Request request = new Request(inputStream);
            Response response = new Response(socket.getOutputStream());

            // 静态资源处理
            if (servletMap.get(request.getUrl()) == null) {
                response.outputHtml(request.getUrl());
            } else {
                // 动态资源servlet请求
                HttpServlet httpServlet = servletMap.get(request.getUrl());
                httpServlet.service(request, response);
            }
            socket.close();
        }
    }

    private Map<String, HttpServlet> servletMap = new HashMap<String, HttpServlet>();

    /**
     * 加载解析web.xml,初始化Servlet
     */
    private void loadServlet() {
        InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("web.xml");
        SAXReader saxReader = new SAXReader();

        try {
            Document document = saxReader.read(resourceAsStream);
            Element rootElement = document.getRootElement();

            List<Element> selectNodes = rootElement.selectNodes("//servlet");
            for (int i = 0; i < selectNodes.size(); i++) {
                Element element = selectNodes.get(i);
                // <servlet-name>rpp</servlet-name>
                Element servletnameElement = (Element) element.selectSingleNode("servlet-name");
                String servletName = servletnameElement.getStringValue();
                // <servlet-class>com.rpp.MyServlet</servlet-class>
                Element servletclassElement = (Element) element.selectSingleNode("servlet-class");
                String servletClass = servletclassElement.getStringValue();
                // 根据servlet-name的值找到url-pattern
                Element servletMapping = (Element) rootElement.selectSingleNode("/web-app/servlet-mapping[servlet-name='" + servletName + "']");
                // /rpp
                String urlPattern = servletMapping.selectSingleNode("url-pattern").getStringValue();
                servletMap.put(urlPattern, (HttpServlet) Class.forName(servletClass).newInstance());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * Minicat 的程序启动入口
     *
     * @param args
     */
    public static void main(String[] args) {
        Bootstrap bootstrap = new Bootstrap();
        try {
            // 启动Minicat
            bootstrap.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  1. 测试
    手写简易版tomcat
    手写简易版tomcat
    手写简易版tomcat
优化改造1 ,使用多线程
  • 上面的方式在多个请求连接时,如果第一个请求阻塞了,后面的请求都不会进来,因为当前处理请求就一个线程
  • 优化思路:使用多线程,单独封装一个类处理请求,继承Thread,代码如下
1. RequestProcessor
public class RequestProcessor extends Thread {

    private Socket socket;
    private Map<String,HttpServlet> servletMap;

    public RequestProcessor(Socket socket, Map<String, HttpServlet> servletMap) {
        this.socket = socket;
        this.servletMap = servletMap;
    }

    @Override
    public void run() {
        try{
            InputStream inputStream = socket.getInputStream();

            // 封装Request对象和Response对象
            Request request = new Request(inputStream);
            Response response = new Response(socket.getOutputStream());

            // 静态资源处理
            if(servletMap.get(request.getUrl()) == null) {
                response.outputHtml(request.getUrl());
            }else{
                // 动态资源servlet请求
                HttpServlet httpServlet = servletMap.get(request.getUrl());
                httpServlet.service(request,response);
            }

            socket.close();

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

    }
}
  1. 修改启动方法
/**
     * 启动方法
     *
     * @throws IOException
     */
    private void start() throws Exception {

        // 加载解析相关的配置,web.xml
        loadServlet();

        ServerSocket serverSocket = new ServerSocket(port);
        System.out.println("=====>>>Minicat start on port:" + port);

        /**
         * 多线程改造(不使用线程池)
         */
        while (true) {
            Socket socket = serverSocket.accept();
            RequestProcessor requestProcessor = new RequestProcessor(socket, servletMap);
            requestProcessor.start();
        }
    }
  1. 在MyServlet中的get方法中增加线程休眠时间,用于验证
public void doGet(Request request, Response response) {

        try {
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        String content = "<h1>MyServlet get</h1>";
        try {
            response.output((HttpProtocolUtil.getHttpHeader200(content.getBytes().length) + content));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
  1. 验证结果

请求http://localhost:8080/rpp,线程一直阻塞的同时,然后请求http://localhost:8080/index.html,可以正常返回结果

优化改造2 ,使用线程池
public class Bootstrap {

    /**
     * 定义socket监听的端口号
     */
    private int port = 8080;

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }
    
	/**
     * 定义一个线程池
     */
    private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            10,
            50,
            100L,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(50),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy()
    );

    /**
     * 启动方法
     *
     * @throws IOException
     */
    private void start() throws Exception {

        // 加载解析相关的配置,web.xml
        loadServlet();

        ServerSocket serverSocket = new ServerSocket(port);
        System.out.println("=====>>>Minicat start on port:" + port);

        /**
         * 多线程改造(使用线程池)
         */
        while (true) {
            Socket socket = serverSocket.accept();
            RequestProcessor requestProcessor = new RequestProcessor(socket, servletMap);
            threadPoolExecutor.execute(requestProcessor);
        }
    }

    private Map<String, HttpServlet> servletMap = new HashMap<String, HttpServlet>();

    /**
     * 加载解析web.xml,初始化Servlet
     */
    private void loadServlet() {
        InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("web.xml");
        SAXReader saxReader = new SAXReader();

        try {
            Document document = saxReader.read(resourceAsStream);
            Element rootElement = document.getRootElement();

            List<Element> selectNodes = rootElement.selectNodes("//servlet");
            for (int i = 0; i < selectNodes.size(); i++) {
                Element element = selectNodes.get(i);
                // <servlet-name>rpp</servlet-name>
                Element servletnameElement = (Element) element.selectSingleNode("servlet-name");
                String servletName = servletnameElement.getStringValue();
                // <servlet-class>com.rpp.MyServlet</servlet-class>
                Element servletclassElement = (Element) element.selectSingleNode("servlet-class");
                String servletClass = servletclassElement.getStringValue();
                // 根据servlet-name的值找到url-pattern
                Element servletMapping = (Element) rootElement.selectSingleNode("/web-app/servlet-mapping[servlet-name='" + servletName + "']");
                // /rpp
                String urlPattern = servletMapping.selectSingleNode("url-pattern").getStringValue();
                servletMap.put(urlPattern, (HttpServlet) Class.forName(servletClass).newInstance());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * Minicat 的程序启动入口
     *
     * @param args
     */
    public static void main(String[] args) {
        Bootstrap bootstrap = new Bootstrap();
        try {
            // 启动Minicat
            bootstrap.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
相关标签: tomcat