Tomcat原理之手写

目录

一、准备工作

1.1 Tomcat概要

1.2 技术储备

二、实现步骤

三、相关类的实现和介绍

3.1 包结构

3.2 Servlet配置类

3.3 Tomcat实现类

3.4 请求响应实现类Request/Response

3.5 工具类HttpUtil/FileUtil

3.6 Servlet抽象和测试实现类

四、测试运行

4.1 成功请求servlet(200)

4.2 错误请求servlet(5xx)

4.3 找不到请求servlet(404)

4.4 成功请求静态资源(200)

4.5 找不到请求静态资源(404)


一、准备工作

在准备工作这一部分中,将预热一下Tomcat的基本知识,进而罗列所需的基本技术储备。

1.1 Tomcat概要

Apache Tomcat作为目前广大Java技术爱好者使用最多、接触最早的轻量级Web应用服务器,至今流行依旧,也被SpringBoot内置其中。

作为一个Web应用资源容器(包括了静态Html或别的资源和servlet配置),它可以处理静态Html页面,同时也是servlet容器,用来处理动态Java服务请求;作为一款Java服务器,它的本质又可以理解为是一个ServerSocket,监听指定端口,接收浏览器发送过来的请求,本质也就是一个个socket,解析套接字输入流里的请求体内容,请求处理完毕后将响应信息,以标准的http协议规范的形式返回给浏览器(强调http协议规范,否则浏览器不认识)。

简单说,Tomcat可以被认为是Web资源容器+ServerSocket。

1.2 技术储备

socket套接字、IO、Http、servlet、xml。

二、实现步骤

根据使用Tomcat的经验,以及JavaWeb工程的配置方式等的分析,得出以下实现方案:

  • 初始化servlet,即一般从web.xml中加载servlet配置,存至Map,该对象就是servlet容器
  • 启动ServerSocket服务端,时刻准备接收浏览器请求
  • 当请求来临,根据http协议规范解析请求头信息,获取到url,判断静态或动态资源,即是否存在于urlServletClassMap中
  • 若存在于urlServletClassMap,则分发请求至对应的servlet,反射生成servlet实例,进入servlet的service生命周期阶段,调用doGet/doPost等,最后response.write输出内容(JSON、Xml或其他),service中途报错则response.write一个500页面
  • 若不存在于urlServletClassMap,则尝试请求静态资源,可以找到资源则response.write资源内容,反之response.write一个404页面

三、相关类的实现和介绍

3.1 包结构

Tomcat原理之手写_第1张图片

3.2 Servlet配置类

Tomcat作为servlet容器,在启动时会解析web.xml文件,并将servlet配置加载到服务器内存中。

3.2.1 servlet配置类ServletConfig

这些servlet在web.xml中的样式如下图。我们可以将一对servlet的配置抽象成配置类ServletConfig,它需要具有的属性也来自上图的三个节点。对应以下Java代码:

Tomcat原理之手写_第2张图片

3.2.2 servlet配置加载类ServletConfigMapping

定义好Servlet配置类后,我们需要在Tomcat启动后自动加载所有配置的ServletConfig,存到一个集合中。ServletConfigMapping代码如下(这里我们只add一个servlet用来测试):

package com.szh.tomcat.config;
import java.util.ArrayList;
import java.util.List;
/**
 * 解析并保存了web.xml里的很多对配置
 */
public class ServletConfigMapping {
	private static List configs = new ArrayList<>();
	static {
		configs.add(new ServletConfig("myServlet", "/testServlet", "com.szh.tomcat.servlet.MyServlet"));
	}
	public static List getConfigs() {
		return configs;
	}
}

3.3 Tomcat实现类

Tomcat作为一个socket服务端,需要占用一个网络端口,需要启动函数(启动脚本),需要servlet初始化方法,需要将请求分发至对应Servlet实现类,以下先贴出MyTomcat的代码:

package com.szh.tomcat;

import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.szh.tomcat.config.ServletConfig;
import com.szh.tomcat.config.ServletConfigMapping;
import com.szh.tomcat.servlet.Request;
import com.szh.tomcat.servlet.Response;
import com.szh.tomcat.servlet.Servlet;
import com.szh.tomcat.util.HttpUtil;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Data
public class MyTomcat {
	private int port = 8080;

	/**
	 * servlet配置的url-clazz对
	 */
	private Map> urlClassMap = new HashMap<>();

	public void start() throws Exception {
		initServlet();
		ServerSocket serverSocket = new ServerSocket(port);
		log.info("MyTomcat starting on {}..", port);
		while (true) {
			Socket socket = serverSocket.accept();
			Request request = new Request(socket.getInputStream());
			Response response = new Response(socket.getOutputStream());
			if (request.getUrl().equals("/")) {
				// 亦可为"/"配置一欢迎页,即web.xml中的
				response.write(HttpUtil.getHttpResponseContext404());
			} else if (urlClassMap.get(request.getUrl()) == null) {
				response.writeHtml(request.getUrl());
			} else {
				dispatch(request, response);
			}
			socket.close();
		}
	}

