手写一个简化版Tomcat[通俗易懂]

手写一个简化版Tomcat

大家好,又见面了,我是你们的朋友全栈君。

      Tomcat作为Web服务器深受市场欢迎,有必要对其进行深入的研究。在工作中,我们经常会把写好的代码打包放在Tomcat里并启动,然后在浏览器里就能愉快的调用我们写的代码来实现相应的功能了,那么Tomcat是如何工作的?

一、Tomcat工作原理

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

172917_4tib_3577599.png

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

          173214_Xxkq_3577599.png

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

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

163755_KFNs_3577599.png

即一个由 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的装载、初始化、执行以及资源回收,它是最底层容器。

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

200436_4vKb_3577599.png

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

      本文实现效果比较简单,仅供新手参考,大神勿喷。当浏览器访问对应地址时:

202207_5Bfg_3577599.png

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

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

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

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

三、实现自己的Tomcat

工程文件结构和pom.xml文件:

210927_76zb_3577599.png

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

public class HttpServer {
  // 用于判断是否需要关闭容器
  private boolean shutdown = false;
  
  public void acceptWait() {
    ServerSocket serverSocket = null;
    try {
    	//端口号,最大链接数,ip地址
      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();
        //如果请求地址是/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地址返回:

public class Request {
  private InputStream is;
  private String url;

  public Request(InputStream input) {
    this.is = input;
  }
  public void parse() {
    //从socket中读取一个2048长度字符
    StringBuffer request = new StringBuffer(Response.BUFFER_SIZE);
    int i;
    byte[] buffer = new byte[Response.BUFFER_SIZE];
    try {
      i = is.read(buffer);
    }
    catch (IOException e) {
      e.printStackTrace();
      i = -1;
    }
    for (int j=0; j<i; j++) {
      request.append((char) buffer[j]);
    }
    //打印读取的socket中的内容
    System.out.print(request.toString());
    url = parseUrL(request.toString());
  }

  private String parseUrL(String requestString) {
    int index1, index2;
    index1 = requestString.indexOf(' ');//看socket获取请求头是否有值
    if (index1 != -1) {
      index2 = requestString.indexOf(' ', index1 + 1);
      if (index2 > index1)
        return requestString.substring(index1 + 1, index2);
    }
    return null;
  }

  public String getUrL() {
    return url;
  }

}

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

public class Response {
  public static final int BUFFER_SIZE = 2048;
  //浏览器访问D盘的文件
  private static final String WEB_ROOT ="D:";
  private Request request;
  private OutputStream output;

  public Response(OutputStream output) {
    this.output = output;
  }
  public void setRequest(Request request) {
    this.request = request;
  }

  public void sendStaticResource() throws IOException {
    byte[] bytes = new byte[BUFFER_SIZE];
    FileInputStream fis = null;
    try {
    	//拼接本地目录和浏览器端口号后面的目录
      File file = new File(WEB_ROOT, request.getUrL());
      //如果文件存在,且不是个目录
      if (file.exists() && !file.isDirectory()) {
        fis = new FileInputStream(file);
        int ch = fis.read(bytes, 0, BUFFER_SIZE);
        while (ch!=-1) {
          output.write(bytes, 0, ch);
          ch = fis.read(bytes, 0, BUFFER_SIZE);
        }
      }else {
           //文件不存在,返回给浏览器响应提示,这里可以拼接HTML任何元素
    	  String retMessage = "<h1>"+file.getName()+" file or directory not exists</h1>";
          String returnMessage ="HTTP/1.1 404 File Not Found\r\n" +
                  "Content-Type: text/html\r\n" +
                  "Content-Length: "+retMessage.length()+"\r\n" +
                  "\r\n" +
                  retMessage;
        output.write(returnMessage.getBytes());
      }
    }
    catch (Exception e) {
      System.out.println(e.toString() );
    }
    finally {
      if (fis!=null)
        fis.close();
    }
  }
}

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

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

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

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

还能做什么扩展,实现什么功能,读者可以在评论中与我探讨。

本文代码地址:https://github.com/qq53182347/liugh-tomcat

转载于:https://my.oschina.net/liughDevelop/blog/1790893

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/107593.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • 站点页面静态化具体解释

    站点页面静态化具体解释

    2022年2月1日
    55
  • iptables之ipset使用介绍[通俗易懂]

    iptables之ipset使用介绍[通俗易懂]ipset是什么?ipset是iptables的扩展,它允许你创建匹配整个地址集合的规则。而不像普通的iptables链只能单IP匹配,ip集合存储在带索引的数据结构中,这种结构即时集合比较大也可以进行高效的查找,除了一些常用的情况,比如阻止一些危险主机访问本机,从而减少系统资源占用或网络拥塞,IPsets也具备一些新防火墙设计方法,并简化了配置.官网:http://ipset.netfilt…

    2022年9月28日
    2
  • ps制作图案浮雕字效果_ps怎么设置文字浮雕效果

    ps制作图案浮雕字效果_ps怎么设置文字浮雕效果Q:ps如何做浮雕字A:小婷整理此问题:ps浮雕字怎么做的答案。如下面图像石头上面的浮雕字,在photoshop教程中的做法很多。小婷就以PS中图层样式中的“斜面和浮雕”来完成。感兴趣,不妨随意找一张图像,我们一起动手试试。 ps如何做浮雕字教程详细操做步骤如下:1.按下CTRL+O,弹出打开对话框,选择石头图像文件。2.按下CTRL+J,复制背景图层,得到图层1.3.

    2025年9月15日
    3
  • Java线上问题排查神器Arthas快速上手与原理浅谈

    Java线上问题排查神器Arthas快速上手与原理浅谈前言当你兴冲冲地开始运行自己的Java项目时,你是否遇到过如下问题:程序在稳定运行了,可是实现的功能点了没反应。为了修复Bug而上线的新版本,上线后发现Bug依然在,却想不通哪里有问题?想到可能出现问题的地方,却发现那里没打日志,没法在运行中看到问题,只能加了日志输出重新打包——部署——上线程序功能正常了,可是为啥响应时间这么慢,在哪里出现了问题?程序不但稳定运行,而且功能完美,但…

    2025年5月24日
    1
  • java oracle 连接池_oracle数据库连接池配置

    java oracle 连接池_oracle数据库连接池配置频繁的创建和销毁数据库连接即消耗系统资源又使得程序效率低下,在这种情况下,出现了使用数据库连接池的方法,类似于线程池,初期创建一定数量的连接供应用程序使用,当使用完成后将其归还给连接池而不是销毁,这样有效的提高了资源利用率,下面分享一种简单的创建连接池的方法:1.首先,我们新建一个maven工程,并且导入ojdbc,dbcp,junit三个包待用2.然后,我…

    2025年11月29日
    6
  • 通达信5分钟.lc5和.lc1文件格式

    通达信5分钟.lc5和.lc1文件格式一、通达信日线*.day文件文件名即股票代码每32个字节为一天数据每4个字节为一个字段,每个字段内低字节在前00~03字节:年月日,整型04~07字节:开盘价*100,整型08~11字节:最高价*100,整型12~15字节:最低价*100,整型16~19字节:收盘价*100,整型2…

    2022年7月24日
    90

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号