从零开始手写Tomcat,一文彻底搞懂Tomcat运行流程(附源码)

一:理论铺垫

tomcat的功能是什么?

tomcat是java的一个中间件,浏览器发出HTTP请求后经过tomcat中间件,转发到目的服务器,目的服务器返回响应消息,通过tomcat返回给浏览器。tomcat的使用很简单,但是作为合格的程序员,光会用可不行,接下来就通过手写一个tomcat彻底搞懂tomcat。

在手写Tomcat之前,首先重温一下http和servlet,http协议分为请求协议和响应协议:

http请求协议部分数据

GET /user HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9

第一部分:请求行:请求类型,资源路径以及http版本(上述第一行)

第二部分:请求头:紧接在请求行之后,用于说明服务器需要使用的附加信息(第二到第八行)

第三部分:空行(请求头和主体之间必须有换行)

第四部分:主体数据,可以添加任意数据

第三部分和第四部分在上述文档中没有显示

http响应协议

HTTP/1.1 200
Content-Type:text/html

OK

第一部分:状态行,http版本,状态码,状态信息(第一行)

第二部分:响应报文头部,说明服务器需要用到的附加信息(第二行)

第三部分:空行(第三行)

第四部分:响应正文(第四行)

Servlet的运行过程:

实例化Servlet

init方法进行初始化

当收到客户端请求信息时,调用service方法处理客户端请求,service会根据不同的请求类型,调用不同的doXXX()方法

销毁(servlet容器关闭时,实例也随之销毁)

二:理解客户端和服务器的通信

客户端和服务器的通信,说到底就是两个数据的传输,客户端发送inputStream给服务器,服务器回复outputStream给客户端。

首先用代码模拟一下这个过程:

public class TomcatServerV1 {
    public static void main(String[] args)throws IOException{
        //开启ServerSocket服务,设置端口号为8080
        ServerSocket serverSocket=new ServerSocket(8080);
        System.out.println("======服务启动成功========");
        //当服务没有关闭时
        while(!serverSocket.isClosed()){
            //使用socket进行通信
            Socket socket=serverSocket.accept();
            //收到客户端发出的inputstream
           InputStream inputStream=socket.getInputStream();
           System.out.println("执行客户请求:"+Thread.currentThread());
           System.out.println("收到客户请求");
           //读取inputstream的内容
           BufferedReader reader=new BufferedReader(new InputStreamReader(inputStream,"utf-8"));
           String msg=null;
           while((msg=reader.readLine())!=null){
                if(msg.length()==0) break;
                System.out.println(msg);
           }
           //返回outputstream,主体内容是OK
           String resp="OK";
           OutputStream outputStream=socket.getOutputStream();
           System.out.println(resp);
           outputStream.write(resp.getBytes());
           outputStream.flush();
           outputStream.close();
           socket.close();
        }
    }
}

上面的代码把客户端和服务器的通信简单了模拟了一遍,我们可以执行一遍,运行项目后,在浏览器中输入http://localhost:8080/hello,观察控制台信息,收到了客户的请求,这里的信息正是http请求协议的内容。

但是我们会发现客户端出现报错,为什么我们返回了一个OK过去,但是客户端没有显示呢?原因就在于客户端只能识别符合HTTP响应协议的数据,我们必须把outputstream的数据让客户端能看懂,其实也很简单,只需要把返回的数据加上HTTP响应协议的报文头部就行,即我们上面复习的HTTP协议,新建一个response类,封装请求信息:

public class Response {
    public OutputStream outputStream;

    public static final String responsebody="HTTP/1.1 200+\r\n"+"Content-Type:text/html+\r\n"
            +"\r\n";
    public Response(OutputStream outputStream){
        this.outputStream=outputStream;
    }
}

 

Response类定义了responsebody,包括了http响应协议的头部信息,修改前面的代码

将第22行的
 String resp="OK";
 修改为
 String resp= Response.responsebody+"OK";

再次执行代码,客户端出现了我们自定义的“OK”

使用BIO模型解决只能连接一次的弊端

上面的代码虽然做到了服务器和客户端的通信,但是有个弊端,服务器一次只能连接一个客户端。tomcat在解决这个问题时使用了BIO模型,简单来讲就是每个连接一个线程,下面就来实现BIO:

