• 手写一个简化版Tomcat


    一、Tomcat工作原理

          我们启动Tomcat时双击的startup.bat文件的主要作用是找到catalina.bat,并且把参数传递给它,而catalina.bat中有这样一段话:

    Bootstrap.class是整个Tomcat 的入口,我们在Tomcat源码里找到这个类,其中就有我们经常使用的main方法:

     

       这个类有两个作用 :1.初始化一个守护进程变量、加载类和相应参数。2.解析命令,并执行。

           源码不过多赘述,我们在这里只需要把握整体架构,有兴趣的同学可以自己研究下源码。Tomcat的server.xml配置文件中可以对应构架图中位置,多层的表示可以配置多个:

     

    即一个由 Server->Service->Engine->Host->Context 组成的结构,从里层向外层分别是:

    • Server:服务器Tomcat的顶级元素,它包含了所有东西。

    • Service:一组 Engine(引擎) 的集合,包括线程池 Executor 和连接器 Connector 的定义。

    • Engine(引擎):一个 Engine代表一个完整的 Servlet 引擎,它接收来自Connector的请求,并决定传给哪个Host来处理。

    • Container(容器):Host、Context、Engine和Wraper都继承自Container接口,它们都是容器。

    • Connector(连接器):将Service和Container连接起来,注册到一个Service,把来自客户端的请求转发到Container。

    • Host:即虚拟主机,所谓的”一个虚拟主机”可简单理解为”一个网站”。

    • Context(上下文 ): 即 Web 应用程序一个 Context 即对于一个 Web 应用程序。Context容器直接管理Servlet的运行,Servlet会被其给包装成一个StandardWrapper类去运行。Wrapper负责管理一个Servlet的装载、初始化、执行以及资源回收,它是最底层容器。

    比如现在有以下网址,根据“/”切割的链接就会定位到具体的处理逻辑上,且每个容器都有过滤功能。

     

    二、梳理自己的Tomcat实现思路

    一个请求要请求服务器端的一个文件,服务端根据路径查找该文件,如果有则读取给文件并把文件内容响应回客户端。    

    实现以上效果整体思路如下:

          1.ServerSocket占用8080端口,用while(true)循环等待用户发请求。

          2.拿到浏览器的请求,解析并返回URL地址,用I/O输入流读取本地磁盘上相应文件。

          3.读取文件,如果文件不存在则构建响应报文头、HTML正文内容,如果存在则把文件写到浏览器端。

    三、实现自己的Tomcat

    工程文件结构:

     1.HttpServer核心处理类,用于接受用户请求,传递HTTP请求头信息,关闭容器:

    package com.jp.jpHttp;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.net.InetAddress;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    /**
     * HttpServer核心处理类,用于接受用户请求,传递HTTP请求头信息,关闭容器
     * 
     */
    public class HttpServer {
        // 用于判断是否需要关闭容器
        private boolean shutdown = false;
    
        public void acceptWait() {
            ServerSocket serverSocket = null;
            try {
                /**
                 * serverSocket的三个参数
                 * TCP端口号:0-65535,端口号 0 在所有空闲端口上创建套接字
                 * 最大连接数:传入连接指示(对连接的请求)的最大队列长度被设置为 backlog 参数。如果队列满时收到连接指示,则拒绝该连接。
                 * ip地址: 参数可以在 ServerSocket 的多宿主主机 (multi-homed host) 上使用,ServerSocket 仅接受对其地址之一的连接请求。如果 bindAddr 为 null,则默认接受任何/所有本地地址上的连接
                 */
                serverSocket = new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));
            } catch (IOException e) {
                e.printStackTrace();
                System.exit(1);
            }
            // 等待用户发请求
            while (!shutdown) {
                try {
                    Socket socket = serverSocket.accept();
                    InputStream is = socket.getInputStream();
                    OutputStream os = socket.getOutputStream();
                    
                    // 接受请求参数
                    Request request = new Request(is);
                    request.parse();
                    
                    // 创建用于返回浏览器的对象
                    Response response = new Response(os);
                    response.setRequest(request);
                    response.sendStaticResource();
                    
                    // 关闭一次请求的socket,因为http请求就是采用短连接的方式
                    socket.close();
                    System.out.println("服务端的serverSocket关闭");
                    // 如果请求地址是/shutdown 则关闭容器
                    if (null != request) {
                        shutdown = request.getUrL().equals("/shutdown");
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    continue;
                }
            }
        }
    
        public static void main(String[] args) {
            HttpServer server = new HttpServer();
            server.acceptWait();
        }
    }

    2.创建Request类,获取HTTP的请求头所有信息并截取URL地址返回:

     1 package com.jp.jpHttp;
     2 
     3 import java.io.IOException;
     4 import java.io.InputStream;
     5 
     6 /**
     7  * 创建Request类,获取HTTP的请求头所有信息并截取URL地址返回
     8  *
     9  */
    10 public class Request {
    11     private InputStream is;
    12     private String url;
    13 
    14     public Request(InputStream input) {
    15         this.is = input;
    16     }
    17 
    18     public void parse() {
    19         // 从socket中读取一个2048长度字符
    20         StringBuffer request = new StringBuffer(Response.BUFFER_SIZE);
    21         int i;
    22         byte[] buffer = new byte[Response.BUFFER_SIZE];
    23         try {
    24             i = is.read(buffer);//从输入流is读取一定数量的字节,并存储到buffer字节数组中
    25         } catch (IOException e) {
    26             e.printStackTrace();
    27             i = -1;
    28         }
    29         for (int j = 0; j < i; j++) {
    30             request.append((char) buffer[j]);//把buffer字符数组中的字节拼成字符串 request
    31         }
    32         // 打印读取的socket中的内容
    33         System.out.println("打印socket的输入流中的内容");
    34         System.out.print(request.toString());//打印字符串request,就是socket里的输入流,即来自客户端的内容
    35         url = parseUrL(request.toString()); //从字符串request中抽取出请求路径
    36     }
    37 
    38     //从字符串request中抽取出请求路径的函数
    39     private String parseUrL(String requestString) {
    40         int index1, index2;
    41         index1 = requestString.indexOf(' ');// 看socket获取请求头是否有值
    42         if (index1 != -1) {
    43             index2 = requestString.indexOf(' ', index1 + 1);
    44             if (index2 > index1)
    45                 System.out.println();
    46                 System.out.println("获取到请求文件的路径(url)" + requestString.substring(index1 + 1, index2));
    47                 return requestString.substring(index1 + 1, index2);
    48         }
    49         return null;
    50     }
    51 
    52     public String getUrL() {
    53         return url;
    54     }
    55 
    56 }

    3.创建Response类,响应请求读取文件并写回到浏览器

     1 package com.jp.jpHttp;
     2 
     3 import java.io.File;
     4 import java.io.FileInputStream;
     5 import java.io.IOException;
     6 import java.io.OutputStream;
     7 
     8 /**
     9  * 创建Response类,响应请求读取文件并写回到浏览器
    10  */
    11 public class Response {
    12     public static final int BUFFER_SIZE = 2048;
    13     // 浏览器访问D盘的文件
    14     private static final String WEB_ROOT = "D:";
    15     private Request request;
    16     private OutputStream output;
    17 
    18     public Response(OutputStream output) {
    19         this.output = output;
    20     }
    21 
    22     public void setRequest(Request request) {
    23         this.request = request;
    24     }
    25 
    26     public void sendStaticResource() throws IOException {
    27         byte[] bytes = new byte[BUFFER_SIZE];
    28         FileInputStream fis = null;
    29         try {
    30             // 拼接本地目录和浏览器端口号后面的目录
    31             File file = new File(WEB_ROOT, request.getUrL());
    32             System.out.println("请求路径拼接为服务器内的文件路径:"+ file.getAbsolutePath());
    33             //System.out.println("测试应用程序是否可以读取此抽象路径名表示的文件:" + file.canRead());
    34             // 如果文件存在,且不是个目录
    35             if (file.exists() && !file.isDirectory()) {
    36                 fis = new FileInputStream(file);
    37                 int ch = fis.read(bytes, 0, BUFFER_SIZE);
    38                 while (ch != -1) {
    39                     output.write(bytes, 0, ch);
    40                     ch = fis.read(bytes, 0, BUFFER_SIZE);
    41                 }
    42                 System.out.println("请求的文件存在,正在把文件作为响应写进socket的输出流");
    43             } else {
    44                 // 文件不存在,返回给浏览器响应提示,这里可以拼接HTML任何元素
    45                 String retMessage = "<h1>" + file.getName() + " file or directory not exists</h1>";
    46                 String returnMessage = "HTTP/1.1 404 File Not Found
    " + "Content-Type: text/html
    "
    47                         + "Content-Length: " + retMessage.length() + "
    " + "
    " + retMessage;
    48                 output.write(returnMessage.getBytes());
    49                 System.out.println("请求的文件不存在,返回404");
    50             }
    51         } catch (Exception e) {
    52             System.out.println(e.toString());
    53         } finally {
    54             if (fis != null)
    55                 fis.close();
    56         }
    57     }
    58 }

    实验文件

    实验结果

     

     

    请求文件不存在

    当请求http://localhost:8080/shutdown 时,关闭容器,即不再监听端口

    四、读者可以自己做的优化,扩展的点

          1.在WEB_INF文件夹下读取web.xml解析,通过请求名找到对应的类名,通过类名创建对象,用反射来初始化配置信息,如welcome页面,Servlet、servlet-mapping,filter,listener,启动加载级别等。

          2.抽象Servlet类来转码处理请求和响应的业务。发过来的请求会有很多,也就意味着我们应该会有很多的Servlet,例如:RegisterServlet、LoginServlet等等还有很多其他的访问。可以用到类似于工厂模式的方法处理,随时产生很多的Servlet,来满足不同的功能性的请求。

          3.使用多线程技术。本文的代码是死循环,且只能有一个链接,而现实中的情况是往往会有很多很多的客户端发请求,可以把每个浏览器的通信封装到一个线程当中。

    https://my.oschina.net/liughDevelop/blog/1790893

  • 相关阅读:
    第九周学习进度
    用户场景描述
    第九天
    第10天
    求两个有序数组的中值
    计算字符串中最长子字符串的长度
    计算两个数之和
    将string 转int
    判断一个int 型整数 是否为回文数
    php 对象的一些特性
  • 原文地址:https://www.cnblogs.com/xdyixia/p/9410666.html
Copyright © 2020-2023  润新知