	/**
	 * servlet容器初始化:将web.xml中的servlet放进Map<url, servletClass>中
	 */
	public void initServlet() throws ClassNotFoundException {
		List configs = ServletConfigMapping.getConfigs();
		log.info("初始化servlet:{}", configs);
		for (ServletConfig servletConfig : configs) {
			urlClassMap.put(servletConfig.getUrlMapping(), (Class) Class.forName(servletConfig.getClazz()));
		}
	}

	/**
	 * 分发各servlet:myServlet去找com.szh.tomcat.servlet.MyServlet,
	 * myServlet2去找com.szh.tomcat.servlet.MyServlet2
	 */
	public void dispatch(Request request, Response response) throws InstantiationException, IllegalAccessException {
		Class servletClass = urlClassMap.get(request.getUrl());
		if (servletClass != null) {
			Servlet servlet = servletClass.newInstance();
			try {
				servlet.service(request, response);
			} catch (Throwable cause) {
				response.write(HttpUtil.getHttpResponseContext500(cause));
			}
		} else {
			response.write(HttpUtil.getHttpResponseContext404());
		}
	}

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

可以看出,MyTomcat主要有2个核心的函数,initServlet()dispatch(req, resp),分别用来初始化servlet容器和分发请求。另外,它还导入了工具类HttpUtil和自定义的请求Request、响应Response以及Servlet抽象类,当然你也可以尝试使用HttpRequest、HttpResponse、HttpServlet。接下来一个个介绍这几个类。

3.4 请求响应实现类Request/Response

3.4.1 请求实现类Request

客户端或浏览器发过来的请求,最原始就是一个从socket中获取到的InputStream,解析这个输入流中的内容后,即可获取到Request的属性:请求网址url和请求方法methodType。因此我们将Request抽象如下:

package com.szh.tomcat.servlet;

import java.io.IOException;
import java.io.InputStream;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Data
public class Request {

	private String url;
	private String methodType;
	private InputStream inputStream;

	public Request(InputStream inputStream) throws IOException {
		this.inputStream = inputStream;
		int count = 0;
		while (count == 0) {
			// available预估网络包长度,可能为0
			count = inputStream.available();
		}
		byte[] bytes = new byte[count];
		inputStream.read(bytes);
		log.info(new String(bytes, "utf-8"));
		extractFields(new String(bytes, "utf-8"));
	}

	/**
	 * 根据Http协议标准解析请求头信息,提取并封装到Request中
	 */
	private void extractFields(String content) {
		if ("".equals(content)) {
			log.info("empty");
		} else {
			String firstLine = content.split("\n")[0];
			String[] segments = firstLine.split("\\s");
			setUrl(segments[1]);
			setMethodType(segments[0]);
		}
	}

}

3.4.2 响应实现类Response

对Response而言,它的任务就是通过socket获取到的输出流,将处理后的响应内容写回给客户端,所以我们将Response抽象如下:

package com.szh.tomcat.servlet;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;

import com.szh.tomcat.util.FileUtil;
import com.szh.tomcat.util.HttpUtil;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class Response {

	private OutputStream outputStream;

	public Response(OutputStream outputStream) {
		super();
		this.outputStream = outputStream;
	}

	public void write(String content) {
		try {
			outputStream.write(content.getBytes("utf-8"));
		} catch (IOException e) {
			log.error(e.getMessage());
		}
	}

	public void writeHtml(String path) throws IOException {
		String resourcePath = FileUtil.getResourcePath(path);
		File file = new File(resourcePath);
		if (file.exists()) {
			FileUtil.writeFile(file, outputStream);
		} else {
			write(HttpUtil.getHttpResponseContext404());
		}
	}
}

3.5 工具类HttpUtil/FileUtil

3.5.1 Http工具类HttpUtil

HttpUtil工具类以Http协议标准返回各种返回码对应的响应串,如下:

package com.szh.tomcat.util;

/**
 * 以Http协议标准规范,构造响应信息体,相当于response.setContentType("text/html")
 */
public class HttpUtil {

