实现一个简易版的Tomcat(十七)

前言

此版本将会完善项目的一些细节,如:表单提交请求为post等...


一、支持post请求提交form表单

页面上要提交用户输入内容我们使用了form表单,而form表单的提交方式是通过method属性设置的,默认值为GET。还有一种提交方式为POST请求,使用这种方式提交表单时,数据会被包含在请求的消息正文中。

因此服务端如果想获取POST形式提交的数据,需要在HttpRequest中实现解析消息正文的工作。

通常页面上用户提交的数据包含敏感信息,比如密码。或者表单中包含附件信息时就要使用POST形式提交。

修改登录功能测试POST请求(注册功能同理,这里只是测试,所以以登录功能为例),实现步骤如下:

  1. 将登录页面login.html中form表单的提交方式修改为POST。
  2. 提交登录后会发现消息头中多出了Content-Type和Content-Length,表明当前请求包含消息正文,并说明了正文类型和长度。因此我们要在HttpRequest解析消息正文的方法中根据这两个头来解析内容。
    • 第一步:在Request类中重构解析uri代码,并创建一个parseParameter方法来解析post请求:
      /**
           * 进一步解析uri
           */
          private void parseUri() {
              /*
               * uri会有两种不同的情况:
               * 1:含有参数
               *   如果含有参数,要按照"?"将uri拆分,并将请求部分赋值给requestURI这个属性,
               *   将参数部分赋值给queryString这个属性,然后再将参数部分进一步拆分,将每一个
               *   参数保存到parameters这个Map中,其中key是参数名,value是参数值。
               *
               * 2:不含有参数
               *   不含有参数则直接将uri赋值给requestURI即可,其余两个属性不需要做任何操作。
               */
              try {
                  //将uri解码,将其中%XX内容还原为对应的文字。
                  uri = URLDecoder.decode(uri,"UTF-8");
              } catch (UnsupportedEncodingException e) {
                  e.printStackTrace();
              }
              if (!uri.contains("?")) {
                  requestURI = uri;
              } else {
                  //先按照"?"拆分uri
                  String[] data = uri.split("\\?");
                  requestURI = data[0];
                  if (data.length>1) {
                      queryString = data[1];
                      //进一步拆分为一个参数
                      parseParameter(queryString);
                  }
              }
          }
      
          private void parseParameter(String line) {
              String[] data = line.split("&");
              //username="xxx";
              for (String pairs : data) {
                  //再将每个参数按照“=”拆分
                  String[] arr = pairs.split("=");
                  if (arr.length>1) {
                      parameters.put(arr[0], arr[1]);
                  } else {
                      parameters.put(arr[0], null);
                  }
              }
          }
    • 之前一直没有在解析消息正文中写代码是因为是get请求,信息会在浏览器地址栏上,但是post请求会将信息保存到消息正文中,所以我们要根据消息头里的Content-Type和Content-Length来解析正文内容,代码如下:
      /**
           * 解析消息正文
           */
          private void parseContent() {
              System.out.println("Content:开始解析消息正文...");
              //首先判断当前请求方式是否为post
              if ("post".equalsIgnoreCase(method)) {
                  //获取Content-Length得知响应正文的长度
                  if (headers.containsKey("Content-Length")) {
                      int len = Integer.parseInt(headers.get("Content-Length"));
                      //读取响应正文数据
                      //1.先根据Content-Length创建字节数组
                      byte[] data = new byte[len];
                      try {
                          //2.将正文一次性读取到数组中
                          in.read(data);
      
                          //根据Content-Type判定正文类型
                          if (headers.containsKey("Content-Type")) {
                              String contentType = headers.get("Content-Type");
                              System.out.println("正文类型:"+contentType);
                              //根据正文类型不同做不同处理
                              if ("application/x-www-form-urlencoded".equals(contentType)) {
                                  /*
                                   * 如果正文类型是上述判定的值,则正文内容实际上就是原GET形式提交
                                   * 表单时在地址栏中"?"右侧的内容,是一个字符串。
                                   */
                                  String line = new String(data,"ISO8859-1");
                                  System.out.println("正文内容:"+line);
                                  line = URLDecoder.decode(line,"UTF-8");
                                  System.out.println("转码后内容:"+line);
                                  //拆分参数
                                  parseParameter(line);
                                  System.out.println("parameters:"+parameters);
                              }
                              //将来可以扩充对其他正文类型的支持
                          }
                      } catch (IOException e) {
                          e.printStackTrace();
                      }
                  }
              }
              System.out.println("Content:消息正文解析完毕!");
          }

       

二、使用线程池维护处理客户端交互的线程

之前的版本中,当一个客户端连接时,我们会启动一个线程来运行ClientHandler。每个ClientHandler完成的流程就是解析请求、处理请求、响应客户端,之后线程结束。

当多个客户端请求时会造成线程的频繁创建,由于线程的生命周期很短又会导致频繁的销毁,给系统带来额外的开销。

并且线程数量过多时,由于每个线程都要占用进程内存,那么线程越多占用资源越大,会导致可能出现的内存溢出问题。并且线程数量不加以控制,也会造成CPU的过度切换从而降低整体并发性能。
因此我们要使用线程池管理线程的数量并重用线程。

