2025-12-25
网络编程
0

目录

如何实现一个简单的 Tomcat Web 容器?
创建 BIO 模型的 Web 服务器
映射器工厂处理请求
控制器的实现
整体的测试效果

在写完《Java 网络编程原理 - 应用程序是如何建立 TCP 连接并响应网络请求的?》 一文以后,也是了解了 Tomcat 的底层原理,同时掌握了网络通信的原理和各种序列化方式,我就想着如何自己实现一个 Tomcat web 容器呢?

核心功能是来接收浏览器的请求,同时根据请求过来的信息,进行路由到接口上进行数据处理,根据最后处理的数据结果进行响应。

image.png

如何实现一个简单的 Tomcat Web 容器?

我得构架设计思路图如下,实现一个简单的 Web 容器:

image.png

创建 BIO 模型的 Web 服务器

通过 main 方法进行启动,结合 ServerSocket 创建一个 TCP 服务器,监听 8080 端口,通过多线程处理多个客户端连接,当然也可以改用 NIO 模式,一个线程足以。

handleClient 方法中是对客户端的数据流进行读取和响应,通过输入流读取 HTTP 请求,封装成自定义的 MyServletRequest 请求对象。

java
/** * Web 服务器 * * @author Created by Liushigong on 2025-12-24 */ public class WebTcpServer { /** * 服务器监听的端口 */ private static final int SERVER_PROT = 8080; /** * 线程池,用于处理客户端连接 */ private static final Executor executor = Executors.newFixedThreadPool(10); /** * 启动服务 */ public static void start() { // 创建服务器Socket ServerSocket serverSocket = null; try { serverSocket = new ServerSocket(SERVER_PROT); // 持续监听新的连接 while (true) { // 阻塞,直到有客户端连接 Socket clientSocket = serverSocket.accept(); // 为每个客户端连接创建一个新的线程来处理,避免阻塞主线程 executor.execute(() -> handleClient(clientSocket)); } } catch (IOException e) { System.err.printf("服务器启动或运行异常: %s\n", e.getMessage()); } finally { // 关闭服务器socket if (serverSocket != null && !serverSocket.isClosed()) { try { serverSocket.close(); System.out.println("服务器已关闭。"); } catch (IOException e) { System.err.printf("关闭服务器socket时发生错误: %s\n", e.getMessage()); } } } } private static void handleClient(Socket clientSocket) { try ( // 使用 try-with-resources 确保流自动关闭 BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); OutputStream clientOut = clientSocket.getOutputStream(); ) { // 构建请求对象 MyServletRequest request = new MyServletRequest(); // 读取客户端消息 String clientMessage; // 持续读取客户端消息 while ((clientMessage = in.readLine()) != null) { // 解析请求成功后,返回响应 if (parse(request, clientMessage)) { // 调用请求接口 MyServletResponse response = MappingFactory.invoke(request); // 发送HTTP响应头 clientOut.write(response.getResponseHeaders().getBytes(StandardCharsets.UTF_8)); // 发送响应体 clientOut.write(response.getResponseData()); clientOut.flush(); break; } ; } } catch (IOException e) { System.err.printf("处理客户端连接时发生错误: %s\n", e.getMessage()); } finally { try { // 关闭客户端socket clientSocket.close(); } catch (IOException e) { System.err.printf("关闭客户端socket时发生错误: %s\n", e.getMessage()); } } } public static boolean parse(MyServletRequest request, String message) { // 当前线程的对象 if (message.contains("GET") || message.contains("POST")) { String data = message.substring(message.indexOf("/"), message.indexOf(" HTTP/")); request.setPath(data); } else if (message.contains("Host")) { String data = message.substring(message.indexOf(" ") + 1); request.setHost(data); } else if (message.contains("sec-ch-ua-platform")) { String data = message.substring(message.indexOf("\"") + 1, message.lastIndexOf("\"")); request.setPlatform(data); } // 遇到空行,表示HTTP头部结束 return message.isEmpty(); } public static void main(String[] args) { // 初始化控制器 WebController.init(); // 启动服务 start(); } }

映射器工厂处理请求

映射器工厂中存储了 url 与控制器对象和方法的缓存信息,当数据流读取完成以后,根据请求路径从 Map 缓存中查找对应的 Servlet 进行反射调用,获取响应数据进行返回。

java
import java.lang.reflect.Method; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * 映射器工厂 * * @author Created by Liushigong on 2025-12-24 */ public class MappingFactory { /** * url 与 方法映射 */ private static final Map<String, Method> mapping = new ConcurrentHashMap<>(); /** * url 与 对象映射 */ private static final Map<String, Object> objectMap = new ConcurrentHashMap<>(); /** * 注册映射关系 * * @param path url * @param object 对象 * @param method 方法 */ public static void register(String path, Object object, Method method) { mapping.put(path, method); objectMap.put(path, object); } public static Method get(String path) { return mapping.get(path); } /** * 调用方法 * * @param request 请求信息 * @return 响应信息 */ public static MyServletResponse invoke(MyServletRequest request) { // 请求信息 MyServletResponse response = new MyServletResponse(); if (request.getPath() == null) { response.setStatus(400); response.setBody("<html><body><h1>400 Bad Request</h1></body></html>"); } else if (!objectMap.containsKey(request.getPath())) { response.setStatus(404); response.setBody("<html><body><h1>404 Not Found</h1></body></html>"); } else { try { // 调用方法 Object body = get(request.getPath()).invoke(objectMap.get(request.getPath()), request); response.setBody(String.valueOf(body)); } catch (Exception e) { response.setStatus(500); response.setBody("<html><body><h1>500 Internal Server Error</h1></body></html>"); } } return response; } }

控制器的实现

定义 @WebPath 注解用来标识控制器的接口信息,此处在启动时直接调用 init() 初始化控制器的方法,到映射器工厂中进行缓存。

java
import cn.hutool.core.util.ReflectUtil; import java.lang.reflect.Method; import java.util.List; /** * 自定义控制器 * * @author Created by Liushigong on 2025-12-24 */ public class WebController { @WebPath(path = "/test") public String test(MyServletRequest request) { return request.getPath() + " 调用成功,噢耶!"; } @WebPath(path = "/test1") public String test1(MyServletRequest request) { return request.getPath() + " 调用成功,噢耶!"; } @WebPath(path = "/test2") public String test2(MyServletRequest request) { return request.getPath() + " 调用成功,噢耶!"; } @WebPath(path = "/test3") public String test3(MyServletRequest request) { throw new RuntimeException("测试异常"); } /** * 初始化控制器 */ public static void init(){ // 当前对象 WebController webController = new WebController(); // 获取所有带 @WebPath 注解的方法 List<Method> publicMethods = ReflectUtil.getPublicMethods(WebController.class, method -> method.isAnnotationPresent(WebPath.class)); for (Method method : publicMethods) { WebPath webPath = method.getAnnotation(WebPath.class); // 注册到映射器工厂 MappingFactory.register(webPath.path(), webController, method); } } }

自定义注解,用来标识接口信息

java
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface WebPath { /** * URL路径地址 */ String path(); }

自定义请求对象

java
@Data public class MyServletRequest { /** * 请求路径 */ private String path; /** * 请求主机 */ private String host; /** * 请求平台 */ private String platform; }

自定义的响应体对象

java
import lombok.Data; import java.nio.charset.StandardCharsets; /** * @author Created by Liushigong on 2025-12-24 */ @Data public class MyServletResponse { /** * 响应状态码 */ private int status = 200; /** * 响应体类型 */ private String contentType = "text/html;charset=UTF-8"; /** * 响应体 */ private String body; /** * 获取响应体字节数组 */ public byte[] getResponseData() { return body.getBytes(StandardCharsets.UTF_8); } /** * 生成HTTP响应头 */ public String getResponseHeaders() { // 空行分隔头和体 return "HTTP/1.1 " + status + " OK\r\n" + "Content-Type: " + contentType + "\r\n" + "Content-Length: " + getResponseData().length + "\r\n" + "Connection: close\r\n" + "\r\n"; } }

整体的测试效果

当页面上访问 http://localhost:8080/test 时,效果如下

image.png

访问 http://localhost:8080/test3 时,效果如下

image.png

访问 http://localhost:8080/test4 时,效果如下

image.png

短短几个类就能实现这么一个轻量级的web服务器,整体效果还是很不错的。

上述实现省略了 Servlet 解析的过程,直接通过 WebController.init() 进行的初始化,扩展性上我并没有做太多处理,这主要是因为当前我并没有交给 Spring IOC 容器管理 Bean 对象。

如果想要集成 Spring,可以依赖 spring-contextspring-core 等jar包,以下是启动IOC容器的方式:

java
// 启动 IOC 容器 ConfigurableApplicationContext applicationContext = new AnnotationConfigApplicationContext(); applicationContext.refresh();

将 Servlet 注册成 Bean 对象以后,可以结合 BeanPostProcessor #postProcessAfterInitialization 后置处理,自定义个 @Controller 一类的注解,用来标识是控制器,然后自动解析请求接口,进行映射器自动注册的操作,后续通过请求进行分发路由进行调用。

像接口的拦截器,参数解析器和结果响应处理器等都可以进行补充,有兴趣的小伙伴可以看下我画的 Spring MVC 的执行流程图,自行扩展实现。

image.png

本文作者:柳始恭

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!