Handwritten Tomcat based on traditional I/O

This article is excerpted from Netty 4 core principles

As we know, Tomcat is a Web container based on J2EE specification, and the main entry is Web XML file. Web. Servlet, Filter, Listener, etc. are mainly configured in the XML file, while servlet, Filter and Listener are only abstract implementations in J2EE, and the specific business logic is implemented by the developer. This chapter takes the most commonly used servlet as an example to expand in detail.

1 Environmental preparation

1.1 define GPServlet abstract class

First, we create the GPServlet class. As we all know, the most commonly used methods in the life cycle of GPServlet are doGet() method and doPost() method, while doGet() method and doPost() method are branch implementations of service() method. See the following simple Servlet source code implementation.

package com.tom.tomcat.http;

public abstract class GPServlet {

    public void service(GPRequest request,GPResponse response) throws Exception{

        //The service() method determines whether to call doGet() or doPost()
        if("GET".equalsIgnoreCase(request.getMethod())){
            doGet(request, response);
        }else{
            doPost(request, response);
        }

    }

    public abstract void doGet(GPRequest request,GPResponse response) throws Exception;

    public abstract void doPost(GPRequest request,GPResponse response) throws Exception;

}

From the above code, we can see that the doGet() method and doPost() method have two parameters GPRequest and GPResponse objects. These two objects are created by the Web container and are mainly used to encapsulate the Input and Output of the underlying Socket. GPRequest is the encapsulation of Input and GPResponse is the encapsulation of Output.

1.2 create user business code

The following two business logic FirstServlet and SecondServlet are implemented based on GPServlet. The implementation code of FirstServlet class is as follows.

package com.tom.tomcat.servlet;

import com.tom.tomcat.http.GPRequest;
import com.tom.tomcat.http.GPResponse;
import com.tom.tomcat.http.GPServlet;

public class FirstServlet extends GPServlet {

    public void doGet(GPRequest request, GPResponse response) throws Exception {
        this.doPost(request, response);
    }

    public void doPost(GPRequest request, GPResponse response) throws Exception {
        response.write("This is First Servlet");
    }

}
SecondServlet The implementation code of class is as follows.
package com.tom.tomcat.servlet;

import com.tom.tomcat.http.GPRequest;
import com.tom.tomcat.http.GPResponse;
import com.tom.tomcat.http.GPServlet;

public class SecondServlet extends GPServlet {

    public void doGet(GPRequest request, GPResponse response) throws Exception {
        this.doPost(request, response);
    }

    public void doPost(GPRequest request, GPResponse response) throws Exception {
        response.write("This is Second Servlet");
    }

}

1.3 complete web Properties configuration

To simplify the operation, we use web Properties file instead of web XML file, the specific contents are as follows.

servlet.one.url=/firstServlet.do
servlet.one.className=com.tom.tomcat.servlet.FirstServlet

servlet.two.url=/secondServlet.do
servlet.two.className=com.tom.tomcat.servlet.SecondServlet

The above code configures / firstservlet for two servlets respectively Do and / secondservlet URL mapping for do.

2 handwriting Tomcat based on traditional I/O

Let's look at the basic implementation of GPRequest and GPResponse.

2.1 create GPRequest object

GPRequest is mainly used to parse the HTTP request header information. We send an HTTP request from the browser, such as entering in the browser address bar http://localhost:8080 , the request obtained by the background server is actually a string. The specific format is as follows.

Example:

GET / HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 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

After GPRequest obtains the input content, it parses the string of character information that meets HTTP. Let's look at the simple and direct code implementation of GPRequest.

package com.tom.tomcat.http;

import java.io.InputStream;

/**
 * Created by Tom.
 */
public class GPRequest {

    private String method;
    private String url;

