preface

I recently read How Tomcat Works in Depth and found it easy to read, and every single one

Chapter 1 explains the principle of Tomcat step by step. In the following chapters, Tomcat is improved based on the new functions in chapter 1.

The result is a simplified version of Tomcat. So if you are interested, please read it in order. This article is to record the knowledge points of chapter 3

And source code implementation (make wheels).

Copy the code

Review the content

In the previous chapter, we implemented a simple Servlet container that calls and executes user-defined classes that implement the Servlet interface.

This chapter content

  • The module mimics Tomcat and implements the Connector, Bootstrap, and core modules.
  • The ability to execute custom servlets that inherit HttpServlet classes (the previous chapter implemented the Servlet interface)
  • Ability to parse user request Parameters/cookies/request headers

Prior to the start

  • A brief introduction to Connector

    The connector simply receives the request and then dumps it to the Container to execute the request.

  • Javax.mail. Servlet. HTTP. The HttpServlet class

    In the previous chapter, our custom Servlet implemented the Servlet interface. Instantiating Servlet is when we will parse Request/Response (respectively the ServletRequest/ServletResponse interface) was introduced into the corresponding service () method of execution. GenericServlet, HttpServlet, GenericServlet, GenericServlet, GenericServlet, GenericServlet, GenericServlet We can override the doGet()/doPost() method and see if Tomcat supports servlets that inherit from the HttpServlet class:

// Snippets of HttpServlet source code

public abstract class HttpServlet extends GenericServlet {

.

public void service(ServletRequest req, ServletResponse res)

throws ServletException, IOException
{

HttpServletRequest request;

HttpServletResponse response;

// If the request/ Response object is not Http, an exception is thrown

if(! (reqinstanceof HttpServletRequest &&

res instanceof HttpServletResponse)) {

throw new ServletException("non-HTTP request or response");

}



request = (HttpServletRequest) req;

response = (HttpServletResponse) res;



service(request, response);

}

.

}

Copy the code

Take a look at the source code for the ServletProcess invocation Servlet in the previous chapter:

servlet.service(new RequestFacade(request), new ResponseFacade(response));

Copy the code

Obviously chapter request/response in HttpServlet will throw an exception, so in this chapter we will request/response as well as their appearance classes implement it/the HttpServletResponse interface.

Code implementation

Before the code implementation, let’s have a look at the overall module and process execution diagram (click to enlarge if you can’t see clearly) :

1. The Bootstrap module

We don’t have much work to do at the moment, just start HttpConnector:

 public final class Bootstrap {

public static void main(String[] args){

new HttpConnector().start();

}

}

Copy the code

2. HttpConnector module (connector)

The connector module and the core module below are the predecessors of the HttpServer class from the previous chapter, which we split functionally

Wait and establish connection (HttpConnector)/ HttpProcess two modules.

Copy the code

The connector waits for a request and throws it to the appropriate actuator to execute:

public class HttpConnector implements Runnable {

public void start(a){

new Thread(this).start();

}

@Override

public void run(a) {

ServerSocket serverSocket = new ServerSocket(8080.1, InetAddress.getByName("127.0.0.1"));

while (true) {

Socket accept = serverSocket.accept();

HttpProcess process = new HttpProcess(this);

process.process(accept);

}

serverSocket.close();

}

}

Copy the code

3. Core module (actuator)

As mentioned above, the executor is also the predecessor of the HttpServer class in the previous chapter, except that this chapter has changed the way the request information is parsed.

Copy the code
  • The main code
public class HttpProcess {

private HttpRequest request;

private HttpResponse response;



public void process(Socket socket) {

InputStream input = socket.getInputStream();

OutputStream output = socket.getOutputStream();

// Initialize request and response

request = new HttpRequest(input);

response = new HttpResponse(output, request);

// Parse the request lines and headers

this.parseRequestLine(input);

this.parseHeaders(input);

// Call the corresponding handler

if (request.getRequestURI().startsWith(SERVLET_URI_START_WITH)) {

new ServletProcess().process(request, response);

} else {

new StaticResourceProcess().process(request, response);

}

}

}

