考虑自己实现一个Tomcat,都有哪些关键的要点呢?

第一,提供 Socket 服务

Tomcat 的启动,必然是 Socket 服务,只不过它支持 HTTP 协议而已!

这里其实可以扩展思考下,Tomcat 既然是基于 Socket,那么是基于BIO or NIO or AIO 呢?

第二,进行请求的分发

要知道一个 Tomcat 可以为多个 Web 应用提供服务,那么很显然,Tomcat 可以把 URL 下发到不同的Web应用。

第三,需要把请求和响应封装成`request / response

我们在 Web 应用这一层,可从来没有封装过 request/response 的,我们都是直接使用的,这就是因为 Tomcat 替我们做好了这一步。

创建MyRequest对象

首先创建自定义的请求类,其中定义url与method两个属性,表示请求的url以及请求的方式。

其构造函数需要传入一个输入流,该输入流通过客户端的套接字对象得到。

package com.king.utils;

//实现自己的请求类

public class MyRequest {

    //请求的url

    private String url;

    //请求的方法类型

    private String method;

    //构造函数 传入一个输入流

    public MyRequest(InputStream inputStream) throws IOException {

        //用于存放http请求内容的容器

        StringBuilder httpRequest=new StringBuilder();

        //用于从输入流中读取数据的字节数组

        byte[]httpRequestByte=new byte[1024];

        int length=0;

        //将输入流中的内容读到字节数组中,并且对长度进行判断

        if((length=inputStream.read(httpRequestByte))>0) {

            //证明输入流中有内容,则将字节数组添加到容器中

            httpRequest.append(new String(httpRequestByte,0,length));

        }

        //将容器中的内容打印出来

        System.out.println("httpRequest = [ "+httpRequest+" ]");              

        //从httpRequest中获取url,method存储到myRequest中

        String httpHead=httpRequest.toString().split("\n")[0];

        System.out.println("MyRequests header:"+httpHead);

        if(httpHead!=null){

        url=httpHead.split("\\s")[1];

             method=httpHead.split("\\s")[0];

             System.out.println("MyRequests = [ "+this+" ]"+method+":"+url);

        }

    }

    public String getUrl() {

        return url;

    }

    public void setUrl(String url) {

        this.url = url;

    }

    public String getMethod() {

        return method;

    }

    public void setMethod(String method) {

        this.method = method;

    }

 }

创建MyResponse对象

创建自定义的响应类,构造函数需要传入由客户端的套接字获取的输出流。

定义其write方法,像浏览器写出一个响应头,并且包含我们要写出的内容content。

package com.king.utils;

//实现自己的响应类

public class MyResponse {

    //定义输出流

    private OutputStream outputStream;

    //构造函数 传入输出流

    public MyResponse(OutputStream outputStream) {

        this.outputStream=outputStream;

    }    

    //创建写出方法

    public void write(String content)throws IOException{

        //用来存放要写出数据的容器

        StringBuffer stringBuffer=new StringBuffer();

        stringBuffer.append("HTTP/1.1 200 OK\r\n")

        .append("Content-type:text/html\r\n")

        .append("\r\n")

        .append("<html><head><title>Hello World</title></head><body>")

        .append(content)

        .append("</body><html>");

        //转换成字节数组 并进行写出

        outputStream.write(stringBuffer.toString().getBytes());

        //System.out.println("sss");

        outputStream.close();

    }    

}

创建MyServlet对象

由于我们自己写一个Servlet的时候需要继承HttpServlet,因此在这里首先定义了一个抽象类——MyServlet对象。

在其中定义了两个需要子类实现的抽象方法doGet和doSet。

并且创建了一个service方法,通过对传入的request对象的请求方式进行判断,确定调用的是doGet方法或是doPost方法。

package com.king.utils;

//写一个抽象类作为servlet的父类

public abstract class MyServlet {

    //需要子类实现的抽象方法

    protected abstract void doGet(MyRequest request,MyResponse response);

    protected abstract void doPost(MyRequest request,MyResponse response);

     //父类自己的方法

    //父类的service方法对传入的request以及response

    //的方法类型进行判断,由此调用doGet或doPost方法

    public void service(MyRequest request,MyResponse response) throws NoSuchMethodException {

        if(request.getMethod().equalsIgnoreCase("POST")) {

            doPost(request, response);

        }else if(request.getMethod().equalsIgnoreCase("GET")) {

            doGet(request, response);

        }else {

            throw new NoSuchMethodException("not support");

        }

    }

}


创建业务相关的Servlet

这里我创建了两个业务相关类



创建映射关系结构ServletMapping

该结构实现的是请求的url与具体的Servlet之间的关系映射。

package com.king.utils;

//请求url与项目中的servlet的映射关系

public class ServletMapping {

    //servlet的名字

    private String servletName;

    //请求的url

    private String url;

    //servlet类

    private String clazz;

    public String getServletName() {

        return servletName;

    }

    public void setServletName(String servletName) {

        this.servletName = servletName;

    }

    public String getUrl() {

        return url;

    }

    public void setUrl(String url) {

        this.url = url;

    }

    public String getClazz() {

        return clazz;

    }

    public void setClazz(String clazz) {

        this.clazz = clazz;

    }

    public ServletMapping(String servletName, String url, String clazz) {

        super();

        this.servletName = servletName;

        this.url = url;

        this.clazz = clazz;

    }

}

映射关系配置对象ServletMappingConfig

配置类中定义了一个列表,里面存储着项目中的映射关系。

package com.king.utils;

//创建一个存储有请求路径与servlet的对应关系的 映射关系配置类

public class ServletMappingConfig {

    //使用一个list类型 里面存储的是映射关系类Mapping

    public static List<ServletMapping>servletMappings=new ArrayList<>(16);

    //向其中添加映射关系

    static {

    //TODO使用解析web.xml的配置文件的方式替换硬编码。

        servletMappings.add(new ServletMapping("first","/first", "com.king.servlet.FirstServlet"));

        servletMappings.add(new ServletMapping("second","/second", "com.king.servlet.SecondServlet"));

        servletMappings.add(new ServletMapping("test","/test", "com.king.servlet.TestServlet"));//这个不存在的

    }

}

自定义tomcat服务端MyTomcat

在服务端MyTomcat中主要做了如下几件事情:

1)初始化请求的映射关系。

2)创建服务端套接字,并绑定某个端口。

3)进入循环,用户接受客户端的链接。

4)通过客户端套接字创建request与response对象。

5)根据request对象的请求方式调用相应的方法。

6)启动MyTomcat!

package com.king;

//Tomcat服务器类 编写对请求做分发处理的相关逻辑

public class MyTomcat {

    //端口号

    private int port=8080;

    //用于存放请求路径与对应的servlet类的请求映射关系的map

    //相应的信息从配置类中获取

    private Map<String, String>urlServletMap=new HashMap<>(16);

    //构造方法

    public MyTomcat(int port) {

        this.port=port;

    }

     //tomcat服务器的启动方法

    public void start() {

        //初始化请求映射关系

        initServletMapping();

        //TODO 使用NIO替换BIO

        //服务端的套接字

        ServerSocket serverSocket=null;

        try {

            //创建绑定到某个端口的服务端套接字

            serverSocket=new ServerSocket(port);

            System.out.println("MyTomcat begin start...");

            //循环 用于接收客户端

            while(true) {

                //接收到的客户端的套接字

                Socket socket=serverSocket.accept();

                //获取客户端的输入输出流

                InputStream inputStream=socket.getInputStream();

                OutputStream outputStream=socket.getOutputStream();

                //通过输入输出流创建请求与响应对象

                MyRequest request=new MyRequest(inputStream);

                MyResponse response=new MyResponse(outputStream);

                

                //根据请求对象的method分发请求 调用相应的方法

                dispatch(request, response);

                //关闭客户端套接字

                socket.close();

            }

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

    

    //初始化请求映射关系,相关信息从配置类中获取

    private void initServletMapping() {

        for(ServletMapping servletMapping:ServletMappingConfig.servletMappings) {

            urlServletMap.put(servletMapping.getUrl(), servletMapping.getClazz());

        }

    }

    //通过当前的request以及response对象分发请求

    private void dispatch(MyRequest request,MyResponse response) throws IOException {

        //根据请求的url获取对应的servlet类的string

        String clazz=urlServletMap.get(request.getUrl());

        System.out.println("====="+clazz);

        if(clazz==null){

        response.write("404");

        return;

        }

        try {

            //通过类的string将其转化为对象

            Class servletClass=Class.forName(clazz);

            //实例化一个对象

            MyServlet myServlet=(MyServlet)servletClass.newInstance();

             //调用父类方法,根据request的method对调用方法进行判断

            //完成对myServlet中doGet与doPost方法的调用

            myServlet.service(request, response);

        } catch (ClassNotFoundException e) {

            e.printStackTrace();

            response.write("501");

        } catch (IllegalAccessException e) {

            e.printStackTrace();

        } catch (InstantiationException e) {

            e.printStackTrace();

        } catch (NoSuchMethodException e) {

            e.printStackTrace();

        }

    }    

    //main方法  直接启动tomcat服务器

    public static void main(String[] args) {

        new MyTomcat(8080).start();

    }    

}



浏览器输入http://127.0.0.1:8080/first

即可显示返回

FirstServlet servlet get>>>