手写Tomcat
2023年11月15日大约 13 分钟约 2540 字
整体架构分析
Tomcat 有三种运行模式(BIO, NIO, APR), 因为老师核心讲解的是 Tomcat 如何接收客户端请求,解析请求, 调用 Servlet , 并返回结果的机制流程, 采用 BIO 线程模型来模拟。

第一阶段
基于Socket开发服务端流程

1.ServerSocket在服务端监听指定端口,如果浏览器/客户端连接该端口,则建立连接,返回Socket对象。
2.Socket表示服务端/客户端间的链接,通过Socket可以得到InputStream和OutputStream流对象。
需求分析
浏览器请求 http://localhost:8080/??,服务端返回 hi,LiAng梁

代码实现
package com.lzw.tomcat;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 这是第一个版本的Tomcat可以完成接收浏览器的请求,并返回信息
*/
public class LzwTomcatV1 {
public static void main(String[] args) throws IOException {
// 1. 创建 ServerSocket ,在8080端口监听
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("=============== Tomcat在8080端口监听 ===============");
while(!serverSocket.isClosed()){
//等待浏览器/客户端连接
//如果有连接过来,就创建一个socket
//这个socket就是服务端和浏览器的连接/通道
Socket socket = serverSocket.accept();
//先接收浏览器发送的数据
//inputStream 是字节流 => BufferedReader(字符流)
InputStream inputStream = socket.getInputStream();
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
String mes = null;
System.out.println("=============== 接收到浏览器发送的数据 ===============");
//循环读取
while((mes = bufferedReader.readLine()) != null){
//判断mes的长度是否为0
if(mes.length() == 0){
break;
}
System.out.println(mes);
}
//我们的tomcat 回送-http响应方式
OutputStream outputStream = socket.getOutputStream();
//构建一个 http 响应的响应头
String respHeader = "HTTP/1.1 200 OK\r\n" +
"Content-Type: text/html;charset=utf-8\r\n\r\n";
String resp = respHeader + "hi,LiAng梁";
System.out.println("=============== tomcat给浏览器返回的数据 ===============");
System.out.println(resp);
outputStream.write(resp.getBytes());////将resp字符串以byte[] 方式返回
outputStream.flush();
outputStream.close();
inputStream.close();
socket.close();
}
}
}
第二阶段
BIO线程模型

需求分析
浏览器请求 http://localhost:8080, 服务端返回 hi,LiAng梁, 后台lzwtomcat 使用 BIO 线程模型,支持多线程=> 对前面的开发模式进行改造

