zoukankan      html  css  js  c++  java
  • 【深入剖析Tomcat笔记】第三篇 基本容器模型

    简述

    简单回顾一下上文,上文中我们通过ServerSocket和Socket类实现基本的Socket连接。此篇我们将DemoServer进行重构。上篇最后,我们发现了一些问题,但这些问题无法进行整体性解决,因此我们将项目进行更为合理的拆分成独立的模块。
    结构图
    重构后结构图如上。重构之后,主要分为Connecor、Processor、Response、Request几个模块

    测试源码方法请参考【深入剖析Tomcat笔记】第二篇 ServerSocket模型
    此篇Demo中Servlet测试需要自己配置Servlet。

    本期源代码:
    practice-tomcat


    Connector模块

    相比起上一篇中的简单Socket连接,容器所遇到的启动场景会越来越复杂,针对于不同的使用场景,Socket在接入方案上就需要提供更多的选择。因此,有别于之前的简单创建,我们将ServerSocket实例化场景单独抽象为一个模块Connector。
    对于ServerSocket初始化参数(*int port , int backlog , InetAddress bindAddr)
    在实际使用中,我们通常会基于文件配置的形式提供状态无关的配置。Connector实例拆分,可以模块化提供基于文件的配置,如maxIdle,maxKeepAliveRequests等属性的配置,在实际Tomcat配置中< connector >标签中承担的是Connector的配置工作。
    对于Connector模块进行梳理,在Connector所提供的的核心功能包括:
    1. 创建ServerSocket
    2. 请求接入
    3. 请求处理

    将请求接入和请求处理 模块分离,这样做的优势在于

    1. 模块化,代码条理更清晰
    2. 为多线程处理提供了条件
    3. 扩展性增强

    代码如下

        /**
         * 连接器连接操作
         * 创建ServerSocket
         * 接入请求
         * 调用Prossor进行处理
         */
        void connect() {
            ServerSocket serverSocket = null;
            int port = 8080;
            //1.创建ServerSocket
            try {
                serverSocket = new ServerSocket(port, 1, InetAddress.getByName("localhost"));
            } catch (IOException e) {
                e.printStackTrace();
                System.exit(1);
            }
    
            //若非终止参数,则循环接入请求
            while (!stopped) {
                Socket socket = null;
                try {
                    //2.socket请求接入
                    socket = serverSocket.accept();
    
                    //判断是否由于端口映射造成的二次访问
                    if(socket.getLocalPort() != port)
                        continue;
    
                } catch (IOException e) {
                    e.printStackTrace();
                }
    
                //3.Socket请求转交Processor处理
                HttpProcessor processor = new HttpProcessor ();
                processor.process(socket);
            }
        }

    Processor模块

    Processor承担了主要的解析逻辑,在实际使用场景中,解析的场景更加多样化,例如对HTTP的解析和对于SMTP的解析是不同的场景。实际上Processor是对于四层网络模型中应用层的解析场景,例如实际中Tomcat中对于Http的解析提供的Processor是HttpProcessor。
    Processor模块的主要职责:
    1. 请求头解析
    2. Request构造
    3. Response构造
    4. 请求解析

    代码如下

        /**
         * 解析器解析
         * @param socket Socket请求
         */
        public void process(Socket socket) {
            SocketInputStream input = null;
            OutputStream output = null;
            try {
                   //Request构造
                    input = socket.getInputStream();
                    if(input == null)
                        continue;
    
                    Request request = new Request(input);
                    request.parse();
    
                    if(request.getUri() == null)
                        continue;
    
                    //Request构造
                    output = socket.getOutputStream();
                    Response response = new Response(output);
                    response.setRequest(request);
    
                    //请求解析
                    if(request.getUri().startsWith("/servlet/")) {
                        ServletProcessor processor = new ServletProcessor();
                        processor.process(request, response);
                    } else {
                        StaticResourceProcessor processor = new StaticResourceProcessor();
                        processor.process(request, response);
                    }
    
                    response.sendStaticResource();
                    socket.close();
    
                    shutdown = request.getUri().equals(SHUTDOWN_COMMEND);
                } catch (IOException e) {
                    e.printStackTrace();
                    System.exit(1);
                }
        }

    对应Processor有两个实现ServletProcessor和StaticResourceProcessor,在解析请求行后,对于请求类型进行判断,决定调用哪种解析器。实例代码中,为了简化配置,使用了”/servlet/”进行判断,实际使用中,我们通过web.xml进行Servlet配置。这里不难将xml映射关系转化为ServletProcessor解析判断的请求行。

    StaticResourceProcessor是对静态资源的解析,例如:index.html,icon.jpg等文件。
    在此处实际代码为,具体实现见Response。

         public void process(Request request, Response response) {
    
            response.sendStaticResource();
        }

    ServletProcessor是对Servlet的解析,由于Servlet解析是动态解析,所以此处需要加载运行在平台之上的Servlet服务。此处需要用到java.net.URLClassLoader对Servlet服务进行加载。

        /**
         * 动态Servlet处理
         * @param request 请求 处理
         * @param response 结果处理
         */
        public void process(Request request, Response response) {
            String uri = request.getUri();
            String servletName = uri.substring(uri.lastIndexOf("/") + 1);
            URLClassLoader loader = null;
    
    
            try {
                URL[] urls = new URL[1];
                File classPath = new File(Constants.WEB_ROOT);
    
                /**
                 * 因为URL类还有一个构造方法URL(String protocol, String host, String file)
                 * 编译器可能造成无法识别
                 * 这里还可以写作
                 * urls[0] = new URL(null, repository, (URLStreamHandler)null);
                 */
                URLStreamHandler streamHandler = null;
                String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString();
    
                urls[0] = new URL(null, repository, streamHandler);
                loader = new URLClassLoader(urls);
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            //ClassLoader加载Servlet服务
            Class myClass = null;
            try {
                myClass = loader.loadClass(servletName);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
    
            //RequestFacade和ResponseFacade包装类
            //Servlet实例化,载入service()
            Servlet servlet = null;
            RequestFacade requestFacade = new RequestFacade(request);
            ResponseFacade responseFacade = new ResponseFacade(response);
            try {
                servlet = (Servlet) myClass.newInstance();
                servlet.service((ServletRequest) requestFacade, (ServletResponse) responseFacade);
            } catch (ServletException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }

    Request模块

    Request是对外部请求的封装,在Processor中Request用到的主要包括对请求行解析和请求头解析,构造方法如下

    public Request(InputStream input) {
            this.inputStream = input;
        }

    在第一章中我们了解到,HTTP协议请求行格式为 “GET /index.html HTTP/1.1”,取第一个空格前字符串记录为Method = “GET”,取第二个空格前第一个空格后字符串记录为uri = “/index.html”,取第二个空格后字符串为Protocol = “HTTP/1.1”。

    public void parse() {
            StringBuffer requset = new StringBuffer(BUFFER_SIZE);
            int i;
            byte[] buffer = new byte[BUFFER_SIZE];
            try {
                i = inputStream.read(buffer);
            } catch (IOException e) {
                e.printStackTrace();
                i = -1;
            }
            for (int j = 0; j < i; j++) {
                requset.append((char) buffer[j]);
            }
            System.out.println(requset.toString());
            uri = parseUri(requset.toString());
        }
    
        //解析请求行
        String parseUri(String requestString) {
            int index1, index2;
            index1 = requestString.indexOf(' ');
            if(index1 != -1) {
                index2 = requestString.indexOf(' ', index1 + 1);
                return index2 > index1 ? requestString.substring(index1 + 1, index2) : null;
            }
            return null;
        }

    Response模块

    Response在Processor中用到的方法主要是sendStaticResource(),将静态资源进行转发。

    public void sendStaticResource() {
            byte[] bytes = new byte[BUFFER_SIZE];
            FileInputStream fis = null;
            if(request == null)
                return;
            File file = new File(Constants.WEB_ROOT, request.getUri());
            try {
                if(file.isFile()) {
                    fis = new FileInputStream(file);
                    int ch = fis.read(bytes, 0, BUFFER_SIZE);
                    while (ch != -1) {
                        outputStream.write(bytes, 0, ch);
                        ch = fis.read(bytes, 0, BUFFER_SIZE);
                    }
                } else {
                    String errorMessage = "HTTP/1.1 404 File Not Found
    " +
                            "Content-Type: text/html
    " +
                            "Content-Length: 23
    
    " +
                            "
    " +
                            "<h1>File Not Found</h1>";
                    outputStream.write(errorMessage.getBytes());
                }
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    if( fis != null)
                        fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return;
        }

    Facade类

    包装类主要用于解决访问性问题,一条很通用的设计原则是

    第13条使类和成员的可访问性最小化
    要区别设计良好的模块与设计不好的模块,最重要的因素在于,这个模块对于外部的其他模块而言,是否隐藏其内部数据和其他实现细节。设计良好的模块会隐藏所有的设计细节,把它的API与它的实现清晰地隔离开来。然后,模块之间只通过他们的API进行通信,一个模块不需要知道其他模块的内部工作情况。这个概念被称为信息隐藏或封装,是软件设计的基本原则之一。
    –《Effective Java》

    信息隐藏可以解除模块间的耦合关系,使这些模块可以独一的开发、测试和优化。并使模块更为清晰的结构,易于理解,易于性能评测,易于整体性优化,易于重构。
    Java对于此方面的主要实现是通过多态实现 访问控制 ,访问控制最基本的使用是访问控制修饰符实现,但仅仅使用 访问权限修饰符 在面向接口设计方面,也会产生一些问题。

    从源码来看

            ...
            //RequestFacade和ResponseFacade包装类
            //Servlet实例化,载入service()
            Servlet servlet = null;
            RequestFacade requestFacade = new RequestFacade(request);
            ResponseFacade responseFacade = new ResponseFacade(response);
            try {
                servlet = (Servlet) myClass.newInstance();
                servlet.service((ServletRequest) requestFacade, (ServletResponse) responseFacade);
           ...

    因为使用面向接口编程,这里子类都被转化为ServletRequest,面向接口是一种普遍方案,但在这里会造成一个问题

        //Client代码
        protected void doPost(ServletRequest req, ServletResponse resp) throws ServletException, IOException {
            req = (Request)req;
            //回顾parse()方法,其访问性控制是public,这一点是无可厚非的
            //public的可访问性是针对Tomcat项目内部,并不针对项目外部
            //但Client端完全可以通过类型下转,转换为Request调用parse方法
            //这样的使用是危险的
            req = req.parse();
            ...
    

    在这里Tomcat使用了Facade设计模式(外观设计模式)

    Facade设计模式
    为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更容易使用。
    –《设计模式》

    Facade优势有:

    - Client屏蔽子系统组件,减少客户处理对象的数目并使得子系统使用起来更加方便
    - 它实现了子系统与客户之间的松耦合关系,而子系统内部的功能组件往往是斤耦合的。减少这种耦合关系对于Client的影响
    - 不限制子系统类,可以易用性和通用性之间选择

    实际的UML关系如下
    Response&Request Facade UML Structrue

    public class RequestFacade implements ServletRequest {
        private ServletRequest request = null;
    
        //构造方法
        public RequestFacade(Request request) {
            this.request = request;
        }
    
        //实际使用调用Request对应方法实现,其他方法类似
        public Object getAttribute(String attribute) {
            return request.getAttribute(attribute);
        }
        ...

    采用Facade模式后,Client端 再执行 req = (Request)req 由于Request类和RequestFacade没有继承关系,所以类型转换会失败,且对外提供的ServletRequest 接口实现不会受到影响。

  • 相关阅读:
    hash_map和map的区别
    STL中map与hash_map容器的选择收藏
    ServletContextListener和ContextLoaderListener的区别
    解决Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.7.0:compile
    详解Tomcat线程池原理及参数释义
    Tomcat使用线程池配置高并发连接
    详解 Tomcat 的连接数与线程池
    ServletContextListener接口用法
    Spring Quartz定时任务如何获得ServletContext对象?
    如何在spring quartz类中拿到ServletContext
  • 原文地址:https://www.cnblogs.com/cunchen/p/9464119.html
Copyright © 2011-2022 走看看