public class TomcatServerV2 {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket=new ServerSocket(8080);
        System.out.println("====服务启动====");
        while(!serverSocket.isClosed()){
            Socket socket=serverSocket.accept();
            //对于每个连接,都开启一个线程
            RequestHandler requestHandler=new RequestHandler(socket);
            new Thread(requestHandler).start();

        }
    }
}
public class RequestHandler implements Runnable{
    public Socket socket;
    public RequestHandler(Socket socket)
    {
        this.socket=socket;
    }
    //继承Runnable接口,实现run方法
    public void run() {
        InputStream inputStream=null;
        try{
            inputStream=socket.getInputStream();
            System.out.println("执行客户请求"+Thread.currentThread());
            System.out.println("====收到客户端请求====");
            BufferedReader reader=new BufferedReader(new InputStreamReader(inputStream,"utf-8"));
            String msg=null;
            while((msg=reader.readLine())!=null){
                if(msg.length()==0){
                    break;
                }
                System.out.println(msg);
            }
            String resp= Response.responsebody + "OK";
            OutputStream outputStream=socket.getOutputStream();
            System.out.println(resp);
            outputStream.write(resp.getBytes());
            outputStream.flush();
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(socket!=null){
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

上述的代码和第一版本没有太多差别,但是通过几句代码的添加实现了每个连接给一个线程。

三:手写Tomcat

使用maven来管理项目,源码在文末,项目结构如下:

在pom.xml中添加两个jar包的依赖:

<dependencies>
  <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>servlet-api</artifactId>
    <version>2.5</version>
  </dependency>
  <dependency>
    <groupId>dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>1.1</version>
  </dependency>

我们之前对Response响应进行了封装,同样对于Request也需要封装:

public class Request {
    //获取uri,如 /user
    private String uri;
    //获取请求方法,这里只写get和post GET or POST
    private String method;
    public Request(InputStream inputStream){
        try {
            //获取inputStream
            BufferedReader read=new BufferedReader(new InputStreamReader(inputStream,"utf-8"));
           //取HTTP请求响应的第一行,GET /user HTTP/1.1,按空格隔开
           String[] data=read.readLine().split(" ");
           //取uri和method
           this.uri=data[1];
           this.method=data[0];
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
//省略uri和method的getter和setter方法
}

tomcat使用servlet进行请求处理,我们平常使用servlet时,会继承HttpServlet这个抽象类,然后重写里面的doGet,doPost等方法,最源头还会继承一个Servlet接口,该接口主要提供init,service等方法,我们可以看Servlet接口的代码

我们也用同样的方式来实现,首先建一个MyServlet接口,然后创建一个抽象类MyHttpServlet继承接口,最后建一个UserServlet实现具体的doGet,doPost等方法

public interface MyServlet {
    void init() throws Exception;
    void service(Request request, Response response) throws Exception;
    void destory();
}
public abstract class MyHttpServlet implements MyServlet {

    //如果有请求过来,就会调用这个方法,然后再根据请求类型来调用不同的doXXX()方法
    public void service(Request request, Response response) throws Exception {
        if("get".equalsIgnoreCase(request.getMethod())){
            this.doGet(request,response);
        }else {
            this.doPost(request,response);
        }
    }
    public abstract void doGet(Request request,Response response);
    public abstract void doPost(Request request,Response response);
}
public class UserServlet extends MyHttpServlet {
    @Override
    public void doGet(Request request, Response response) {
        this.doPost(request,response);
    }
    @Override
    public void doPost(Request request, Response response) {
        try {
            //省略业务调用的代码,tomcat会根据request对象里面的inputStream拿到对应的参数进行业务调用
            //模拟业务层调用后的返回
            OutputStream outputStream=response.outputStream;
            String result=Response.responsebody+"user handle successful";
            outputStream.write(result.getBytes());
            outputStream.flush();
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public void init() throws Exception { }
    public void destory() { }

}

UserServlet中原本应该还需要写对业务代码的调用,但是这次项目因为模拟简单的Tomcat实现就不写业务调用了。

Servlet写完后还需要做很重要的一步,解析web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4"
         xmlns="http://java.sun.com/xml/ns/j2ee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <display-name>Archetype Created Web Application</display-name>
    <servlet>
        <servlet-name>userServlet</servlet-name>
        <servlet-class>com.sdxb.servlet.UserServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>userServlet</servlet-name>
        <url-pattern>/user</url-pattern>
    </servlet-mapping>
</web-app>

这是我为这个项目写的web.xml,包含了两个映射,第一个是ServletName和Servlet实现类的映射,第二个是ServletName和uri的映射,意思就是如果检测到uri是/user,就能映射给UserServlet类。解析web.xml用的是最初导入的dom4j,在socket包下建一个MyTomcat类:

public class MyTomcat {
    //自定义端口号为1314
    public static final int port =1314;
    //定义web.xml中的两个映射
    public static final HashMap<String, MyHttpServlet> servletMapping=new HashMap<String, MyHttpServlet>();
    public static final HashMap<String,String> urlmapping=new HashMap<String, String>();
    
    public static void main(String[] args){
        MyTomcat myTomcat=new MyTomcat();
        myTomcat.init();
        myTomcat.run();
    }
    //初始化,加载web.xml里面配置的servlet信息
    private void init(){
        try {
            //获取web.xml目录地址
            String path=MyTomcat.class.getResource("/").getPath();
            //实例化SAXReader对象
            SAXReader reader=new SAXReader();
            //读取web.xml文件
            Document document=reader.read(new File(path+"web.xml"));
            //获取根标签(servlet和servlet-mapping),放在一个List中
            Element rootelement=document.getRootElement();
            List<Element> elements=rootelement.elements();
            //循环将映射写进map映射里
            for(Element element:elements){
                if ("servlet".equalsIgnoreCase(element.getName())){
                    Element servletname=element.element("servlet-name");
                    Element servletclass=element.element("servlet-class");
                    System.out.println(servletname.getText()+"==>"+servletclass.getText());
                    //需要注意的是servletMapping映射的第二个参数,要通过反射的方式进行实例化
                    servletMapping.put(servletname.getText(),
                            (MyHttpServlet) Class.forName(servletclass.getText().trim()).newInstance());
                }else if ("servlet-mapping".equalsIgnoreCase(element.getName())){
                    Element servletname=element.element("servlet-name");
                    Element urlpattern=element.element("url-pattern");
                    System.out.println(servletname.getText()+"==>"+urlpattern.getText());
                    urlmapping.put(urlpattern.getText(),servletname.getText());
                }
            }
        } catch (DocumentException e) {
            e.printStackTrace();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }
    //负责启动容器
    private void run(){
        ServerSocket serverSocket= null;
        try {
            serverSocket = new ServerSocket(port);
            System.out.println("====服务启动====");
            while(!serverSocket.isClosed()){
                Socket socket=serverSocket.accept();
                RequestHandler requestHandler=new RequestHandler(socket);
                new Thread(requestHandler).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

线程处理类也和最初的demo类似,不同的是这里需要根据不同的映射结果返回不同的数据给客户端

public class RequestHandler implements Runnable{
    public Socket socket;
    public RequestHandler(Socket socket)
    {
        this.socket=socket;
    }

    public void run() {
        try{
            //将inputstream封装成我们自己的request,用来获取uri,method等信息
            Request request=new Request(socket.getInputStream());
            //将outputstream封装成我们的response对象
            Response response=new Response(socket.getOutputStream());
            String uri=request.getUri();
            System.out.println(uri);
            //根据uri得到servletname
            String servletname=MyTomcat.urlmapping.get(uri);
            //根据servletname得到Servlet对象,如果web.xml文件中有映射就不为空
            MyHttpServlet servlet= MyTomcat.servletMapping.get(servletname);
            if(servlet!=null){
                //不为空执行service方法,即跳转到doGet和doPost方法
                servlet.service(request,response);
            }else{
                String resp=Response.responsebody+"can not find servlet";
                OutputStream outputStream=socket.getOutputStream();
                System.out.println(resp);
                outputStream.write(resp.getBytes());
                outputStream.flush();
                outputStream.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if(socket!=null){
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

至此,Tomcat运行流程就写完了,最好的学习办法是自己敲一遍代码,并理解代码每一步在干什么,下一步去到了哪里。运行结果如下:

最后提供github源码:github源码

发布了54 篇原创文章 · 获赞 604 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_41973594/article/details/102712793