本文记录了手写tomcat的关键步骤,阅读大约需要10分钟,文末有压缩包可下载验证
1. 模拟Tomcat两个功能
- 接收http请求
- 返回资源
2. 步骤
- 使用Socket接收请求
- 将请求信息封装成Request,返回信息封装成Response
- 处理静态资源,若找不到则在页面上显示404
- 启动时加载web.xml,保存url与servlet的映射关系
- 处理动态资源,若找不到则在页面上显示404
- 使用多线程优化请求处理
3. 项目结构
3.1 Bootstrap 启动类
package server;
import ...
public class Bootstrap {
/** 端口 */
int port = 8080;
/** servlet容器 */
Map<String, HttpServlet> servletMap = new HashMap<>();
/**
* 启动入口
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
Bootstrap bootstrap = new Bootstrap();
bootstrap.start();
}
private void start() throws Exception {
// 1. 启动时加载web.xml,保存url与servlet的映射关系
loadServlet();
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("MiniCat start on port:" + port);
while (true) {
// 2. 使用Socket接收请求
Socket socket = serverSocket.accept();
// 3. 将请求信息封装成Request,返回信息封装成Response
Request request = new Request(socket.getInputStream());
Response response = new Response(socket.getOutputStream());
if (request.getUrl().contains("html")) {
// 4. 处理静态资源,若找不到则在页面上显示404
response.outputHtml(request.getUrl());
} else {
// 5. 处理动态资源,若找不到则在页面上显示404
HttpServlet httpServlet = servletMap.get(request.getUrl());
if (httpServlet == null) {
response.output(HttpProtocolUtil.getHttpHeader404());
} else {
httpServlet.service(request, response);
}
}
socket.close();
}
}
/**
* 加载并解析web.xml,保存url与servlet的映射关系
*/
private void loadServlet() throws Exception {
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("web.xml");
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(inputStream);
Element rootElement = document.getRootElement();
List<Node> servletNodes = rootElement.selectNodes("//servlet");
for (Node servlet : servletNodes) {
// <servlet-name>hello</servlet-name>
String servletName = servlet.selectSingleNode("servlet-name").getStringValue();
// <servlet-class>server.servlet.HelloServlet</servlet-class>
String servletClass = servlet.selectSingleNode("servlet-class").getStringValue();
// 使用Xpath表达式,根据servletName查找url-pattern
Element servletMapping = (Element) rootElement
.selectSingleNode("/web-app/servlet-mapping[servlet-name='" + servletName + "']");
String urlPattern = servletMapping.selectSingleNode("url-pattern").getStringValue();
// 保存映射关系
servletMap.put(urlPattern, (HttpServlet) Class.forName(servletClass).newInstance());
}
}
}
3.2 Request 解析请求信息
package server.pojo;
import ...
@Data
public class Request {
/** 请求方式:GET POST */
private String method;
/** 资源路径: / 或者 /index.html*/
private String url;
/** 请求信息 */
private InputStream inputStream;
/**
* 在构造器中,将请求信息封装成Request
* @param inputStream
*/
public Request(InputStream inputStream) throws IOException {
this.inputStream = inputStream;
int count = 0;
while (count == 0) {
// 获取准备到来的字节数的估计值
count = inputStream.available();
}
// 读进来并缓存在字节数组中
byte[] bytes = new byte[count];
inputStream.read(bytes);
// 获取请求头信息的第一行,解析method和url
// GET / HTTP/1.1
String header = new String(bytes);
String first = header.split("\\n")[0];
String[] s = first.split(" ");
this.method = s[0];
this.url = s[1];
System.out.println("===>method:" + method + ", url:" + url);
}
}
3.3 Response 封装响应信息
package server.pojo;
import ...
@Data
public class Response {
private OutputStream outputStream;
public Response(OutputStream outputStream) {
this.outputStream = outputStream;
}
/**
* 根据url查找静态资源的绝对路径,并写到response
* @param path
*/
public void outputHtml(String path) throws IOException {
// 获取静态资源的绝对路径
String absolutePath = StaticResourceUtil.getAbsolutePath(path);
// 读取静态资源文件
File file = new File(absolutePath);
if (file.exists() && file.isFile()) {
// 读取静态资源文件,输出静态资源
StaticResourceUtil.outputStaticResource(new FileInputStream(file), outputStream);
} else {
// 找不到文件,输出404
output(HttpProtocolUtil.getHttpHeader404());
}
}
/**
* 使用输出流输出指定字符串
* @param content
*/
public void output(String content) throws IOException {
outputStream.write(content.getBytes());
}
}