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 接口实现不会受到影响。

  • 相关阅读:
    Effective Java 第三版——72. 赞成使用标准异常
    Effective Java 第三版——71. 避免不必要地使用检查异常
    Effective Java 第三版——70. 对可恢复条件使用检查异常,对编程错误使用运行时异常
    Effective Java 第三版——69. 仅在发生异常的条件下使用异常
    Effective Java 第三版——68. 遵守普遍接受的命名约定
    Effective Java 第三版——67. 明智谨慎地进行优化
    Effective Java 第三版——66. 明智谨慎地使用本地方法
    Effective Java 第三版——65. 接口优于反射
    Effective Java 第三版——64. 通过对象的接口引用对象
    Effective Java 第三版——63. 注意字符串连接的性能
  • 原文地址:https://www.cnblogs.com/cunchen/p/9464122.html
Copyright © 2011-2022 走看看