代码实现
package com.lzw.tomcat.handler;
import java.io.*;
import java.net.Socket;
/**
* 1. LzwRequestHandler 对象是一个线程对象
* 2. 处理一个Http请求的
*/
public class LzwRequestHandler implements Runnable {
//定义一个Socket
private Socket socket = null;
public LzwRequestHandler(Socket socket){
this.socket = socket;
}
@Override
public void run() {
//这里我们可以对客户端/浏览器进行IO编程/交互
try {
InputStream inputStream = socket.getInputStream();
//把inputStream => BufferedReader 方便进行按行读取
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
System.out.println("=============== 接收到浏览器发送的数据 ===============");
String mes = null;
//如果读的数据不为空,就继续读取
while((mes = bufferedReader.readLine()) != null){
//判断mes的长度是否为0
if(mes.length() == 0){
break;
}
System.out.println(mes);
}
//构建http响应头
//返回的http的响应体和响应头之间有两个换行 \r\n\r\n
String respHeader = "HTTP/1.1 200 OK\r\n" +
"Content-Type: text/html;charset=utf-8\r\n\r\n";
String resp = respHeader + "<h1>hi,LiAng梁</h1>";
System.out.println("=============== tomcat给浏览器返回的数据 ===============");
System.out.println(resp);
//返回数据给我们的浏览器/客户端 -> 封装成 http响应
OutputStream outputStream = socket.getOutputStream();
//resp.getBytes() 把字符串转成字符数组
outputStream.write(resp.getBytes());
outputStream.flush();
outputStream.close();
inputStream.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}finally {
//最后一定确保socket要关闭
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
package com.lzw.tomcat;
import com.lzw.tomcat.handler.LzwRequestHandler;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class LzwTomcatV2 {
public static void main(String[] args) throws IOException {
//在8080端口监听
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("=============== TomcatV2 在8080端口监听 ===============");
//只要serverSocket没有关闭,就一直等待浏览器/客户端的链接
while (!serverSocket.isClosed()){
//1. 接收到浏览器的连接后,如果成功,就会得到socket
//2. 这个socket 就是 服务器和 浏览器的数据通道
Socket socket = serverSocket.accept();
//3. 创建一个线程对象,并且把socket给该线程
new Thread(new LzwRequestHandler(socket)).start();
}
}
}
第三阶段
Servlet 生命周期

需求分析
浏览器请求 http://localhost:8080/lzwCalServlet, 提交数据,完成计算任务,如果 servlet 不存在,返回 404


代码实现
LzwRequest
package com.lzw.tomcat.http;
import jdk.internal.util.xml.impl.Input;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
/**
* 1. LzwRequest 作用是封装http请求的数据
* 2. 比如 method get/post uri,参数
* 3. LzwRequest 作用等价原生的servlet 中的 HttpServletRequest
* 4. 先只考虑 get请求
*/
public class LzwRequest {
private String method;
private String uri;
//存放参数列表 参数名-参数值 => HashMap
private HashMap<String, String> parametersMapping = new HashMap<>();
private InputStream inputStream = null;
//构造器 对http请求进行封装
//inputStream是和 对应http请求的socket关联
public LzwRequest(InputStream inputStream){
this.inputStream = inputStream;
//完成对http请求数据的封装..
encapHttpRequest();
}
/**
* 将http请求的相关数据,进行封装,然后提供相关的方法,进行获取
*/
private void encapHttpRequest() {
try {
//inputStream => BufferedReader
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
//读取第一行
/**
* GET /hspCalServlet?num1=10&num2=30 HTTP/1.1
* Host: localhost:8080
* User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:97.0) Gecko/20100101 Fi
*/
String requestLine = bufferedReader.readLine();
//GET - /lzwCalServlet?num1=10&num2=30 - HTTP/1.1
String[] requestLineArr = requestLine.split(" ");
//得到method
method = requestLineArr[0];
//解析得到 /hspCalServlet
//1. 先看看uri 有没有参数列表
int index = requestLineArr[1].indexOf("?");
if(index == -1){//说明没有参数列表
uri = requestLineArr[1];
}else{
//[0,index)
uri = requestLineArr[1].substring(0, index);
//获取参数列表->parametersMapping
//parameters => num1=10&num2=30s
String parameters = requestLineArr[1].substring(index + 1);
String[] parametersPair = parameters.split("&");
//防止用户提交时 /lzw对http请求进行封装CalServlet?
if(null != parametersPair && !"".equals(parametersPair)){
//再次分割
for (String parameterPair : parametersPair) {
//parameterVal ["num1", "10"]
String[] parameterVal = parameterPair.split("=");
if (parameterVal.length == 2) {
//放入到 parametersMapping
parametersMapping.put(parameterVal[0], parameterVal[1]);
}
}
}
}
//这里不能关闭流 inputStream 和 socket关联
//inputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
//request对象有一个特别重要方法
public String getParameter(String name) {
if (parametersMapping.containsKey(name)) {
return parametersMapping.get(name);
} else {
return "";
}
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getUri() {
return uri;
}
public void setUri(String uri) {
this.uri = uri;
}
@Override
public String toString() {
return "LzwRequest{" +
"method='" + method + '\'' +
", uri='" + uri + '\'' +
", parametersMapping=" + parametersMapping +
'}';
}
}
LzwResponse
package com.lzw.tomcat.http;
import java.io.OutputStream;
/**
* 1. LzwResponse 对象可以封装OutputStream(是socket关联)
* 2. 即可以通过 LzwResponse 对象返回Http响应给浏览器/客户端
* 3. LzwResponse 作用等价于原生的servlet的 HttpServletResponse
*/
public class LzwResponse {
private OutputStream outputStream = null;
//写一个http的响应头 => 先死后活
public static final String respHeader = "HTTP/1.1 200 OK\r\n" +
"Content-Type: text/html;charset=utf-8\r\n\r\n";
//在创建 HspResponse 对象时,传入的outputStream是和Socket关联的
public LzwResponse(OutputStream outputStream) {
this.outputStream = outputStream;
}
//当我们需要给浏览器返回数据时,可以通过HspResponse 的输出流完成
public OutputStream getOutputStream() {
return outputStream;
}
}
LzwServlet
package com.lzw.tomcat.servlet;
import com.lzw.tomcat.http.LzwRequest;
import com.lzw.tomcat.http.LzwResponse;
import java.io.IOException;
/**
* 保留三个核心方法声明
*/
public interface LzwServlet {
void init() throws Exception;
void service(LzwRequest request, LzwResponse response) throws IOException;
void destroy();
}
LzwHttpServlet
package com.lzw.tomcat.servlet;
import com.lzw.tomcat.http.LzwRequest;
import com.lzw.tomcat.http.LzwResponse;
import java.io.IOException;
public abstract class LzwHttpServlet implements LzwServlet{
@Override
public void service(LzwRequest request, LzwResponse response) throws IOException {
if("GET".equalsIgnoreCase(request.getMethod())){
this.doGet(request, response);
}else if("POST".equalsIgnoreCase(request.getMethod())){
this.doPost(request, response);
}
}
//这里我们使用的了模板设计模式 => java 基础的 抽象类专门讲过模板设计模式
//让LzwHttpServlet 子类 LzwCalServlet 实现
public abstract void doGet(LzwRequest request, LzwResponse response);
public abstract void doPost(LzwRequest request, LzwResponse response);
}
LzwCalServlet
package com.lzw.tomcat.servlet;
import com.lzw.tomcat.http.LzwRequest;
import com.lzw.tomcat.http.LzwResponse;
import com.lzw.tomcat.utils.WebUtils;
import java.io.IOException;
import java.io.OutputStream;
public class LzwCalServlet extends LzwHttpServlet {
@Override
public void doGet(LzwRequest request, LzwResponse response) {
//写业务代码,完成计算任务
int num1 = WebUtils.parseInt(request.getParameter("num1"), 0);
int num2 = WebUtils.parseInt(request.getParameter("num2"), 0);
int sum = num1 + num2;
//返回计算结果给浏览器
//outputStream 和 当前的 socket关联
OutputStream outputStream = response.getOutputStream();
String respMes = LzwResponse.respHeader + "<h1>" + num1 + " + " + num2 + " = " + sum + " LzwTomcat </h1>";
try {
outputStream.write(respMes.getBytes());
outputStream.flush();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void doPost(LzwRequest request, LzwResponse response) {
this.doGet(request, response);
}
@Override
public void init() throws Exception {
}
@Override
public void destroy() {
}
}
LzwRequestHandler
package com.lzw.tomcat.handler;
import com.lzw.tomcat.LzwTomcatV3;
import com.lzw.tomcat.http.LzwRequest;
import com.lzw.tomcat.http.LzwResponse;
import com.lzw.tomcat.servlet.LzwCalServlet;
import com.lzw.tomcat.servlet.LzwHttpServlet;
import java.io.*;
import java.net.Socket;
/**
* 1. LzwRequestHandler 对象是一个线程对象
* 2. 处理一个Http请求的
*/
public class LzwRequestHandler implements Runnable {
//定义一个Socket
private Socket socket = null;
public LzwRequestHandler(Socket socket){
this.socket = socket;
}
@Override
public void run() {
//这里我们可以对客户端/浏览器进行IO编程/交互
try {
LzwRequest lzwRequest = new LzwRequest(socket.getInputStream());
//这里我们可以同LzwResponse对象,返回数据给浏览器/客户端
LzwResponse lzwResponse = new LzwResponse(socket.getOutputStream());
//1. 得到 uri => 就是 servletUrlMapping 的 url-pattern
String uri = lzwRequest.getUri();
String servletName = LzwTomcatV3.servletUrlMapping.get(uri);
if(servletName == null) {
servletName = "";
}
//2. 通过uri->servletName->servlet的实例 , 真正的运行类型是其子类 LzwCalServlet
LzwHttpServlet lzwHttpServlet = LzwTomcatV3.servletMapping.get(servletName);
//3. 调用service , 通过OOP的动态绑定机制,调用运行类型的 doGet/doPost
if (lzwHttpServlet != null) {//得到
lzwHttpServlet.service(lzwRequest, lzwResponse);
} else { //没有这个servlet , 返回404的提示信息
String resp = LzwResponse.respHeader + "<h1>404 Not Found</h1>";
OutputStream outputStream = lzwResponse.getOutputStream();
outputStream.write(resp.getBytes());
outputStream.flush();
outputStream.close();
}
socket.close();
} catch (IOException e) {
e.printStackTrace();
}finally {
//最后一定确保socket要关闭
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
LzwTomcatV3
package com.lzw.tomcat;
import com.lzw.tomcat.handler.LzwRequestHandler;
import com.lzw.tomcat.servlet.LzwHttpServlet;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
* 第3版 Tomcat,实现通过xml+反射来初始化容器
*/
public class LzwTomcatV3 {
//1. 存放容器 servletMapping
// -ConcurrentHashMap
// -HashMap
// key - value
// ServletName 对应的实例
public static final ConcurrentHashMap<String, LzwHttpServlet> servletMapping = new ConcurrentHashMap<>();
//2容器 servletUrlMapping
// -ConcurrentHashMap
// -HashMap
// key - value
// url-pattern ServletName
public static final ConcurrentHashMap<String, String> servletUrlMapping = new ConcurrentHashMap<>();
public static void main(String[] args) {
LzwTomcatV3 lzwTomcatV3 = new LzwTomcatV3();
lzwTomcatV3.init();
//启动lzwTomcat 容器
lzwTomcatV3.run();
}
//启动LzwTomcatV3
public void run(){
try {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("=============== lzwTomcatV3在8080监听 ===============");
while (!serverSocket.isClosed()) {
Socket socket = serverSocket.accept();
LzwRequestHandler lzwRequestHandler = new LzwRequestHandler(socket);
new Thread(lzwRequestHandler).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
//直接对两个容器进行初始化
public void init() {
//读取web.xml => dom4j
String path = LzwTomcatV3.class.getResource("/").getPath();
System.out.println("path = " + path);
//使用dom4j技术完成读取
SAXReader saxReader = new SAXReader();
try {
System.out.println(path + "web.xml");
Document document = saxReader.read(new File(path + "web.xml"));
System.out.println("document = " + document);
//得到根元素
Element rootElement = document.getRootElement();
//得到根元素下面的所有元素
List<Element> elements = rootElement.elements();
//遍历并过滤到 servlet servlet-mapping
for (Element element : elements) {
if ("servlet".equalsIgnoreCase(element.getName())) {
//这是一个servlet配置
// System.out.println("发现 servlet");
//使用反射将该servlet实例放入到servletMapping
Element servletName = element.element("servlet-name");
Element servletClass = element.element("servlet-class");
servletMapping.put(servletName.getText(),(LzwHttpServlet) Class.forName(servletClass.getText().trim()).newInstance());
}else if("servlet-mapping".equalsIgnoreCase(element.getName())){
//这是一个servlet-mapping
// System.out.println("发现 servlet-mapping");
Element servletName = element.element("servlet-name");
Element urlPatter = element.element("url-pattern");
servletUrlMapping.put(urlPatter.getText(), servletName.getText());
}
}
} catch (Exception e) {
e.printStackTrace();
}
//验证,这两个容器是否初始化成功
System.out.println("servletMapping= " + servletMapping);
System.out.println("servletUrlMapping= " + servletUrlMapping);
}
}
WebUtils
package com.lzw.tomcat.utils;
import java.io.BufferedReader;
import java.io.FileReader;
public class WebUtils {
//将字符串转成数字方法
public static int parseInt(String strNum, int defaultVal) {
try {
return Integer.parseInt(strNum);
} catch (NumberFormatException e) {
System.out.println(strNum + " 不能转成数字");
}
return defaultVal;
}
}