实现Tomcat——实现javax.servlet.Servlet接口

0. 环境配置

这里使用IntelliJ IDEAMaven WebApp项目,不过这里我们不会使用/启动Tomcat服务器

本文的目的就是使用Socket实现一个服务器;此服务器是一个Servlet容器,我们需要遵循Servlet接口规范,即javax.servlet.*

这里由于我们使用的是Maven项目,所以这里引入servlet api 依赖,servlet api的版本为3.1

<!--  Servlet API  -->
<dependency>
  <groupId>javax.servlet</groupId>
  <artifactId>javax.servlet-api</artifactId>
  <version>3.1.0</version>
</dependency>

1. Servlet接口规范

我们查看Servlet接口,发现其接口方法只有5个

public interface Servlet {
	public void init(ServletConfig config) throws ServletException;
	public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
	public void destroy();
	public ServletConfig getServletConfig();
	public String getServletInfo();
}

其中init,service,destroy是3个与Servlet生命周期相关的函数。getServletConfig(),getServletInfo从方法名可以看出,它们与Servlet的信息相关。

实例化某个Servlet类后,servlet会调用其init方法来对servlet进行初始化,servlet容器只会调用该方法一次,调用后就可以执行service方法来处理请求相应逻辑。在servlet接收任何请求之前,必须是经过正确初始化的,初始化在servlet的生命周期中只会执行一次,所以我们可以在init方法中做一些初始化操作,如初始化默认值,载入数据库驱动等。。一般情况,我们可以将init方法留空,什么也不做。

当客户端的请求到达时,servlet容器就会响应相应servlet的service方法,并传入javax.servlet.ServletReqeustjavax.servlet.ServletResponse到service方法,其中ServletReqeust,ServletResponse分别包含HTTP请求响应的相关信息。service方法会在客户端每次请求时反复地被调用。

在Servlet实例服务移除前,servlet容器会调用servlet实例的destroy方法,一般当servlet容器关闭或者要释放内存时,才会将servlet实例移除。当且仅当servlet实例的service方法中所有线程都退出或执行超时后,才会调用destroy方法。

所以,现在我们总结以下,Servlet容器的处理流程:

  • 1. 当客户端请求服务器时,第一次调用某个servlet时,要先加载该servlet类,并调用其init方法(仅此一次)
  • 2. 针对于每个请求,都会创建一个 javax.servlet.ServletReqeust实例和一个javax.servlet.ServletResponse实例
  • 3. 调用该servlet的service方法,刚创建的ServletReqeust,ServletResponse作为service方法的参数。
  • 4. 当关闭该servlet类时,调用其destroy方法,并卸载该servlet类。

所以现在我们可以简单地实现一个自己的Servlet类,我们称之为PrimitiveServlet,它继承自servlet接口,所以我们必须要重写servlet的5个方法:

public class PrimitiveServlet implements Servlet {
    @Override
    public void init(ServletConfig servletConfig) throws ServletException {
        System.out.println("Init...");
    }

    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        System.out.println("From service");
        PrintWriter out = response.getWriter();
        // 头部信息
        out.write("HTTP/1.1 200 OK\r\n" +
                    "Content-Type: text/html\r\n" +
                    "\r\n" );
        out.println("<h1>Hello " + this.getClass().getSimpleName() + " </h1>");
    }

    @Override
    public void destroy() {
        System.out.println("Destroy...");
    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public String getServletInfo() {
        return null;
    }
}

我们看到,这里的servlet的service方法,我们简单地返回一个html文本,其内容是
Hello ${ServletName}。同时,我们暂时只对servlet的3个生命周期函数做一个简单实现。

2. 我们要实现的服务器的任务

我们的任务就是根据不同请求的URL地址,来做出不同的响应,这里我们做的很简单,假定用户只会按照下面的格式来访问我们的服务器。

  • http://localhost:8080/staticResource,此类Url访问,我们当作静态资源的请求
  • http://localhost:8080/servlet/servletName,此类Url访问,我们当作一个Servlet请求
  • http://localhost:8080/SHUTDOWN,关闭服务器。

3. HttpServer类

这里我们定义一个HTTP Server服务器的的主启动类,名为TomHttpServer1,它主要用来创建服务端Socket,并处理Socket请求。

public class TomHttpServer1 {

   Logger logger = LoggerFactory.getLogger(TomHttpServer1.class);

   public static final String SHUTDOWN = "/SHUTDOWN";

   public static final int PORT = 8080;

   private boolean isShutDown = false;

   public static void main(String[] args) {
       TomHttpServer1 httpServer = new TomHttpServer1();
       httpServer.await();
   }

