看着Tomcat源码,写服务器(第二天)

1. 看着Tomcat源码,写服务器

Q: 为什么需要使用Servlet?

A: Servlet是一种Java编程语言的技术,用于构建动态Web应用程序。它可以处理客户端发送的请求并生成相应的响应。使用Servlet可以实现与数据库的交互、数据处理、用户身份验证等功能。此外,Servlet还具有可扩展性和可重用性,可以轻松地进行开发和维护。因此,使用Servlet可以提升Web应用程序的性能和功能,使其更加强大和灵活。

Q: 如果没有servlet会怎么样?

A: 在没有servlet的情况下,Web应用程序将无法处理动态内容和交互式功能。servlet是一种服务器端的Java程序,用于处理HTTP请求和响应。它可以与数据库进行交互,生成动态的网页内容,并处理用户提交的表单数据。因此,没有servlet的话,Web应用程序将无法实现与用户的交互,无法获取和显示动态数据,也无法与数据库进行通信。这将严重限制Web应用程序的功能和灵活性。

Q: servlet和http协议之间有着什么关联?

A: servlet是一种用于处理HTTP请求和响应的Java技术。它可以被看作是一种在服务器端运行的小程序,通过HTTP协议与客户端进行通信。HTTP协议则是一种用于在网络上传输超文本的协议。它定义了客户端和服务器之间的通信规则和格式。因此,可以说servlet是基于HTTP协议的一种技术,用于实现服务器端的业务逻辑处理和响应客户端的请求。

Q: 为什么说servlet实际上是一个接口呢?

A: servlet是Java编程语言中的一个规范,它定义了一种在Web服务器上运行的组件的行为和功能。简而言之,servlet提供了一种处理HTTP请求和生成HTTP响应的方式。它可以被认为是一个用于构建Web应用程序的基础框架。通过实现servlet接口,开发人员可以编写自己的servlet类,并根据自己的需求来处理客户端的请求和生成响应。因此,servlet实际上是一个接口,它为开发人员提供了一种定义和实现自己的Web应用程序的方式。

核心概念

  • Servlet容器 :用于提供HTTP请求给Servlet的环境。它负责接收和处理来自客户端的请求,并将请求传递给相应的Servlet进行处理。 Servlet容器还负责管理Servlet的生命周期,包括初始化Servlet、调用Servlet的服务方法以及销毁Servlet的过程。
  • Servlet接口 :所有Servlet都必须实现的接口,它定义了Servlet的生命周期方法(init, service, destroy)。这些方法分别在Servlet被初始化、每次接收到客户端请求时以及Servlet被销毁时被调用。通过实现Servlet接口,开发人员可以编写自己的Servlet类,并在其中实现业务逻辑。

此外,Java Servlet API 中还有几个其他重要的组件,用于各种目的,例如处理会话、管理请求和响应头以及处理 URL 映射。这些组件为使用 Java Servlet 技术开发 Web 应用程序提供了强大而灵活的平台。

重要组件

  • PrimitiveServlet :示例 Servlet,用于测试容器。
  • Servlet 生命周期方法
  • init() :当servlet首次初始化时调用此方法。它用于执行任何必要的设置或初始化任务。
  • service() :该方法负责处理发送到servlet的请求。它处理servlet的逻辑并确定如何响应每个请求。
  • destroy() :当servlet即将被销毁时调用此方法。它用于清理servlet使用的任何资源并执行任何必要的关闭任务。
  • ServletRequest 和 ServletResponse :Servlet 使用的对象,封装请求和响应。

下面还有俩不怎么需要去记的生命周期:

  • thread_safe():Servlet 被设计为线程安全的,这意味着它可以同时处理多个请求。这是通过使 Servlet 容器创建多个 Servlet 实例,并在不同的线程中调用 service() 方法来实现的。
  • session() :Servlet 还可以管理用户会话,允许跨多个请求跟踪用户特定的数据。Servlet 容器提供了用于创建、访问和管理用户会话的 API。

接下来我会写两个应用程序去带你们回忆它们:

应用程序 1

  • 功能:处理简单的 Servlet 和静态资源,不调用 init 和 destroy 方法。
  • 组件:HttpServer1, Request, Response, StaticResourceProcessor, ServletProcessor1, Constants。

需要添加一个关于servlet的pom依赖:

    
            javax.servlet
            javax.servlet-api
            3.1.0
            compile
        