我们需要改良WebServer主类,代码如下:

1.创建线程池,先定义50条线程

2.利用线程池与客户端交互

三、优化Servlet

现在项目还存在一个问题:
每当我们添加一个新业务时,我们除了准备该业务需要的页面和处理该业务的Servlet类之外,还需要在ClientHandler添加一个分支判断:
当请求路径符合特定值时,就实例化对应的业务类并调用它的service方法。这样的做法不理想,我们最好将来能做到添加新业务时,不要每次都去修改ClientHandler,而是通过配置文件告知ClientHandler多了一个新的请求并对应哪个业务处理类,使得ClientHandler通过这种方式调用其service方法处理。

实现步骤:

1.首先在config目录下(之前放web.xml文件的目录)创建一个serlvets.xml文件,以后只要创建一个Servlet就需要在此xml文件中配置该Servlet。

2.要保证所有的Servlet类都有service方法,以免ClientHandler无法调用到该方法(我这里之前全部的Servlet的业务逻辑都写在service方法里面)。

3.定义一个超类:HttpServlet,并定义抽象方法service并要求所有的Servlet都继承它,这样所有的子类都一定包含这个方法了。

注:为什么定义成抽象方法?因为每个Servlet的业务是不一样的,因此service方法中的代码并不相同,所有抽象方法是保证了子类一定有这个方法,具体功能可以自行实现。 

4.在com.webserver.core包下创建ServletContext类用于解析servlets.xml,用的到思想和之前解析web.xml大致一样,不过这里会涉及到Servlet的生命周期,在这里简单阐述一下:

  • Serlvet的生命周期:
    1. 加载和实例化:程序启动后,Servlet容器通过类加载器找到Serlvet类并加载,加载成功后通过反射创建对应的Serlvet实例,并调用其无参构造,注:在编写Serlvet类时,根据业务需要,可以写有参构造,但是一定不能没有无参构造!
    2. 初始化:由于我们这里不涉及数据库连接,所以仅仅只有调用init方法,注:init方法只被调用一次,所以是静态的!
    3. 处理请求:调用service方法,根据不同的实现类调用不同的service方法,然后就是后续的一系列响应工作,注:在调用service方法之前,init方法必须被执行成功!
    4. 销毁:当一个Servlet容器检测到一个Servlet实例应该从服务中被移出的时候,此时容器会调用destroy方法释放资源,保存数据到持久层,该实例会被GC回收,需要时会再次创建一个新的Servlet实例。由于此项目是一个Tomcat服务器底层的代码,并不关联数据库,而且实现的功能较少,这里就不需要destroy方法。注:destroy方法也只被调用一次,所以也是静态的。
/**
 * 保存服务端相关公用信息
 * @Author JIANG
 */
public class ServerContext {
    private static Map<String, HttpServlet> servletMapping = new HashMap<>();

    static {
        intiServletMapping();
    }

    /**
     * 初始化所有请求与对应Servlet实例
     */
    private static void intiServletMapping() {

        /*
         * 解析config/servlets.xml
         * 将根标签下所有的servlet标签获取到,并将其中的
         * 属性:path的值作为key
         * 属性:className的值利用反射加载并实例化对应的Servlet作为value
         * 保存到servletMapping中完成初始化。
         */

        try {
            SAXReader reader = new SAXReader();
            Document doc = reader.read("config/servlets.xml");
            Element root = doc.getRootElement();
            List<Element> list = root.elements("servlet");
            for (Element e : list) {
                String key = e.attributeValue("path");
                Class<?> cls = Class.forName( e.attributeValue("className"));
                HttpServlet servlet = (HttpServlet) cls.newInstance();
                servletMapping.put(key, servlet);
            }
            System.out.println("servletMapping:"+servletMapping);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 根据请求路径获取对应的Servlet
     * @param path
     * @return
     */
    public static HttpServlet getServlet(String path) {
        return servletMapping.get(path);
    }

}

 5.在ClientHandler中重构处理业务请求的代码,这样代码简化了许多:

至此,WebServer整个项目完结。 


总结:

本项目的目的主要是解析Tomcat的底层是如何请求和响应,其实服务器的搭建比较简单,代码量并不大,主要是一些细节,比如说从最开始的处理空请求到如何解析post请求,再到uri的中文字符集转码,再到解析xml文件获取响应头类型,根据响应头类型来发送响应正文,用到的都是JAVASE一些比较基础的内容API,后面简单的实现了一些登录等功能,简单阐述了静态页面和动态页面的区别,最后利用servlet的思想将整个业务层重新优化了一下,最终呈现出的结果就是一个简易版的Tomcat,当然我们在工作中有框架支持,这些底层的东西也几乎不会写到,这里把这些思想分享给大家,有不足的地方请大家多多指出,我会尽可能的吸取大家的意见和建议。

最后,附一张整个项目的结构图,包含整体的思路:

如果大家对源码感兴趣可以去我的github下载:https://github.com/jiangyangsong

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值