   public void await()  {

       ServerSocket serverSocket = null;
       try {
           serverSocket = new ServerSocket(PORT, 1, InetAddress.getByName("localhost"));
       } catch (Exception e){
           e.printStackTrace();
           System.exit(1);
       }

       while(!isShutDown){
           Socket socket = null;
           InputStream input = null;
           OutputStream output = null;
           try {
               socket = serverSocket.accept();
               input = socket.getInputStream();
               output = socket.getOutputStream();

               // 创建ServletRequest,ServletResponse
               TomServletRequest servletRequest = new TomServletRequest(input);
               TomServletResponse servletResponse = new TomServletResponse(output, servletRequest);

               // process
               if(servletRequest.getUri() != null && servletRequest.getUri().startsWith("/servlet/")){
                   // 如果是请求Servlet
                   ServletProcessor1 sp1 = new ServletProcessor1();
                   sp1.process(servletRequest, servletResponse);
               } else {
                   // 否则,我们认为它是请求静态资源
                   StaticResourceProcessor srp = new StaticResourceProcessor();
                   srp.process(servletRequest, servletResponse);
               }
               socket.close();
               isShutDown = servletRequest.getUri().equals(SHUTDOWN);
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
   }
}

我们看到main函数,await方法为主要的逻辑所在。await方法首先初始化一个监听在8080端口的ServerSocket,然后根据Servlet 规范,我们创建ServletRequest,ServletResponse两个接口的实现类TomServletRequest,TomServletResponse实例,然后根据请求的URI来判断是请求静态资源还是请求servlet

servletRequest.getUri().startsWith("/servlet/")

如果是Servlet请求,我们就交由ServletProcessor1去处理,否则交由StaticResourceProcessor去处理。
这里的sp1.process(servletRequest, servletResponse);srp.process(servletRequest, servletResponse);暂时可以简单地理解为service(servletRequest,servletResponse)方法。

然后isShutDown = servletRequest.getUri().equals(SHUTDOWN);表示如果用户输入如下URL,整个服务器就停止运行。
http://localhost:8080/SHUTDOWN

上面代码的4个类TomServletRequest,TomServletResponse,StaticResourceProcessor ,StaticResourceProcessor我们现在仍然没有实现,接下来,会一一实现。

4. ServletRequest类

public class TomServletRequest implements ServletRequest {

    private Logger logger = LoggerFactory.getLogger(TomServletRequest.class);

    private InputStream input;
    private String uri;

    public TomServletRequest(InputStream input) {
        this.input = input;
        this.parse();
    }

    private void parse(){
        StringBuffer requestStringBuffer = new StringBuffer(2048);
        int len = -1;
        byte[] buffer = new byte[2048];
        try{
            len = input.read(buffer);
        } catch (Exception e){
            e.printStackTrace();
        }
        for(int j = 0 ; j < len ; j ++){
            requestStringBuffer.append((char) buffer[j]);
        }
        logger.info("request string: \n{}", requestStringBuffer);
        uri = parseUri(requestStringBuffer.toString());
        logger.info("parse uri: \n{}", uri);
    }

    public String getUri() {
        return uri;
    }

    /**
     * The first line of the http header is just like this :
     * GET /servlet/tomservlet HTTP/1.1
     * @param requestString
     * @return
     */
    private String parseUri(String requestString){
        int index1, index2;
        index1 = requestString.indexOf(' ');
        if(index1 != -1 ){
            index2 = requestString.indexOf(' ', index1 + 1);
            if(index2 > index1){
                return requestString.substring(index1 + 1, index2);
            }
        }
        return null;
    }

    /**
     * 下面是部分重写的代码,代码实现为空
     * 后面我们将一一实现,这里暂时省略,一面占篇幅
     */

Socket接收到请求后,就获取socket对应的InputStream,并传入TomServletRequest类的构造函数TomServletRequest(InputStream input)以创建一个ServletRequest类。

这里我们注意到一个parse()方法,它能够解析出HTTP头信息的请求URI地址,核心代码就是parseUri函数。parseUri函数很容易理解,我们可以参考下面的HTTP头信息的第一行理解一下:我们只需要找到第一个空格第二个空格的索引即可,两个索引之间的字符就是HTTP请求的URI地址。

GET /servlet/tomservlet HTTP/1.1

5. Constants类

定义常量类,定义当前maven web的根目录。

public class Constants {
    public static final String WEB_ROOT = new File("").getAbsoluteFile().getPath()
                                                + "\\src\\main\\webapp";

6. ServletResponse类

public class TomServletResponse implements ServletResponse {

    private static final int BUFFER_SIZE = 1024;
    private OutputStream out;
    private TomServletRequest servletRequest;
    private PrintWriter writer;

    public TomServletResponse(OutputStream out, TomServletRequest servletRequest) {
        this.out = out;
        this.servletRequest = servletRequest;
    }

    public void sendStaticResources() throws IOException {
        FileInputStream fis = null;
        try{
            File file = new File(Constants.WEB_ROOT, servletRequest.getUri());
            fis = new FileInputStream(file);

            // 头部信息
            out.write(
                    ("HTTP/1.1 200 OK\r\n" +
                    "Content-Type: text/html\r\n" +
                    "\r\n" ).getBytes());

            byte[] bytes = new byte[BUFFER_SIZE];
            int ch = -1;
            while ( (ch = fis.read(bytes, 0, BUFFER_SIZE) )!=-1) {
                out.write(bytes, 0, ch);
            }

        } catch (FileNotFoundException e) {
            String notFoundMessage =
                    "HTTP/1.1 404 FILE NOT FOUND\r\n" +
                    "Content-Type: text/html\r\n" +
                    "Content-Length: 28\r\n" +
                    "\r\n" +
                    "<h1>404: FILE NOT FOUND</h1>";
            out.write(notFoundMessage.getBytes());
        } catch (Exception e){
            e.printStackTrace();
        } finally{
            if(fis != null){
                fis.close();
            }
        }
    }

    @Override
    public PrintWriter getWriter() throws IOException {
        // true stand for autoFlush
        writer = new PrintWriter(out, true);
        return writer;
    }

    /**
     * 下面是重写ServletResponse的方法
     * 之后我们会实现它们
     */

这里sendStaticResources为核心方法,意思是发送静态资源。通过之前request的parse解析出URI,那么现在我们已经知道了客户端请求的URI是什么了,所以我们可以根据URI来定位到具体的静态文件,并发送给客户端。上面的代码很容易理解,就是读取静态资源文件然后返回给客户端,我们假定请求的静态资源都是html文档。

这里要注意头部信息要带上,不然浏览器输入URL时,浏览器会报错ERR_INVALID_HTTP_RESPONSE,得不到响应。

7. StaticResourceProcessor

StaticResourceProcessor,静态资源处理器。我们直接使用TomServletResponse实现的方法sendStaticResources即可。

public class StaticResourceProcessor {

    public void process(TomServletRequest servletRequest, TomServletResponse servletResponse){
        try{
            servletResponse.sendStaticResources();
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

8. StaticResourceProcessor

静态资源的处理,我们已经解决了,就是通过请求的URI来定位到Web根目录下的文件,并读取放回给客户端。

但是,我们如何通过servlet字符串来定位到Servlet类,并且交由Servlet类来处理此Servlet请求呢?
我们先上源码,然后再细细分析。

public class ServletProcessor1 {

    public void process(TomServletRequest servletRequest, TomServletResponse servletResponse){
        String uri = servletRequest.getUri();
        String servletName = uri.substring(uri.lastIndexOf("/") + 1);
        URLClassLoader loader = null;
        try{
        	// 我们的请求中URL只有一个,所以实例化一个大小为1的URL数组
            URL[] urls = new URL[1];
            URLStreamHandler urlStreamHandler = null;
            File classPath = new File(Constants.WEB_ROOT);
            // repository,从此URL目录("仓库")来加载类
            String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString();
            urls[0] = new URL(null, repository, urlStreamHandler);
            // 从urls指定的url来加载类
            loader = new URLClassLoader(urls);
        } catch (Exception e){
            e.printStackTrace();
        }
        Class<?> clazz = null;
        try{
            clazz = loader.loadClass(servletName);
        } catch (Exception e){
            e.printStackTrace();
        }
        Servlet servlet = null;
        try{
            servlet = (Servlet) clazz.newInstance();
            servlet.service(servletRequest, servletResponse);
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

首先我们要介绍的一个类就是URLClassLoader类,它是ClassLoader类的直接子类,它的构造函数函数URLClassLoader(URL[])接收一个URL数组,意思是,从这些URL目录下去加载Servlet类。

下面是核心代码,通过Class类来newInstance,并调用其service方法来处理Servlet请求

// 实例化类加载器
URLClassLoader loader = new URLClassLoader(urls);
// 使用类加载器来加载Servlet类
Class<?> clazz = loader.loadClass(servletName);
// 实例化Servlet类
Servlet servlet = (Servlet) clazz.newInstance();
// 调用其service方法
servlet.service(servletRequest, servletResponse);

9. 总结

现在我们可以开始访问了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值