手动实现 Tomcat 底层机制+自己设计 Servlet

  |   0 评论   |   0 浏览

手动实现 Tomcat 底层机制+自己设计 Servlet

1.Tomcat 整体架构分析

●说明: Tomcat 有三种运行模式(BIO, NIO, APR), 因为老师核心讲解的是 Tomcat 如何接收客户端请求,解析请求, 调用 Servlet , 并返回结果的机制流程, 采用 BIO 线程模型来模拟.[绘图]

image-20220308210120313

2.实现任务阶段 1- 编写自己 Tomcat, 能给浏览器返回 hi llp,你好tomcat

1.基于 socket 开发服务端-流程

image-20220308220845668

2.需求分析/图解

需求分析如图, 浏览器请求 http://localhost:8080/??, 服务端返回 hi llp,你好tomcat

image-20220308221024682

3.分析+代码实现

客户端(浏览器)请求tomcat,建立socket链接;服务端处理请求并响应给服务端。

image-20220308221052813

tcp协议的数据没有封装成http协议的格式就返回给浏览器会出现解析问题,火狐支持直接解析

public class TomcatV1 {
    public static void main(String[] args) throws IOException {

        //1. 创建ServerSocket, 在 8080端口监听
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("=======mytomcat在8080端口监听======");
        while (!serverSocket.isClosed()) {

            //等待浏览器/客户端的连接
            //如果有连接来,就创建一个socket
            //这个socket就是服务端和浏览器端的连接/通道
            Socket socket = serverSocket.accept();

            //先接收浏览器发送的数据
            //inputStream 是字节流=> BufferedReader(字符流)
            InputStream inputStream = socket.getInputStream();
            BufferedReader bufferedReader =
                    new BufferedReader(new InputStreamReader(inputStream, "utf-8"));

            String mes = null;
            System.out.println("=======接收到浏览器发送的数据=======");
            //循环的读取
            while ((mes = bufferedReader.readLine()) != null) {
                //判断mes的长度是否为0,不做判断当读取到的数据长度为0时无法退出循环
                if (mes.length() == 0) {
                    break;//退出while
                }
                System.out.println(mes);
            }

            //我们的tomcat会送-http响应方式
            OutputStream outputStream = socket.getOutputStream();
            //构建一个http响应的头
            //\r\n 表示换行
            //http响应体,需要前面有两个换行 \r\n\r\n
            String respHeader = "HTTP/1.1 200 OK\r\n" +
                    "Content-Type: text/html;charset=utf-8\r\n\r\n";
            String resp = respHeader + "hi, hspedu 韩顺平教育";

            System.out.println("========我们的tomcat 给浏览器会送的数据======");
            System.out.println(resp);

            outputStream.write(resp.getBytes());//将resp字符串以byte[] 方式返回

            outputStream.flush();
            outputStream.close();
            //关闭外层流即可关闭底层流
            bufferedReader.close();
            socket.close();
        }
    }
}

3.实现任务阶段 2- 使用 BIO 线程模型,支持多线程

1.BIO 线程模型介绍

image-20220309223210400

2.需求分析/图解

需求分析如图, 浏览器请求 http://localhost:8080, 服务端返回 hi , 孙悟空~, 后台tomcat 使用 BIO 线程模型,支持多线程=> 对前面的开发模式进行改造

image-20220309223424040

3.分析+代码实现

TomcatV2

public class TomcatV2 {
    public static void main(String[] args) throws IOException {
        //在8080端口监听
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("=======tomcatV2 在8080监听=======");
        //只要 serverSocket没有关闭,就一直等待浏览器/客户端的连接
        while (!serverSocket.isClosed()) {
            //1. 接收到浏览器的连接后,如果成功,就会得到socket
            //2. 这个socket 就是 服务器和 浏览器的数据通道
            Socket socket = serverSocket.accept();
            //3. 创建一个线程对象,并且把socket给该线程
            //  这个是java线程基础
            HspRequestHandler hspRequestHandler =
                    new HspRequestHandler(socket);
            new Thread(hspRequestHandler).start();

        }
    }
}

RequestHandler线程对象

public class RequestHandler implements Runnable{
    private Socket socket;
    public RequestHandler(Socket socket){
        this.socket = socket;
    }