    public GPRequest(InputStream in){
        try {
            //Get HTTP content
            String content = "";
            byte[] buff = new byte[1024];
            int len = 0;
            if ((len = in.read(buff)) > 0) {
                content = new String(buff,0,len);
            }

            String line = content.split("\\n")[0];
            String [] arr = line.split("\\s");

            this.method = arr[0];
            this.url = arr[1].split("\\?")[0];
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public String getUrl() {
        return url;
    }

    public String getMethod() {
        return method;
    }
}

In the above code, GPRequest mainly provides getUrl() method and getMethod() method. The input stream InputStream is passed in as the construction parameter of GPRequest. In the constructor, the request method and URL are extracted by string cutting.

2.2 create GPResponse object

Next, let's look at the implementation of GPResponse, which is similar to the implementation idea of GPRequest. It is to Output formatted strings from Output according to HTTP specification. The specific format is as follows:

Example:

HTTP/1.1 200 OK
Server: Tomcat
Location: http://localhost:8080
Connection: Keep-Alive
Content-Type: text/html;

This My Servlet

Let's look at the code.

package com.tom.tomcat.http;

import java.io.OutputStream;

/**
 * Created by Tom.
 */
public class GPResponse {
    private OutputStream out;
    public GPResponse(OutputStream out){
        this.out = out;
    }

    public void write(String s) throws Exception {
        //The output should also follow HTTP
        //The status code is 200
        StringBuilder sb = new StringBuilder();
                sb.append("HTTP/1.1 200 OK\n")
                .append("Content-Type: text/html;\n")
                .append("\r\n")
                .append(s);
        out.write(sb.toString().getBytes());
    }
}

In the above code, the output stream OutputStream is passed in as the construction parameter of GPResponse, mainly providing a write() method. Output the string according to the HTTP specification through the write() method.

2.3 create GPTomcat startup class

The first two sections 2.1 and 2.2 are just the reproduction of the J2EE specification. The next is the implementation logic of the real Web container, which is divided into three stages: initialization stage, service readiness stage and request acceptance stage. The first stage: initialization stage, mainly to complete the Web Parsing of XML files.

package com.tom.tomcat;

import com.tom.tomcat.http.GPRequest;
import com.tom.tomcat.http.GPResponse;
import com.tom.tomcat.http.GPServlet;

import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 * Created by Tom.
 */
public class GPTomcat {
    private int port = 8080;
    private ServerSocket server;
    private Map<String,GPServlet> servletMapping = new HashMap<String,GPServlet>();

    private Properties webxml = new Properties();


    private void init(){

        //Load web XML file and initialize the ServletMapping object at the same time
        try{
            String WEB_INF = this.getClass().getResource("/").getPath();
            FileInputStream fis = new FileInputStream(WEB_INF + "web.properties");

            webxml.load(fis);

            for (Object k : webxml.keySet()) {

                String key = k.toString();
                if(key.endsWith(".url")){
                    String servletName = key.replaceAll("\\.url$", "");
                    String url = webxml.getProperty(key);
                    String className = webxml.getProperty(servletName + ".className");
                    //Single instance, multithreading
                    GPServlet obj = (GPServlet)Class.forName(className).newInstance();
                    servletMapping.put(url, obj);
                }

            }


        }catch(Exception e){
            e.printStackTrace();
        }

    }

}

In the above code, first read the web from WEB-INF Properties file and parse it, and then save the corresponding relationship between URL rules and GPServlet to servletMapping. The second stage: service readiness stage, which completes the preparation of ServerSocket. Add the start() method in GPTomcat class.

    public void start(){

        //1. Load the configuration file and initialize ServletMapping
        init();

        try {
            server = new ServerSocket(this.port);

            System.out.println("GPTomcat Started. The listening port is:" + this.port);

            //2. Wait for the user's request, and wait for the user's request with an endless loop
            while (true) {
                Socket client = server.accept();
                //3. For HTTP requests, the data sent is a string - a regular string (HTTP)
                process(client);

            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

The third stage: accept the request stage, and complete the processing of each request. Add the implementation of process() method in GPTomcat.

    private void process(Socket client) throws Exception {

        InputStream is = client.getInputStream();
        OutputStream os = client.getOutputStream();

        //4.Request(InputStrean)/Response(OutputStrean)
        GPRequest request = new GPRequest(is);
        GPResponse response = new GPResponse(os);

        //5. Obtain the URL from the protocol content and instantiate the corresponding Servlet with reflection
        String url = request.getUrl();

        if(servletMapping.containsKey(url)){
            //6. Call the service() method of the instantiated object and execute the specific logical doGet()/doPost() method
            servletMapping.get(url).service(request,response);
        }else{
            response.write("404 - Not Found");
        }


        os.flush();
        os.close();

        is.close();
        client.close();
    }

After each client request, get its corresponding Servlet object from servletMapping, instantiate GPRequest and GPResponse objects at the same time, pass GPRequest and GPResponse objects into service() method as parameters, and finally execute business logic. Finally, add the main() method.

     public static void main(String[] args) {
        new GPTomcat().start();
    }

After the service is started, the operation effect is shown in the figure below.

Added by despyz on Fri, 11 Feb 2022 05:28:30 +0200