代码

Request.java 和 Response其实和之前差不多,只不过实现了servlet的子接口会有一些生成的其他方法,去阅读一下代码

small_http:

git clone https://github.com/MrKrabXie/writeHttpServer.git
process
package crab2;

import javax.servlet.Servlet;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLStreamHandler;

public class ServletProcessor1 {
  public void process(Request request, Response response) {
    // 获取请求的uri
    String uri = request.getUri();
    // 取得uri中"/"后面的字符串作为servletName
    String servletName = uri.substring(uri.lastIndexOf("/") + 1);
    URLClassLoader loader = null;

    try {
      // 创建一个URLClassLoader
      URL[] urls = new URL[1];
      URLStreamHandler streamHandler = null;
      File classPath = new File(Constants.WEB_ROOT);
      // 创建仓库的url
      // 这个url创建方法参考了org.apache.catalina.startup.ClassLoaderFactory类中的createClassLoader方法
      String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString();
      // 生成URL的方法取自org.apache.catalina.loader.StandardClassLoader类中的addRepository方法
      urls[0] = new URL(null, repository, streamHandler);
      loader = new URLClassLoader(urls);
    } catch (IOException e) {
      System.out.println(e.toString());
    }

    // 加载servlet类
    Class myClass = null;
    try {
      myClass = loader.loadClass(servletName);
    } catch (ClassNotFoundException e) {
      System.out.println(e.toString());
    }

    Servlet servlet = null;
    try {
      // 创建servlet实例
      servlet = (Servlet) myClass.newInstance();
      // 在服务请求和响应上调用servlet
      servlet.service((ServletRequest) request, (ServletResponse) response);
    } catch (Exception e) {
      System.out.println(e.toString());
    } catch (Throwable e) {
      System.out.println(e.toString());
    }
  }
}

首先,我们看到类ServletProcessor1,其主要功能是处理Request和Response。在这个类中,一系列的try和catch块尝试创建一个URLClassLoader去加载对应的Servlet并执行服务

这段代码首先从Request中获取URI,然后提取出servletName,即"/"后面的字符串。然后创建一个URLClassLoader,用于加载servlet的类文件。创建ClassLoader需要的repository是从文件系统中获取的WEB_ROOT目录。
经过一系列的加载和实例化后,得到了我们的Servlet实例。然后,将请求和响应进行服务化处理,即servlet.service(request,response)。

package crab2;
import java.io.IOException;
public class StaticResourceProcessor {
  // 处理方法,接受请求和响应为参数
  public void process(Request request, Response response) {
    try {
      // 响应发送静态资源
      response.sendStaticResource();
    }
    // 捕获并打印可能出现的IOException异常
    catch (IOException e) {
      e.printStackTrace();
    }
  }
}

如上

import javax.servlet.*;
import java.io.IOException;
import java.io.PrintWriter;

// 实现了 Servlet 接口的服务器组件,可以接收和响应来自客户端的请求
public class PrimitiveServlet implements Servlet {

  // 初始化函数,在 Servlet 生命周期中只会被调用一次
  public void init(ServletConfig config) throws ServletException {
    System.out.println("初始");
  }