Copy the code

See the above implementation may be a lot of people to some objects a little strange, the following one introduction:

1. The HttpRequest/HttpResponse variable is the Request/Response object from the previous chapter because it is implemented

HttpServletReuqest/HttpServletResponse also change a name, by the way, will be described below;

Each request corresponds to an HttpProcess object, so request/ Response is a member variable;

3. Methods for parsing request lines and request headers are also described below.

Copy the code

  • Equestline and parseHeaders methods

    Let’s look at a raw HTTP request string and see how to parse the request line and request header:

    GET /index.html? utm_source=aaa HTTP/1.1\r\n

    Host: www.baidu.com\r\n

    Connection: keep-alive\r\n

    Pragma: no-cache\r\n

    Cache-Control: no-cache\r\n

    Upgrade-Insecure-Requests: 1\r\n

    User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0Safari 2924.87 /537.36\r\n

    Accept: text/html\r\n

    Accept-Encoding: gzip, deflate, sdch, br\r\n

    Accept-Language: zh-CN,zh; q=0.8\r\n

    Cookie: BAIDUID=462A9AC35EE6158AA7DFCD27AF:FG=1; BIDUPSID=462A9AC35EE6158AA7DF027AF; PSTM=1506310304; BD_CK_SAM=1; PSINO=7; BD_HOME=1; H_PS_PSSID=1459 _24885_21115_25436; BD_UPN=12314353; sug=3; sugstore=0; ORIGIN=2; bdime=0\r\n

    Copy the code

    If the socket is used to read the HTTP request, each line will have the ‘\r\n’ return newline character, but when the browser presses F12, it will be automatically interpreted as a newline character. We analyze the above request information and obtain the following rules:

    - Each line ends with \r\n

    - The request line (the first line) contains a space between the HTTP request method, URI, and request protocol

    - Line 2 begins (request header) key andvalueAre based on':'And a' 'characters

    - The key-value pair of Cookie is'='Break up, to'; 'and' 'Distinguish between before and after key-value pairs

    Copy the code

    Next, we parse the values of the above characters in the following ISO-8859-1 encoding, and create a constant class:

    public class HttpConstant {

    /* enter \r */

    public static final byte CARRIAGE_RETURN = 13;

    /* newline \n */

    public static final byte LINE_FEED = 10;

    * / / * Spaces

    public static final byte SPACE = 32;

    /* Colon: */

    public static final byte COLON = 58;

    }

    Copy the code
  • 1. ParseRequestLine method

    With this in mind, we can easily parse the data in the request row:

    StringBuilder temp = new StringBuilder();

    int cache;

    while((cache = requestStream.read()) ! = -1) {

    // if the first item \r\n is read, the request line has been read

    if (HttpConstant.CARRIAGE_RETURN == cache && HttpConstant.LINE_FEED == requestStream.read()) {

    break;

    }

    temp.append((char)cache);

    }

    String[] requestLineArray = temp.toString().split("");

    Copy the code

    The final split space uses numbers to assemble the request line (if you have a better idea, let me know in the comments section).



Next check if the URI uses “?” Pass parameters, if any, to the QueryString variable of HttpRequest,

Finally, intercept the URI.

String uri  = requestLineArray[1];

int question = uri.indexOf("?");

if (question >= 0) {

request.setQueryString(uri.substring(question+1,uri.length()));

uri = uri.substring(0,question);

}

Copy the code
Is it from? Pass the JsessionID, if so, to the Request object

Copy the code
String match = "; jsessionid=";

int semicolon = uri.indexOf(match);

if (semicolon >= 0) {

String rest = uri.substring(semicolon + match.length());

int semicolon2 = rest.indexOf('; ');

if (semicolon2 >= 0) {

request.setRequestedSessionId(rest.substring(0, semicolon2));

rest = rest.substring(semicolon2);

} else {

request.setRequestedSessionId(rest);

rest = "";

}

request.setRequestedSessionURL(true);

uri = uri.substring(0, semicolon) + rest;

} else {

request.setRequestedSessionId(null);

request.setRequestedSessionURL(false);

}

