目录
一、准备工作
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的基本知识,进而罗列所需的基本技术储备。
Apache Tomcat作为目前广大Java技术爱好者使用最多、接触最早的轻量级Web应用服务器,至今流行依旧,也被SpringBoot内置其中。
作为一个Web应用资源容器(包括了静态Html或别的资源和servlet配置),它可以处理静态Html页面,同时也是servlet容器,用来处理动态Java服务请求;作为一款Java服务器,它的本质又可以理解为是一个ServerSocket,监听指定端口,接收浏览器发送过来的请求,本质也就是一个个socket,解析套接字输入流里的请求体内容,请求处理完毕后将响应信息,以标准的http协议规范的形式返回给浏览器(强调http协议规范,否则浏览器不认识)。
简单说,Tomcat可以被认为是Web资源容器+ServerSocket。
socket套接字、IO、Http、servlet、xml。
根据使用Tomcat的经验,以及JavaWeb工程的配置方式等的分析,得出以下实现方案:
Tomcat作为servlet容器,在启动时会解析web.xml文件,并将servlet配置加载到服务器内存中。
3.2.1 servlet配置类ServletConfig
这些servlet在web.xml中的样式如下图。我们可以将一对servlet的配置抽象成配置类ServletConfig,它需要具有的属性也来自上图的
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;
}
}
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.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.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.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:
启动MyTomcat.main(),直接访问配置的/testServlet,结果如下:
打开MyServlet.doGet方法中的注释,使之发生服务器内部错误,重启MyTomcat然后同样访问,结果如下:
浏览器访问一个未注册的servlet,结果如下:
因为我这里使用的是SpringBoot,所以添加静态资源index.html至src/main/resources,访问该资源,结果如下:
浏览器访问一个不存在的静态资源,结果如下:
以上。