手写Tomcat(二)—Tomcat简化模型架构
一、 Tomcat架构
Tomcat的主要角色是 servlet容器,提供一个解释器,能够解析并执行JavaScript Object Notation (JON)脚本(后更改为Servlet),并将请求传送到指定的服务器(如JavaBean)。因此,它是Servlet处理请求、与客户端通信(RESTful API)以及处理服务器端业务逻辑的关键组件。
1.1 HttpServletRequest
HttpServletRequest类在模型中充当 HTTP请求的元数据载体,是连接网络层(Connector)与业务层(Servlet)的桥梁。
public class HttpServletRequest {private String url;public String method;public String getUrl() {return url;}public void setUrl(String url) {this.url = url;}public String getMethod() {return method;}public void setMethod(String method) {this.method = method;}
}
URL路径:标识客户端请求的资源位置(如
/login
)。HTTP方法:明确操作类型(GET获取、POST提交),用于路由到Servlet的正确处理逻辑。
与实际 Tomcat 实现的差异
简化实现 | 实际 Tomcat 的 HttpServletRequest |
---|---|
仅包含url 和method 字段 | 包含完整的请求信息(头、参数、会话、Cookie等) |
直接通过Setter注入数据 | 数据由Tomcat底层协议解析器自动填充 |
无协议规范校验(如方法合法性) | 严格校验HTTP协议规范(如方法名有效性) |
无线程安全控制 | 每个请求独立实例,天然线程安全 |
1.2 HttpServletResponse
HttpServletResponse类在模型中充当 响应数据的输出通道,是业务逻辑(Servlet)与网络层(Connector)之间的桥梁。
核心思想
数据输出本质:所有响应最终通过底层
OutputStream
发送字节流。分层设计:业务层无需关心网络细节,只需操作抽象响应对象。
协议封装:实际Tomcat隐藏了HTTP协议拼接的复杂性。
public class HttpServletResponse {//用于持有输出流,通过此流将数据发送到客户端private OutputStream outputStream; //接收一个OutputStream(通常来自服务器Socket连接),以便后续写入响应内容。public HttpServletResponse(OutputStream outputStream){this.outputStream = outputStream;} //将字符串转换为字节数组并写入输出流。public void write(String context) throws IOException {outputStream.write(context.getBytes());}
}
1. 响应数据输出通道
持有输出流:通过构造函数接收底层网络输出流(通常来自Tomcat的
Connector
模块),直接操作字节流。数据写入:
write()
方法将字符串转换为字节写入流,完成响应体的发送。2. 在请求处理流程中的角色
Connector阶段:由连接器创建实例,并注入与客户端Socket关联的
OutputStream
。Container阶段:传递到Servlet的
service()
方法,供业务逻辑生成响应内容。3. 基础协议支持
仅处理响应体:直接写入原始字节流,但未包含HTTP协议头(如状态码、Content-Type)。
与实际 Tomcat 实现的差异
简化实现 | 实际 Tomcat 的 HttpServletResponse |
---|---|
仅支持直接写入响应体 | 支持自动管理状态码、响应头、Cookies等 |
无编码控制(默认平台编码) | 提供setCharacterEncoding() 和setContentType() 方法 |
无缓冲机制(直接写Socket流) | 内置缓冲区(BufferedOutputStream )提升性能 |
无重定向/转发支持 | 支持sendRedirect() 和forward() 方法 |
无错误处理机制 | 可调用sendError() 返回标准错误页面 |
1.3 servlet接口
servlet接口在模型中定义了 请求处理的唯一入口,是Tomcat将网络层(Connector)与业务逻辑(开发者代码)解耦的核心设计。
核心思想
面向接口编程:Tomcat通过接口调用开发者代码,降低耦合度。
请求-响应范式:所有Web交互抽象为
request
输入和response
输出。容器管理:Servlet实例的创建、调用、销毁由Tomcat控制,开发者聚焦业务逻辑。
public interface servlet {public void service(HttpServletRequest request, HttpServletResponse response);
}
1. 业务逻辑的统一入口
请求处理抽象化:所有具体业务逻辑(如用户登录、数据查询)必须实现
service()
方法,Tomcat通过此方法将控制权交给开发者。协议无关性:开发者无需关心HTTP协议的解析细节(如报文拆分、状态码生成),只需操作
request
和response
对象。2. 在Tomcat处理流程中的角色
Container阶段调用:当Tomcat完成URL匹配并确定目标Servlet后,调用其
service()
方法。流程控制枢纽:通过
service()
方法,开发者可以:
读取请求参数(
request.getParameter()
)。处理业务数据(如访问数据库)。
生成响应内容(
response.write()
)。3. 设计模式体现
模板方法模式:实际Servlet实现类(如
HttpServlet
)通过重写service()
或具体方法(doGet()
/doPost()
)定义行为。控制反转(IoC):Tomcat容器控制Servlet生命周期及方法调用,开发者仅需实现接口。
与实际 javax.servlet.Servlet
接口的差异
简化实现 | 实际 Servlet 接口 |
---|---|
仅定义service() 方法 | 包含init() 、destroy() 等生命周期方法 |
无配置参数支持 | 可通过ServletConfig 获取初始化参数 |
直接处理HTTP协议 | 通常继承HttpServlet 抽象类,实现doGet() 等方法 |
无线程安全约束 | 规范明确Servlet实例默认单例多线程调用,需开发者自行保证线程安全 |
1.4 HttpServlet
HttpServlet类在模型中实现了 HTTP方法的路由分发,是Tomcat将协议细节与业务逻辑分离的关键设计。
核心思想
职责分离:协议处理(方法路由)与业务逻辑(
doXxx()
)解耦。扩展性:通过覆盖特定方法支持新功能,无需修改框架代码。
规范化:统一Servlet实现模式,降低开发者学习成本。
public abstract class HttpServlet implements servlet{//doGet方法public void doGet(HttpServletRequest request,HttpServletResponse response){}//doPost方法public void doPost(HttpServletRequest request,HttpServletResponse response){}//根据请求方式判断调用的方法@Overridepublic void service(HttpServletRequest request, HttpServletResponse response) {if(request.getMethod().equals("GET")){doGet(request,response);}else if(request.getMethod().equals("POST")){doPost(request,response);}}
}
核心作用
1. HTTP方法的路由分发
请求方法解耦:将不同HTTP方法(GET/POST)的处理逻辑分离到独立方法(
doGet()
/doPost()
),避免在service()
中堆积条件判断。开发者友好性:子类只需覆盖关注的方法(如
doGet()
),无需重写整个service()
。2. 在Tomcat处理流程中的角色
容器调用入口:Tomcat调用
service()
方法后,自动根据请求方法触发对应的doXxx()
。模板方法模式:定义算法骨架(方法路由),具体步骤由子类实现。
3. 规范扩展性
支持新HTTP方法:通过扩展
service()
方法(如添加doPut()
),兼容更多HTTP方法。统一错误处理:可集中处理未实现的方法(如返回405状态码)。
与实际 HttpServlet
的差异
简化实现 | 实际 javax.servlet.http.HttpServlet |
---|---|
仅支持GET/POST方法 | 支持所有HTTP方法(PUT/DELETE/HEAD等) |
无HTTP协议版本处理 | 区分HTTP/1.0和HTTP/1.1的语义差异 |
无默认错误响应 | 自动返回405 Method Not Allowed 或 400 Bad Request |
无线程安全提示 | 明确说明Servlet实例需线程安全 |
1.5 SearchClassUtil
用于获得某一个包下所有类的路径
public class SearchClassUtil {public static List<String> classPaths = new ArrayList<>();public static List<String> searchClass(){//需要扫描的包名String basePath = "com.qcby.myweb";//将获取到的包名转换为路径String classPath = SearchClassUtil.class.getResource("/").getPath();basePath = basePath.replace(".", File.separator);String searchPath = classPath + basePath;doPath(new File(searchPath),classPath);//得到指定包下所有类的绝对路径,利用绝对路径和Java反射机制得到类对象return classPaths;}/*** 该方法会得到所有的类,将类的绝对路径你写入到classPaths中* file*/private static void doPath(File file, String classpath) {if(file.isDirectory()){File[] files = file.listFiles();for(File f1:files){doPath(f1,classpath);}}else{if(file.getName().endsWith(".class")){String path = file.getPath().replace(classpath.replace("/","\\").replaceFirst("\\\\",""),"").replace("\\",".").replace(".class","");classPaths.add(path);}}}public static void main(String[] args) {List<String> classes = SearchClassUtil.searchClass();for(String s:classes){System.out.println(s);}}
}
运行结果
1.6 ResponseUtil
/*** 设置响应头*/
public class ResponseUtil {public static final String responseHeader200 = "HTTP/1.1 200 \r\n"+"Content-Type:text/html\r\n"+"\r\n";public static String getResponseHeader404(){return "HTTP/1.1 404 \r\n"+"Content-Type:text/html\r\n"+"\r\n" + "404";}public static String getResponseHeader200(String context){return "HTTP/1.1 200\r\n"+"Content-Type:text/html\r\n"+"\r\n" + context;}}
1.7 XWServlet
核心目标:替代传统的 web.xml
配置文件,通过声明式编程将 Servlet 的 URL 映射路径直接绑定到类上。
URL 路径映射:通过
url
属性指定 Servlet 处理的请求路径(如/user
)。自动化注册:Tomcat 启动时自动扫描带有该注解的类,并将其注册到容器中。
零配置开发:开发者无需编写
web.xml
,直接在代码中声明路由规则。
@Target(ElementType.TYPE) //仅允许标注在类或接口上
@Retention(RetentionPolicy.RUNTIME) //确保注解在运行时可通过反射获取,这是Tomcat动态注册 Servlet 的前提
public @interface XWServlet {String url() default ""; //指定 Servlet 的访问路径,默认值为空字符串(需开发者显式配置)
}
1.8 TomcatRoute
public class TomcatRoute {public static HashMap<String, HttpServlet> map = new HashMap<>();static {List<String> paths = PackageUtil.getClassName("com.qcby.myweb");for(String path:paths){try {Class clazz = Class.forName(path);HttpServlet a = (HttpServlet) clazz.getDeclaredConstructor().newInstance();XWServlet xwServlet = (XWServlet) clazz.getDeclaredAnnotation(XWServlet.class);map.put(xwServlet.url(),a);} catch (Exception e) {e.printStackTrace();}}}
}
通过 PackageUtil获取com.qcby.myweb包下的所有类路径,并通过反射创建类对象,通过XWServlet 获取类的url,将url及其对应的类对象放入到HashMap中。
1.9 MyTomcat
public class MyTomcat {static HashMap<String,HttpServlet> map = TomcatRoute.map;public static void dispatch(HttpServletRequest request,HttpServletResponse response){HttpServlet servlet = map.get(request.getUrl());if(servlet != null){servlet.service(request,response);}}public static void start() throws IOException {System.out.println("服务器启动");ServerSocket serverSocket = new ServerSocket(8083);while (true){Socket socket = serverSocket.accept();InputStream inputStream = socket.getInputStream();HttpServletRequest request = new HttpServletRequest();BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));String str = reader.readLine();request.setMethod(str.split(" ")[0]);request.setUrl(str.split(" ")[1]);OutputStream outputStream = socket.getOutputStream();HttpServletResponse response = new HttpServletResponse(outputStream);dispatch(request,response);}}public static void main(String[] args) throws IOException {start();}
}
1.9.1 dispatch
public static void dispatch(HttpServletRequest request,HttpServletResponse response){HttpServlet servlet = map.get(request.getUrl());if(servlet != null){servlet.service(request,response);}
}
通过request中的url信息查找 其对应的类对象,若存在,则调用service方式。
1.9.2 Socket
在Tomcat中,Socket(套接字)是实现网络通信的核心组件,主要用于处理客户端与服务器之间的数据传输。
数据从客户端到Tomcat的传递
客户端发起请求
客户端通过浏览器发送HTTP请求,数据经操作系统封装为TCP/IP数据包,通过网卡传输到目标服务器。网卡接收数据
服务器的网卡接收到物理信号后,将其转换为二进制数据流,传递给操作系统内核。Socket监听与处理
Tomcat的Connector组件通过Endpoint
(如NioEndpoint
)监听指定端口,接受Socket连接。操作系统将数据流交给Tomcat的Socket实例,由Processor
解析HTTP协议并生成Request
对象。 注:每个需要网络的进程都要申请端口号,有些端口号是被明确占用的,如Mysql的端口号是3306,HTTP的端口号是8080。Servlet容器处理
解析后的请求通过Adapter
转换为ServletRequest
,由Servlet容器(如Engine
、Host
、Context
)处理业务逻辑,生成响应数据。响应返回客户端
响应数据通过Socket的输出流发送回客户端,经操作系统协议栈封装后,由网卡转换为物理信号传输到客户端。
System.out.println("服务器启动..."); //1.定义ServerSocket对象进行服务器的端口注册ServerSocket serverSocket = new ServerSocket(8080); //端口号用于区分不同的进程while (true){//2.监听客户端的Socket连接程序Socket socket = serverSocket.accept(); //accept()--》用于阻塞监听,没有程序访问时,停止执行,直到有程序访问时,继续执行下一步//3.从socket对象当中获取到一个字节输入流对象InputStream iStream = socket.getInputStream();HttpServletRequest request = new HttpServletRequest();BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));String str = reader.readLine();request.setMethod(str.split(" ")[0]);request.setUrl(str.split(" ")[1]);OutputStream outputStream = socket.getOutputStream();HttpServletResponse response = new HttpServletResponse(outputStream);dispatch(request,response);}
代码逐行解析
1. 接受客户端连接
Socket socket = serverSocket.accept();
阻塞等待客户端连接,返回代表该连接的Socket对象。这是建立TCP通信的第一步。
Tomcat对应实现:
Tomcat的Connector组件(如NioEndpoint)负责监听端口,使用NIO多路复用或BIO模型处理连接,而非简单阻塞式accept()。实际会通过线程池高效管理连接。2. 获取输入流
InputStream inputStream = socket.getInputStream();
获取与客户端Socket关联的输入流,用于读取HTTP请求的原始字节数据。
Tomcat对应实现:
Tomcat的CoyoteAdapter会解析输入流,将其封装为org.apache.coyote.Request对象,处理协议细节(如HTTP头、Chunked编码)。3. 创建请求对象
HttpServletRequest request = new HttpServletRequest();
实例化一个自定义的请求对象,用于承载解析后的请求信息(如URL、方法)。
Tomcat对应实现:
实际创建的是org.apache.catalina.connector.Request对象,包含完整的请求信息(参数、Session、头信息等),由容器自动填充数据。4. 包装缓冲读取器
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
将字节流转换为字符流,便于按行读取请求内容。
潜在问题:
直接使用BufferedReader可能无法正确处理二进制数据(如图片上传)。Tomcat底层使用ByteBuffer进行高效二进制解析。5. 读取请求行
String str = reader.readLine();
读取HTTP请求的第一行(即请求行),格式为方法 URI 协议版本,例如:
GET /index.html HTTP/1.1Tomcat对应实现:
通过Http11Processor解析请求行,严格校验协议合规性(如方法名合法性),并提取完整URI和协议版本。6. 解析HTTP方法与URL
request.setMethod(str.split(" ")[0]); // 如"GET" request.setUrl(str.split(" ")[1]); // 如"/aa.java"
从请求行中提取HTTP方法和请求路径,填充到请求对象中。
简化问题:
未处理URI中的查询参数(如/user?id=1中的id=1)。
未解析协议版本(如HTTP/1.1与HTTP/2差异)。
未处理可能的异常格式(如空路径或非法方法)。
7. 获取输出流
OutputStream outputStream = socket.getOutputStream();
获取与客户端Socket关联的输出流,用于向客户端发送响应数据。
Tomcat对应实现:
输出流会被包装为org.apache.coyote.Response,支持缓冲、分块传输(chunked encoding)和自动压缩(如gzip)。8. 创建响应对象
HttpServletResponse response = new HttpServletResponse(outputStream);
实例化自定义响应对象,关联输出流以便写入响应内容。
Tomcat对应实现:
实际创建的是org.apache.catalina.connector.Response对象,管理状态码、响应头和内容编码。9. 分发请求
dispatch(request, response);
将请求和响应对象传递给调度器,执行后续处理(如路由到Servlet或静态资源)。
Tomcat对应实现:
容器层级路由:依次通过Engine→Host→Context→Wrapper,匹配对应的Servlet。
过滤器链执行:在调用Servlet前,执行所有匹配的Filter。
Servlet生命周期:调用service()方法,处理业务逻辑。
请求处理全流程图示
客户端│▼ Socket.accept() // 建立连接│▼ InputStream → 解析请求行 // 读取HTTP请求基础信息│▼ 创建Request/Response对象 // 封装协议数据│▼ dispatch() → 路由到Servlet // 业务逻辑处理│▼ OutputStream → 返回响应 // 发送HTTP响应