    @Override
    public void run() {
        InputStream inputStream = null;
        try {
            inputStream = socket.getInputStream();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            String readLine = null;
            System.out.println("请求部分: "+Thread.currentThread().getName()+"-"+Thread.currentThread().hashCode());
            while ((readLine = bufferedReader.readLine())!=null){
                if(readLine.length() == 0){
                    break;
                }
                System.out.println(readLine);
            }

            OutputStream outputStream = socket.getOutputStream();
            //这里两个换行,响应头和响应体之间必须有换一行否则响应体内容无法正常解析
            String respHeader = "HTTP/1.1 200 OK\r\n"+"Content-Type: text/html;charset=utf-8\r\n\r\n";
            String respBody = "hi ,孙悟空~";
            String resp = respHeader+respBody;
            System.out.println("响应部分=========");
            System.out.println(resp);
            outputStream.write(resp.getBytes());
            outputStream.flush();
            outputStream.close();
            bufferedReader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(socket!=null){
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

image-20220309223516447

4.实现任务阶段 3- 处理 Servlet

1.Servlet 生命周期-回顾

image-20220309224041101

2.需求分析/图解

●需求分析如图, 浏览器请求 http://localhost:8080/hspCalServlet, 提交数据,完成计算任务,如果 servlet 不存在,返回 404

image-20220310225343036

image-20220310225502410

image-20220310225511883

3.分析+代码实现

●分析示意图

image-20220310225601475

image-20220310225611745

开发流程-一图胜千言

image-20220312150222005

image-20220312151023845

1.Tomcat启动类

public class Tomcat3 {

    /**
     * 定义一个map用于存放web.xml中读取到的Servlet实例;
     * key为ServletName
     * value为HttpServlet实例
     */
    public static final ConcurrentMap<String, LlpHttpServlet> servletMap = new ConcurrentHashMap<>();

    /**
     * 用于存放Servlet-Mapping部分内容
     * key为:ServletPattern
     * value为ServletName
     * 浏览器访问时,可通过uri即map的key直接获取到对应的vlue-ServletName
     * 从而从servletMap中获取对应的HttpServlet实例,如果Servlet实例不存在则返回404
     */
    public static final ConcurrentMap<String,String> servletMappingMap = new ConcurrentHashMap<>();


    public static void main(String[] args) throws Exception {
        init();
        run();
    }

    public static void init() throws Exception {
        SAXReader saxReader = new SAXReader();
        //llp-tomcat\target\classes\web.xml 这里我将web.xml直接拷贝到了\target\classes\目录下
        String path = Tomcat3.class.getResource("/").getPath();
        Document document = saxReader.read(new File(path+"web.xml"));
        //获取根本标签
        Element rootElement = document.getRootElement();
        List<Element> elements = rootElement.elements();
        for (Element element : elements) {
            /**
             *     <servlet>
             *         <servlet-name>LlpCalServlet</servlet-name>
             *         <servlet-class>com.llp.tomcat.servlet.LlpCalServlet</servlet-class>
             *     </servlet>
             *     <servlet-mapping>
             *         <servlet-name>LlpCalServlet</servlet-name>
             *         <url-pattern>/llpCalServlet</url-pattern>
             *     </servlet-mapping>
             */
            String name = element.getName();
            if("servlet".equals(name)){
                String classFullPath = element.element("servlet-class").getText().trim();
                Class<?> aClass = Class.forName(classFullPath);
                 LlpHttpServlet llpHttpServlet = (LlpHttpServlet) aClass.newInstance();
                servletMap.put(element.element("servlet-name").getText().trim(),llpHttpServlet);
            }
            if("servlet-mapping".equals(name)){
                String servletName = element.element("servlet-name").getText().trim();
                String urlPattern = element.element("url-pattern").getText().trim();
                servletMappingMap.put(urlPattern,servletName);
            }
        }
        System.out.println(servletMap);
        System.out.println(servletMappingMap);
    }

    public static  void run() throws Exception {
        ServerSocket serverSocket = new ServerSocket(8080);
        while (!serverSocket.isClosed()) {
            Socket socket = serverSocket.accept();
            new Thread(new RequestHandler(socket)).start();
        }
        serverSocket.close();
    }
}

2.处理请求线程类RequestHandler

public class RequestHandler implements Runnable {
    private Socket socket;

    public RequestHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {

            LlpRequest llpRequest = new LlpRequest(socket.getInputStream());
            LlpResponse llpResponse = new LlpResponse(socket.getOutputStream());
            // LlpCalServlet llpCalServlet = new LlpCalServlet();
            // llpCalServlet.doGet(llpRequest,llpResponse);

            //通过反射动态获取LlpHttpServlet对象执行Service方法,这里利用java多态的特性进行了值传递
            //eg: uri = /llpCalServlet
            String uri = llpRequest.getUri();
            //判断请求的是不是静态资源,以.html为例
            if(WebUtil.isHtml(uri)){
                String url = uri.substring(1);
                String content = WebUtil.readHtml(url);
                System.out.println("content: "+content);
                OutputStream outputStream = llpResponse.getOutputStream();
                outputStream.write((LlpResponse.responseHeader+content).getBytes());
                outputStream.flush();
                return;
            }
            String servletName = Tomcat3.servletMappingMap.get(uri);
            if (servletName == null) {
                servletName = "";
            }
            LlpHttpServlet llpHttpServlet = Tomcat3.servletMap.get(servletName);
            if (llpHttpServlet != null) {
                llpHttpServlet.service(llpRequest, llpResponse);
            } else {
                OutputStream outputStream = llpResponse.getOutputStream();
                outputStream.write((LlpResponse.responseHeader +  "404 Not Found").getBytes());
                outputStream.flush();
            }

            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

3.自定义Servlet

1.自定义Servlet接口-LlpServlet
public interface LlpServlet {
    void init();

    void service(LlpRequest request, LlpResponse response);

    void destroy();

}
2.自定义Servlet抽象类-LlpHttpServlet
public abstract class LlpHttpServlet implements LlpServlet{

    @Override
    public void service(LlpRequest request, LlpResponse response) {
        if("GET".equalsIgnoreCase(request.getMethod())){
            doGet(request,response);
        }else if("POST".equalsIgnoreCase(request.getMethod())){
            doPost(request,response);
        }
        
    }



    protected abstract void doPost(LlpRequest request, LlpResponse response);

    protected abstract void doGet(LlpRequest request, LlpResponse response);

    @Override
    public void init() {

    }

    @Override
    public void destroy() {

    }

}
3.自定义HttpServletRequest、HttpServletResponse
public class LlpRequest {


    private String method; //GET

    private String uri; // /cal

    private Map<String, String> paramterMap = new HashMap<>(); //num1=12&num2=12

    private InputStream inputStream = null;

    //GET /cal?num1=12&num2=21 HTTP/1.1
    public LlpRequest(InputStream inputStream) {
        this.inputStream = inputStream;
        encapHttpRequest();
    }

    public String getParameter(String key){
        return paramterMap.get(key);
    }


    /**
     * 将http请求相关的数据进行封装
     */
    public void encapHttpRequest(){
        try {
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            //请求行
            //GET /cal?num1=12&num2=21 HTTP/1.1
            String requestLine = bufferedReader.readLine();
            String[] requestLineArr = requestLine.split(" ");
            method = requestLineArr[0];
            System.out.println("method: "+method);
            /**
             * /cal?num1=12&num2=21
             * 1.判断是否含有参乎上
             * 2.无参则取出uri
             * 3.有参则分成两段: /cal  num1=12&num2=21
             */
            String url = requestLineArr[1];
            if(url.indexOf("?")==-1){
                uri = url;
            }else {
                uri = url.substring(0, url.indexOf("?"));
                String paramters = url.substring(url.indexOf("?") + 1);
                //防止用户输入 /cal?
                if(paramters!=null && !"".equals(paramters)){
                    String[] split = paramters.split("&");
                    if(split!=null && split.length!=0){
                        for (String s : split) {
                            // num1=12  num2=21
                            String[] split1 = s.split("=");
                            paramterMap.put(split1[0],split1[1]);
                        }
                    }
                }
                System.out.println("uri: "+uri);
                System.out.println("paramterMap: "+paramterMap);
            }

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

    public String getMethod() {
        return method;
    }

    public String getUri() {
        return uri;
    }
}
public class LlpResponse {

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

    private OutputStream outputStream = null;

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

    public OutputStream getOutputStream() {
        return outputStream;
    }

}
4.自定义Servlet业务类
public class LlpCalServlet  extends LlpHttpServlet{

    @Override
    public void doPost(LlpRequest request, LlpResponse response) {
    }

    @Override
    public void doGet(LlpRequest request, LlpResponse response) {
        try {
            Integer num1 = WebUtil.parseInteger(request.getParameter("num1"),0);
            Integer num2 = WebUtil.parseInteger(request.getParameter("num2"),0);

            OutputStream outputStream = response.getOutputStream();
            //这里两个换行,响应头和响应体之间必须有换一行否则响应体内容无法正常解析
            String respHeader = LlpResponse.responseHeader;
            String resp = respHeader +"<h1>"+num1+"+"+num2+"="+(num1+num2)+" llp Tomcat</h1>";
            outputStream.write(resp.getBytes());
            outputStream.flush();
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

}

4.工具类

public class WebUtil {
    public static  Integer parseInteger(String value,Integer deafultValue){
        int num;
        try {
            num  = Integer.parseInt(value);
        } catch (NumberFormatException e) {
            System.out.println("value不能转换为数字类型");
            return deafultValue;
        }
        return num;
    }

    public static boolean isHtml(String uri){
        return uri.endsWith(".html");
    }

    public static String readHtml(String url){
        BufferedReader bufferedReader = null;
        StringBuilder builder = new StringBuilder();
        try {
            String path = WebUtil.class.getResource("/").getPath()+url;
            bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(path)));
            String readLine = null;
            while ((readLine= bufferedReader.readLine())!=null){
                builder.append(readLine);
            }
        } catch (Exception e) {
            return  "404 Not Found";
        }finally {
            if(bufferedReader!=null){
                try {
                    bufferedReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return builder.toString();
    }

}

5.测试效果

image-20220312151451371

image-20220312151544631


标题:手动实现 Tomcat 底层机制+自己设计 Servlet
作者:llp
地址:https://llinp.cn/articles/2022/03/12/1647069437465.html