贯穿示例 · 手写 HTTP 服务器(V1–V7)
从一个裸 Socket 逐步长出手写 Servlet、Filter、Listener,再到 Spring MVC 与 ORM 的框架仿真
贯穿示例 · 手写 HTTP 服务器(V1–V7)
贯穿式教学示例(一):把「Web 容器到底干了什么」这件事,用一个代码库的七次演进讲透。素材在本仓库同级的
HttpWebServerV1…HttpWebServerV7七个工程里,下面每段代码都摘自对应版本的真实源码(有删节)。这条线的意义:课程模块 1–3(WEB 基础 / Servlet、Filter、Listener / JSP)讲的是规范与概念,本示例则从零开始手写一遍——先没有 Tomcat,只有
java.net,看一个 HTTP 服务器是怎么一步步「长」出容器能力的。
一、总览:一条从裸 Socket 到框架仿真的演进链
| 版本 | 新增能力 | 对应课程模块 |
|---|---|---|
| V1 | 裸 ServerSocket,手写解析 HTTP 请求行与头,返回静态文件 | 模块 1 · WEB 基础 |
| V2 | 结构化 Request/Response 对象 + Application 上下文 + Session/SessionManager | 模块 1 · WEB 基础 |
| V3 | Servlet 抽象:GenericServlet/HttpServlet,URL→Servlet 映射与生命周期 | 模块 2 · Servlet |
| V4 | Filter 链:LoggingFilter 包裹 Servlet,做请求前后处理 | 模块 2 · Filter |
| V5 | Listener + Session 事件:SessionCounterListener 响应会话创建/销毁 | 模块 2 · Listener |
| V6 | Spring MVC 仿真:前端控制器 MvcDispatcher + @Controller/@RequestMapping | 模块 5 · SpringMVC |
| V7 | ORM 层:MapperProxy 动态代理读注解生成 SQL(MyBatis 式) | 模块 8 · MyBatis |
交互实验:手写 HTTP 服务器 V1–V7 演进(HttpServerTimeline)
从一个最简 socket 程序一步步长成 Tomcat:每升一版就补上一种 Web 能力。点时间线上的节点,看该版本新增了什么、代表类是什么——这正是模块1–3(Servlet / Filter / Listener)的活教材。
V1 · 最简 socket 服务器
新增能力:ServerSocket 监听 + 收请求/回响应,支持 request/response 内置对象
代表类:SimpleWebServerRequestResponse
第一个能跑的网络程序:绑定端口、读请求行、回一段 HTML。
到 V1 为止,已叠加 1 / 7 项核心能力——V7 凑齐 SSM 三件套雏形。
二、V1:没有容器,只有 Socket
V1 的 SimpleWebServer 是整条链的地基——一行规范都没有,全靠手写。它开一个 ServerSocket,循环 accept() 拿到连接,自己从输入流里读请求行(GET /path HTTP/1.1)和头,再决定返回哪个静态文件:
// HttpWebServerV1/src/SimpleWebServer.java(节选)
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket socket = serverSocket.accept(); // 阻塞等待一个连接
// 读请求行 → 拆出 method / path / protocol
// 读头直到空行 → 拼响应(状态行 + 头 + body)
}
要点:「HTTP 服务器」的本质就是「接受 TCP 连接、按文本协议收发字节」。V1 让你看清,所谓 Web 容器,最初不过是对这个循环的一层又一层封装。
三、V2:把请求/响应/会话对象化
V1 里请求头是一堆散落的字符串。V2 引入结构化模型:
Request/Response:把一次交互封装成对象;Application:一个带attributes(ConcurrentHashMap)、initParameters、serverInfo的容器上下文——这就是后来ServletContext的雏形;Session+SessionManager:用ConcurrentHashMap维护会话表,为「跨请求记住用户」打下基础。
// HttpWebServerV2/src/Application.java(节选)
public class Application {
private Map<String, Object> attributes; // ConcurrentHashMap
private Map<String, String> initParameters;
public Application() {
this.attributes = new ConcurrentHashMap<>();
this.serverInfo = "SimpleWebServer/1.0";
}
}
四、V3:Servlet 抽象与生命周期
到了 V3,「按 URL 分发到不同处理逻辑」被抽象成 Servlet。GenericServlet 提供 init()/service()/destroy() 三段式生命周期,HttpServlet 再按方法拆出 doGet/doPost:
// HttpWebServerV3/src/GenericServlet.java(节选)
public abstract class GenericServlet implements Servlet, ServletConfig {
public abstract void init(); // 由子类覆写
public final void init(ServletConfig config) {
this.config = config;
this.initialized = true;
init(); // 容器注册后只调用一次
}
public abstract void service(Request request, Response response); // 按方法分发由子类实现
}
这一步对应课程模块 2 的核心:Servlet 是「容器托管的处理单元」,容器负责它的创建、复用与销毁。
交互实验:Servlet 生命周期(ServletLifecycleMachine)
点下面的「容器事件」按钮,看 Tomcat 按什么顺序加载类、new 实例、调用 init / service / destroy。 「客户端请求」可以连点多次(service 可重复);「卸载应用」之后一切结束。
当前:未加载 —— 容器尚未加载该 Servlet 类
容器调用日志
- 尚未加载,等待容器处理
三个生命周期方法
| 方法 | 调用次数 | 做了什么 |
|---|---|---|
| init() | 一次 | 实例化后只执行一次:读配置、开数据库连接等一次性准备 |
| service() | 可多次 | 每次请求调用一次;按请求方法分发到 doGet(req,res) / doPost(req,res),可重复调用 |
| destroy() | 一次 | 卸载前只执行一次:释放资源(关连接等),之后对象不再被使用 |
web.xml 中声明并映射 Servlet
<servlet>
<servlet-name>HelloServlet</servlet-name>
<servlet-class>examples.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>HelloServlet</servlet-name>
<url-pattern>/hello.svc</url-pattern>
</servlet-mapping>HttpServletRequest / HttpServletResponse 常用方法
| 对象 | 方法 | 用途 |
|---|---|---|
| request | getParameter(name) | 取表单/查询参数(String,无此参数返回 null) |
| request | getParameterValues(name) | 取同名多值参数(String[],如复选框) |
| request | getSession() | 取当前会话(没有则创建),跨请求保存用户状态 |
| response | setContentType(...) | 设响应类型与编码,如 text/html;charset=UTF-8 |
| response | getWriter() | 取字符输出流,把 HTML/文本写入响应正文 |
Servlet 实例是「单例多线程」的:所有请求共享同一实例,service() 可能被多个线程并发调用。因此不要用实例变量保存本次请求的数据(会串数据),请求相关状态一律用局部变量;确需共享时用同步块或 ThreadLocal。
五、V4:Filter 链——横切关注点
日志、编码、鉴权这类「每个请求都要做一遍」的逻辑,不该塞进每个 Servlet。V4 用 Filter 把它们抽出来,LoggingFilter 包裹真正的处理逻辑,先记一行日志再放行:
// HttpWebServerV4/src/LoggingFilter.java(节选)
public void doFilter(Request req, Response res, FilterChain chain) {
log(req.getMethod(), req.getPath()); // 放行前记 Method/Path
chain.doFilter(req, res); // 放行给下一个环节
log(res.getStatus()); // 回来时记状态码
}
交互实验:Filter 链执行顺序(FilterChainVisualizer)
请求向下依次穿过编码 → 权限 → 日志三个 Filter 才到达 Servlet;响应向上按相反顺序返回。 点「下一步」逐帧走查;切到「权限校验失败」看链如何被短路。
↓ 向下放行EncodingFilter:设置请求/响应字符编码为 UTF-8,放行
链执行日志
- 1.EncodingFilter:设置请求/响应字符编码为 UTF-8,放行
chain.doFilter() 调用模式(对照 LoggingFilter.java)
public void doFilter(Request req, Response res, FilterChain chain) {
// 前置处理:编码 / 权限校验 / 日志 …
chain.doFilter(req, res); // 放行:交给下一个 Filter 或目标 Servlet
// 后置处理:仅当下游没有短路时才会执行到这里
}web.xml 中声明并映射 Filter
<filter>
<filter-name>EncodingFilter</filter-name>
<filter-class>examples.EncodingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>EncodingFilter</filter-name>
<url-pattern>/hello.svc</url-pattern>
</filter-mapping>
…(AuthFilter、LoggingFilter 同式声明,映射同一 url-pattern)六、V5:Listener 与 Session 事件
「会话被创建/销毁时做点什么」这类事件驱动逻辑,交给 Listener。SessionCounterListener 在会话增减时维护一个在线计数——这正是 HttpSessionListener 的仿真:
// HttpWebServerV5/src/SessionCounterListener.java(节选)
private final AtomicInteger activeSessions = new AtomicInteger(0);
public void sessionCreated(HttpSessionEvent se) {
int n = activeSessions.incrementAndGet(); // +1,并回写应用属性供页面展示
}
public void sessionDestroyed(HttpSessionEvent se) {
int n = activeSessions.decrementAndGet(); // -1,同理回写
}
七、V6:Spring MVC 仿真——前端控制器
V6 把「分发」这件事从手写 if-else 升级成注解驱动的前端控制器。MvcDispatcher 扮演 DispatcherServlet:扫描带 @Controller 的类,用 @RequestMapping 建立 URL→方法映射;HelloController 只写业务:
// HttpWebServerV6/src/example/HelloController.java(节选)
@Controller
@RequestMapping("/springhello")
public class HelloController {
@RequestMapping("/world")
public String helloWorld(Request request, Response response) {
return "hello"; // 返回视图名,由视图解析器渲染对应模板
}
}
// HttpWebServerV6/src/com/simspring/bootstrap/MvcDispatcher.java(节选)
// 启动时扫描 @Controller,建立 URL → HandlerMethod 的映射表;
// 每次请求:查表 → 反射调用方法 → 渲染视图
这一步把课程模块 5 的「前端控制器 + 注解映射」从概念落到可运行的代码。
八、V7:ORM 层——动态代理生成 SQL
V7 补上数据访问层,思路与 MyBatis 一致:接口 + 注解 + 动态代理。MapperProxy 实现 InvocationHandler,运行时读取方法上的 @Select/@Insert/@Update/@Delete 注解,拼出 SQL 并执行:
// HttpWebServerV7/src/com/simspring/orm/MapperProxy.java(节选)
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.isAnnotationPresent(Select.class)) {
String sql = method.getAnnotation(Select.class).value();
// 按泛型返回类型区分 List / 单对象,交给 mockDatabase 执行
return mockDatabase.executeSelectList(sql, elementType, args);
}
// … 同理处理 @Insert / @Update / @Delete
}
配合 SqlSessionFactory、RepositoryBeanPostProcessor,一个纯接口就能「凭空」得到可用的数据访问对象——这正是模块 8 MyBatis 的精髓。
九、小结:容器是「一层层包出来的」
| 能力 | V1→V7 里由谁提供 |
|---|---|
| 收发字节 | V1 裸 Socket |
| 请求/会话模型 | V2 Request/Response/Session |
| 处理单元 + 生命周期 | V3 Servlet |
| 横切逻辑 | V4 Filter |
| 事件响应 | V5 Listener |
| 注解分发 | V6 Spring MVC 仿真 |
| 数据访问 | V7 ORM 动态代理 |
读完这条链,再回看 Tomcat 里 DispatcherServlet、Filter、Listener、MyBatis 那一堆概念,你会发现它们不是凭空出现的规范,而是「手写服务器写不下去时」自然长出来的分层。
学习小测验
V1 版本里,HTTP 服务器最底层的本质是( )
从 V2 开始引入 Application(带 attributes/initParameters)对象,它对应后来 Servlet 规范里的哪个概念?
V4 的 LoggingFilter 之所以能把「记日志」从每个 Servlet 里抽出来,靠的是( )
V7 的 MapperProxy 让一个纯接口方法(带 @Select)能直接执行 SQL,其核心机制是( )