Copy the code

A method is called to validate the URI, throwing an exception if the URI is illegal (for example, if it contains dangerous characters that jump to directories like ‘.//’), or else throwing the above parsed content into the request.

String normalizedUri = this.normalize(uri);

if (normalizedUri == null) {

throw new ServletException("Invalid URI: " + uri + "'");

}

request.setRequestURI(normalizedUri);

request.setMethod(requestLineArray[0]);

request.setProtocol(requestLineArray[2]);

Copy the code

Now that we have read the information from the request line, let’s look at the code that reads the request header:

  • ParseHeaders method

    Here’s a pit: When the Socket’s read() method finishes reading, the last byte is not -1. Instead, it blocks and waits for the Socket client to send -1 to finish reading. So we use the InputStream#available() method (which returns the number of bytes that can actually be read) to determine if the read is complete.

public void parseHeader(a) {

StringBuilder sb = new StringBuilder();

int cache;

while (input.available() > 0 && (cache = input.read()) > -1) {

sb.append((char)cache);

}

. See below

}

Copy the code

After reading, the effect is shown as follows:



In the case of a POST request, the form argument follows the blank line:

Also regularly, request headers are separated by \r\n, and if a POST request is submitted, the form parameters are followed by a blank line (two \r\n).

// Split the request header with \r\n

Queue<String> headers = Stream.of(sb.toString().split("\r\n")).collect(toCollection(LinkedList::new));

while(! headers.isEmpty()) {

// Get a request header

String headerString = headers.poll();

// The request header has been read

if (StringUtil.isBlank(headerString)) {

break;

}

// Split the key and value of the request header

String[] headerKeyValue = headerString.split(":");

request.addHeader(headerKeyValue[0], headerKeyValue[1]);

}

// If there is data after a blank line is read, it is the form parameter of the POST request

if(! headers.isEmpty()){

request.setPostParams(headers.poll());

}

Copy the code

General process:



Finally, we set some special Request headers to the Request object (cookie, Content-Type, content-Length).

String contentLength = request.getHeader("content-length");

if(contentLength! =null) {

request.setContentLength(Integer.parseInt(contentLength));

}

request.setContentType(request.getHeader("content-type"));



Cookie[] cookies = parseCookieHeader( request.getHeader("cookie"));

Stream.of(cookies).forEach(cookie -> request.addCookie(cookie));

// If the sessionID is not obtained from a cookie, the sessionID in the cookie is preferred

if(! request.isRequestedSessionIdFromCookie()) {

Stream.of(cookies)

.filter(cookie -> "jsessionid".equals(cookie.getName()))

.findFirst().

ifPresent(cookie -> {

// Set the cookie value

request.setRequestedSessionId(cookie.getValue());

request.setRequestedSessionCookie(true);

request.setRequestedSessionURL(false);

});

}

Copy the code

The method of reading cookies is also simple:

private Cookie[] parseCookieHeader(String cookieListString) {

return Stream.of(cookieListString.split("; "))

.map(cookieStr -> {

String[] cookieArray = cookieStr.split("=");

return new Cookie(cookieArray[0], cookieArray[1]);

}).toArray(Cookie[]::new);

}

Copy the code

If you are not familiar with the syntax of JDK8, you may not understand what you are doing.



HttpProcess can handle requests with multiple attributes. HttpProcess can handle requests with multiple attributes. The request from the previous chapter wasn’t that versatile, was it? Yes, we’ve also tweaked request/ Response in this chapter, see below:

  • HttpRequest(Request object from the previous chapter)

    Yes, at the beginning of this article we said we were going to upgrade Request, so how? Implement the HttpServletRequest interface:

    public class HttpRequest implements HttpServletRequest {

    private String contentType;

    private int contentLength;

    private InputStream input;

    private String method;

    private String protocol;

    private String queryString;

    private String postParams;

    private String requestURI;

    private boolean requestedSessionCookie;

    private String requestedSessionId;

    private boolean requestedSessionURL;

    protected ArrayList<Cookie> cookies = new ArrayList<>();

    protected HashMap<String, ArrayList<String>> headers = new HashMap<>();

    protected ParameterMap parameters;

    .

    }

    Copy the code

    ParameterMap (get, set) {ParameterMap (get, set) {ParameterMap (get, set) {ParameterMap (get, set) {ParameterMap (get, set) {ParameterMap (get, set)}}

    Request header operations:

    public void addHeader(String name, String value) {

    name = name.toLowerCase();

    // If the value corresponding to the key does not exist, new an ArrayList

    ArrayList<String> values = headers.computeIfAbsent(name, k -> new ArrayList<>());

    values.add(value);

    }

    public ArrayList getHeaders(String name) {

    name = name.toLowerCase();

    return headers.get(name);

    }

    public String getHeader(String name) {

    name = name.toLowerCase();

    ArrayList<String> values = headers.get(name);

    if(values ! =null) {

    return values.get(0);

    } else {

    return null;

    }

    }

    public ArrayList getHeaderNames(a) {

    return new ArrayList(headers.keySet());

    }

    Copy the code

    As you can see, the request header is a Map, the key is the name of the request header, and the value is an array of the contents of the request header

    Cookie:

     public Cookie[] getCookies() {

    return cookies.toArray(new Cookie[cookies.size()]);

    }

    public void addCookie(Cookie cookie) {

    cookies.add(cookie);

    }

    Copy the code

    As if there is nothing to say, do the usual operation on List\.

    ParameterMap ParameterMap ParameterMap ParameterMap ParameterMap ParameterMap ParameterMap ParameterMap ParameterMap ParameterMap

    public final class ParameterMap extends HashMap<String.String[] >{

    private boolean locked = false;

    public boolean isLocked(a) {

    return locked;

    }

    public void setLocked(boolean locked) {

    this.locked = locked;

    }

    public String[] put(String key, String[] value) {

    if (locked) {

    throw new IllegalStateException("error");

    }

    return (super.put(key, value));

    }

    .

    }

    Copy the code

    The key is the parameter name, and the value is an array of parameter values. For example: 127.0.0.1:8080 / servlet/QueryServlet? name=geoffrey&name=yip

    So let’s look at the operation on the parameter map:

    public String getParameter(String name) {

    parseParameters();

    String[] values = parameters.get(name);

    return Optional.ofNullable(values).map(arr -> arr[0]).orElse(null);

    }

    public Map getParameterMap(a) {

    parseParameters();

    return this.parameters;

    }

    public ArrayList<String> getParameterNames(a) {

    parseParameters();

    return new ArrayList<>(parameters.keySet());

    }

    public String[] getParameterValues(String name) {

    parseParameters();

    return parameters.get(name);

    }

    Copy the code

    The code is simple, but what is parseParameters()? Yes, it parses the parameters of the request, because we don’t know if the user will use the request parameters with the Servlet, and parsing it is more expensive than other data. Therefore, we will parse the parameters only when the user really uses them to improve the overall response speed. The approximate code is as follows:

    protected void parseParameters(a) {

    if (parsed) {

    // Stop parsing if it has been parsed

    return;

    }

    ParameterMap results = parameters;

    if (results == null) {

    results = new ParameterMap();

    }

    results.setLocked(false);

    String encoding = getCharacterEncoding();

    if (encoding == null) {

    encoding = StringUtil.ISO_8859_1;

    }

    // Parse the request parameters carried by the URI

    String queryString = getQueryString();

    this.parseParameters(results, queryString, encoding);

    // Initializes the content-type value

    String contentType = getContentType();

    if (contentType == null) {

    contentType = "";

    }

    int semicolon = contentType.indexOf('; ');

    if (semicolon >= 0) {

    contentType = contentType.substring(0, semicolon).trim();

    } else {

    contentType = contentType.trim();

    }

    // Parse the form parameters for the POST request

    if (HTTPMethodEnum.POST.name().equals(getMethod()) && getContentLength() > 0

    && "application/x-www-form-urlencoded".equals(contentType)) {

    this.parseParameters(results, getPostParams(), encoding);

    }

    // Lock after parsing

    results.setLocked(true);

    parsed = true;

    parameters = results;

    }

    / * *

    * Parse the request parameters

    * @paramMap Map of parameters in the Request object

    * @paramParams Parameter before parsing

    * @paramEncoding encoding

    * /


    public void parseParameters(ParameterMap map, String params, String encoding) {

    String[] paramArray = params.split("&");

    Stream.of(paramArray).forEach(param -> {

    String[] splitParam = param.split("=");

    String name = splitParam[0];

    String value = splitParam[1];

    // Decode the key and value using URLDecode and add them to the map

    putMapEntry(map, urlDecode(name, encoding), urlDecode(value, encoding));

    });

    }

    Copy the code

    The outline is to put the queryString parameters of the request row and the form data if it is a POST request into ParameterMap and lock the Map based on the previous HttpProcess resolution.

  • HttpResponse(Response object from the previous chapter)

    The HttpResponse object also implements the HttpServletResponse interface, but the details are not implemented in this chapter, so we’ll skip it here.

    public class HttpResponse implements HttpServletResponse {

    .

    }

    Copy the code
  • ServletProcess

The ServletProcess needs to update the request and Response appearance classes to implement the corresponding interfaces:

public void process(HttpRequest request, HttpResponse response) throws IOException {

.

servlet.service(new HttpRequestFacade(request), new HttpResponseFacade(response));

.

}

public class HttpRequestFacade implements HttpServletRequest {

private HttpRequest request;

.

}



public class HttpResponseFacade implements HttpServletResponse {

private HttpResponse response;

.

}

Copy the code

The experiment

Let’s start by writing a Servlet:

/ * *

* Test the registration Servlet

* /


public class RegisterServlet extends HttpServlet {

@Override

public void doGet(HttpServletRequest req, HttpServletResponse resp) {

// Print the form parameters

String name = req.getParameter("name");

String password = req.getParameter("password");

if (StringUtil.isBlank(name) || StringUtil.isBlank(password)) {

try {

resp.getWriter().println("Account/password cannot be empty!");

} finally {

return;

}

}

// Prints the request line

System.out.println("Parse user register method:" + req.getMethod());

/ / print the Cookie

System.out.println("Parse user register cookies:");

Optional.ofNullable(req.getCookies())

.ifPresent(cookies ->

Stream.of(cookies)

.forEach(cookie ->System.out.println(cookie.getName() + ":" + cookie.getValue()

)));

// Prints the request header

System.out.println("Parse http headers:");

Enumeration<String> headerNames = req.getHeaderNames();

while (headerNames.hasMoreElements()) {

String headerName = headerNames.nextElement();

System.out.println(headerName + ":" + req.getHeader(headerName));

}

System.out.println("Parse User register name :" + name);

System.out.println("Parse User register password :" + password);

try {

resp.getWriter().println("Registration successful!");

} finally {

return;

}

}

@Override

public void doPost(HttpServletRequest req, HttpServletResponse resp) {

this.doGet(req, resp);

}

}

Copy the code

Write an HTML:

<html>

<head>

<title>registered</title>

</head>

<body>

<form method="post" action="/servlet/RegisterServlet">

Account:<input type="text" name="name"><br>

Password:<input type="password" name="password"><br>

<input type="submit" value="Submit">

</form>

</body>

</html>

Copy the code

Open browser test:



Console output:

By now, Tomcat 3.0 Web server has been developed, which can implement simple custom Servlet calls, and request line/request header/request parameters /cookie information parsing. There are still a lot of areas to improve:

- HTTPProcess can process only one request at a time. Other requests are blocked and cannot be used by the server.

- Every time you asknewA Servlet, which should be initialized when the project is initialized, is singleton.

- Not following the Servlet specification to implement the corresponding lifecycle, such as init()/destory() methods we did not call.

- it/we still don't realize most of the HttpServletResponse interface method

- The architecture/package structure is too different from Tomcat

- Other unimplemented functions

Copy the code

PS: The source of this chapter has been uploaded to Github: SimpleTomcat