	public static String getHttpResponseContext(int code, String content, String errorMsg) {
		if (code == 200) {
			return "HTTP/1.1 200 OK \n" + 
					"Content-Type: text/html\n" + 
					"\r\n" + content;
		} else if (code == 500) {
			return "HTTP/1.1 500" + " \n" + 
					"Content-Type: text/html\n" + 
					"\r\n" + errorMsg;
		}
		return "HTTP/1.1 404 NOT Found \n" + 
				"Content-Type: text/html\n" + 
				"\r\n" + 
				"

404 Not Found

"; } public static String getHttpResponseContext200(String content) { return getHttpResponseContext(200, content, ""); } public static String getHttpResponseContext404() { return getHttpResponseContext(404, "", ""); } public static String getHttpResponseContext500(Throwable cause) { StringBuffer sb = new StringBuffer(cause.toString()); StackTraceElement[] stes = null; if ((stes = cause.getStackTrace()) != null) { for (StackTraceElement ste : stes) { // sb.append("
        " + ste); sb.append("\n\t" + ste); } } return getHttpResponseContext(500, "", "

Internal Server Error:

".concat("
" + sb.toString() + "
")); } }

3.5.2 静态资源处理工具类FileUtil

FileUtil工具类首先找到请求的静态资源,并将静态资源文件内容写入到输出流,如下:

package com.szh.tomcat.util;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import lombok.extern.slf4j.Slf4j;

/**
 * 将静态资源文件内容写入到输出流
 */
@Slf4j
public class FileUtil {

	public static boolean writeFile(InputStream inputStream, OutputStream outputStream) {
		boolean success = false;
		BufferedInputStream bufferedInputStream;
		BufferedOutputStream bufferedOutputStream;
		try {
			bufferedInputStream = new BufferedInputStream(inputStream);
			bufferedOutputStream = new BufferedOutputStream(outputStream);
			bufferedOutputStream.write(HttpUtil.getHttpResponseContext200("").getBytes("utf-8"));
			int count = 0;
			while (count == 0) {
				count = inputStream.available();
			}
			int fileSize = inputStream.available();
			long written = 0;
			int byteSize = 1024;
			byte[] bytes = new byte[byteSize];
			while (written < fileSize) {
				if (written + byteSize > fileSize) {
					byteSize = (int) (fileSize - written);
					bytes = new byte[byteSize];
				}
				bufferedInputStream.read(bytes);
				bufferedOutputStream.write(bytes);
				bufferedOutputStream.flush();
				written += byteSize;
			}
			success = true;
		} catch (IOException e) {
			log.error(e.getMessage());
		}
		return success;
	}

	public static boolean writeFile(File file, OutputStream outputStream) throws IOException {
		return writeFile(new FileInputStream(file), outputStream);
	}

	public static String getResourcePath(String path) {
		String resource = FileUtil.class.getResource("/").getPath();
		return resource + "\\" + path;
	}
}

3.6 Servlet抽象和测试实现类

3.6.1 Servlet抽象类

该抽象类同javax.servlet.http.HttpServlet,需要doGet/doPost/service方法,且必须强制抛出Web应用内部的异常或错误,以此来使Tomcat做出对应处理(即响应码=5xx),如下:

package com.szh.tomcat.servlet;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public abstract class Servlet {
	public abstract void doGet(Request request, Response response) throws Throwable;
	public abstract void doPost(Request request, Response response) throws Throwable;

	public void service(Request request, Response response) throws Throwable {
		if ("GET".equals(request.getMethodType())) {
			doGet(request, response);
		} else if ("POST".equals(request.getMethodType())) {
			doPost(request, response);
		} else {
			log.error("暂不支持的请求方式!");
		}
	}
}

3.6.2 Servlet测试实现类

最后的阶段,自然是实现一个com.szh.tomcat.servlet.Servlet,以此测试我们的Tomcat是否手写成功。这里我们实现ServletConfigMapping静态块中add的servlet实现类MyServlet,如下:

package com.szh.tomcat.servlet;

import com.szh.tomcat.util.HttpUtil;

public class MyServlet extends Servlet {

	@Override
	public void doGet(Request request, Response response) throws RuntimeException {
		// 测试服务器内部错误5xx
		// System.out.println(1/0);
		String content = "

This is my GET request

"; response.write(HttpUtil.getHttpResponseContext200(content)); } @Override public void doPost(Request request, Response response) throws RuntimeException { String content = "

This is my POST request

"; response.write(HttpUtil.getHttpResponseContext200(content)); } }

四、测试运行

至此,手写Tomcat的工作已完结,测试以下几种case:

4.1 成功请求servlet(200)

启动MyTomcat.main(),直接访问配置的/testServlet,结果如下:

Tomcat原理之手写_第3张图片

4.2 错误请求servlet(5xx)

打开MyServlet.doGet方法中的注释,使之发生服务器内部错误,重启MyTomcat然后同样访问,结果如下:

Tomcat原理之手写_第4张图片

4.3 找不到请求servlet(404)

浏览器访问一个未注册的servlet,结果如下:

Tomcat原理之手写_第5张图片

4.4 成功请求静态资源(200)

因为我这里使用的是SpringBoot,所以添加静态资源index.html至src/main/resources,访问该资源,结果如下:

Tomcat原理之手写_第6张图片

4.5 找不到请求静态资源(404)

浏览器访问一个不存在的静态资源,结果如下:

Tomcat原理之手写_第7张图片


以上。

你可能感兴趣的:(Tomcat,手写系列,Tomcat,servlet,socket,Http)