  // service 函数,每次接收到客户端请求时被调用
  public void service(ServletRequest request, ServletResponse response)
          throws ServletException, IOException {
    System.out.println("来自 service 函数");
    PrintWriter out = response.getWriter();
    String errorMessage = "HTTP/1.1 404 文件未找到\r\n" +
            "Content-Type: text/html\r\n" +
            "Content-Length: 23\r\n" +
            "\r\n" +
            "

hi rose.<\\p>\r\n" + "

it is blue..<\\p>"; out.println(errorMessage); } // 销毁函数,在 Servlet 生命周期结束时被调用 public void destroy() { System.out.println("销毁"); } // 返回 Servlet 的信息,此方法可选 public String getServletInfo() { return null; } // 返回 ServletConfig 对象,它包含了 Servlet 的初始化和启动配置 public ServletConfig getServletConfig() { return null; } }

这段代码定义了一个名为PrimitiveServlet的类,并实现了Servlet接口。
init函数是一个初始化函数,Servlet在其生命周期中只调用一次这个函数。

service函数在每次接收到客户端请求时被调用。request和response对象分别用来接收和响应客户端的请求。

destroy函数在Servlet的生命周期结束时被调用。

getServletInfo函数返回Servlet的信息,这个方法是可选的。
getServletConfig函数返回一个ServletConfig对象,这个对象包含了Servlet的初始化和启动配置。

应用程序 2

思考:

第一个应用程序有一个严重的问题。在 ServletProcessor1 类的 process 方法,你向上转换
Request 实例为 javax.servlet.ServletRequest,并作为第一个参数传递给
servlet 的 service 方 法 。 你 也 向 下 转 换Response 实 例 为
javax.servlet.ServletResponse,并作为第二个参数传递给 servlet 的 service 方法。

try {
servlet = (Servlet) myClass.newInstance();
servlet.service((ServletRequest) request,(ServletResponse) response);
}

这会危害安全性。知道这个 servlet 容器的内部运作的 Servlet 程序员可以分别把
ServletRequest 和 ServletResponse 实 例 向 下 转 换 为 Request 和
Response,并调用他们的公共方法。拥有一个 Request实例,它们就可以调用 parse
方法。拥有一个 Response 实例,就可以调用 sendStaticResource 方法。

讨论
  • 场景: 是在, 老开发的年代, 分别需要用lib包去作为传递桌面端一些内部类,又不想一些桌面端的代码信息泄露, 毕竟现在都是web接口开发就不太容易有这顾虑和问题, 大家这里看看就好。 知道正常接口是向上转。而不能让使用者去向下转就行。
  • 改进:增加了安全性,引入了 facade 类(RequestFacade 和 ResponseFacade)以保护内部实现。
  • 组件:HttpServer2, ServletProcessor2(使用 façade 类处理请求和响应)。
  • 具体原因: 开放内部操作或过多的公有方法可能会影响应用的安全性。如果程序员可以获取 ServletRequest 和 ServletResponse 对象,并将它们向下转型为 crab.Request 和 crab.Response,那么他们就能够调用这些类中的公有方法。
    例如,parse 方法可能会被误用来解析非法或有害的请求,sendStaticResource 方法可能被用来输出不应该被访问的资源。这些都可能导致安全问题。
    这就是为什么通常要坚持最小权限原则(Principle of Least Privilege),只有那么多的权限才能完成工作是必要的。特别是对外部用户,你应该尽可能地限制他们能够访问的方法或资源。这也包括设计 API 和暴露服务时考虑到的,你应该只提供必要的接口,不要为了方便而公开过多的内部操作。

这里贴部分代码,解释一下思路就是: 可以通过默认的访问修饰符去控制,也可以通过一个设计模式进行优化:
下面代码,对比他们属性和方法。猜设计模式是什么设计模式。


// 创建公用的请求类
public class Request implements ServletRequest {
  // 定义输入流
  private InputStream input;
  // 定义uri
  private String uri;
  // 创建构造器,接受输入流参数
  public Request(InputStream input) {
    this.input = input;
  }
  // 获取uri的方法
  public String getUri() {
    return uri;
  }
public class RequestFacade implements ServletRequest {  private ServletRequest request = null;  public RequestFacade(Request request) {
    this.request = request;
  }

  public Object getAttribute(String attribute) {
    return request.getAttribute(attribute);
  }

  public Enumeration getAttributeNames() {
    return request.getAttributeNames();
  }

是使用门面模式(Facade Pattern)的方法来提升安全性。在最初的设计里,当 ServletRequest 对象被传递给 service 方法时,它会被向下转换为 Request 对象。这样做的问题是 service 方法现在可以调用 Request 中所有的公有方法,包括一些可能导致安全问题的方法。
为了解决这个问题,引入了 RequestFacade 类。这个类实现了 ServletRequest 接口,自身包含一个 Request 的引用,但只暴露出 ServletRequest 接口中的方法,而不需要把 Request 对象本身暴露出来。这样一来,尽管开发者仍可以对 ServletRequest 进行向下转型得到 RequestFacade,但他们只能通过 ServletRequest 接口调用对应的公开方法,无法访问到 Request 类内部的方法。
这样,我们就隐藏了 Request 的内部实现,避免了该类对象的滥用,也就提高了系统的安全性。比如,你提到的 parseUri 方法,就因为它不能从外部访问而变得更安全了。

总结

本章通过构建简单的 Servlet 容器,介绍了 Servlet 编程的基础,为构建完整的 Tomcat 容器奠定了基础。

你可能感兴趣的:(tomcat,服务器,java)