Tomcat源码分析之手撕Java Web服务器需要准备哪些工作

作为后端开发人员,源码在实际工作中,分析服务Web 服务器的手准备使用频率极高 ,而在众多 Web 服务器中 ,器需Tomcat 作为不可或缺的工作重要框架,理应成为我们必须学习和掌握的源码重点。

Tomcat 本质上是分析服务一个 Web 框架 ,那么它的手准备内部机制究竟是如何运作的呢 ?若不依赖 Tomcat  ,我们是器需否有能力自行构建一个 Web 服务器呢?

首先 ,建站模板Tomcat 的工作内部实现极为复杂,涵盖众多组件。源码我们将在后续章节中对这些细节展开深入探讨 。分析服务 其次 ,手准备本章将带领大家亲手构建一个 Web 服务器。器需

接下来,工作让我们一起动手 ,实现一个简易的 Web 服务器吧。

(【注】 :参考自《How Tomcat Works》一书)

什么是 Http

HTTP 是一种协议,全称为超文本传输协议 ,它使得 Web 服务器与浏览器能够通过互联网传输与接收数据,属于一种请求/响应的源码下载通信机制 。HTTP 协议的底层依赖于 TCP 协议进行数据传输  。目前,HTTP 已经演进至 2.x 版本,历经从 0.9 、1.0 、1.1 到如今的 2.x,每次迭代都为协议增加了许多新功能  。

在 HTTP 的通信模式中,始终由客户端发起请求 ,服务器接收到请求后处理相应的逻辑 ,并在处理完成后返回响应数据 。免费模板客户端接收完数据后 ,请求流程结束 。在此过程中,客户端和服务器均可以对已建立的连接进行中断操作 ,譬如通过浏览器的停止按钮来终止连接。

具体 Http 可参考 :

对线面试官 - Http 经典面试题

Http 请求

一个 HTTP 协议的请求由三部分组成:

请求行:包括请求方法、URI 和协议/版本,如GET /index.html HTTP/1.1。请求头部 :包含各种元数据信息 ,如主机地址 、用户代理 、内容类型等,用于描述客户端和请求的香港云服务器相关信息。请求主体 :用于传输实际数据 ,通常在 POST 或 PUT 请求中包含,如表单数据或文件内容 。

例如:

复制POST /api/gateway/test HTTP/1.1 Accept: application/json Accept-Encoding: gzip, deflate, br, zstd Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6 Authorization: Bearer eyJhbGiOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMywidXNlcl9uYW1lIjoicWluZ3l1Iiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTcyMzkyMzgyMywiYXV0aG9yaXRpZXMiOlsiNDQiLCIzOCJdLCJqdGkiOiIwMzBlMjJlOC0xYTk2LTRkOWQtOTY5ZC0zYzA4ZGNjOTVkNTQiLCJjbGllbnRfaWQiOiJxbXMtYWRtaW4iLCJ1c2VybmFtZSI6InFpbmd5dSJ9.EAlw27ZlHSULReScmD3Au740bNDc0zP8r4FfrDswUMLBheEzfEDp68skbhdqn3LWm3o6wpAcYq6lIOsZn2n6SLyPTh2MrhyiU4v6og6UasJ-DnajPyQ8f1RvM-YjLIlXira3KxSFR0QITsc7IH_XQJKJOI5ipYt3hwb44FITRqyAZk7usnTmWaTvuzTGKCkhO05Yi1b-U8N-6y22Gn6AkGBgABkiXceiq6Uv9ZXj7E2dPGBEpyASrr-Zop2wPCgpl8BxHp0adoBcEophMakEj7btRhXh7f4vXMxdnO6MqT3gZI94y8c-Hp44hZlhnkzs7EA2JyG8vf22TDDLiLTCxg Connection: keep-alive Content-Length: 64 Content-Type: application/json; charset=UTF-8 Cookie: JSESSIONID=8757AA1D1D00449F8B37FFFE3C50F00A Host: note.clubsea.cn Origin: https://note.clubsea.cn Referer: https://note.clubsea.cn/ Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-origin User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0 access-control-allow-credentials: true lang: zh-cn sec-ch-ua: "Not)A;Brand";v="99", "Microsoft Edge";v="127", "Chromium";v="127" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "macOS"1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.

数据的第一行包含请求方法、URI 、协议和版本。在此例中 ,方法为 POST ,URI 为/api/gateway/test,协议为HTTP/1.1,协议版本为 1.1。各部分通过空格进行分隔。

请求头部从第二行开始,采用英文冒号(:)分隔键和值。高防服务器请求头部与主体内容之间通过一个空行隔开。在此例中 ,请求主体为表单数据 。

http 协议-响应

类似于 HTTP 协议的请求,响应也由三部分构成 :

响应行 :包括协议 、状态码和状态描述 ,如HTTP/1.1 200 OK 。响应头部:包含各种元数据信息,如内容类型 、服务器信息 、日期等 ,用于描述服务器和响应的相关信息 。响应主体:传输实际数据的部分,亿华云例如网页内容或文件数据 。 复制HTTP/1.1 200 OK Content-Type: application/json Transfer-Encoding: chunked Connection: keep-alive Server: nginx Date: Sat, 17 Aug 2024 15:44:03 GMT Access-Control-Allow-Origin: https://note.clubsea.cn Access-Control-Allow-Credentials: true Access-Control-Expose-Headers: * Access-Control-Max-Age: 18000L X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block Cache-Control: no-cache, no-store, max-age=0, must-revalidate Pragma: no-cache Expires: 0 X-Frame-Options: DENY Referrer-Policy: no-referrer Access-Control-Allow-Origin: * Access-Control-Allow-Credentials: true Access-Control-Allow-Methods: GET, POST, OPTIONS Access-Control-Allow-Headers: token,DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,XRequested-With Strict-Transport-Security: max-age=157680001.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.

第一行HTTP/1.1 200 OK表示协议 、状态码和状态描述。随后是响应头部部分。响应头部与主体内容之间由一个空行分隔 。

什么是 Socket

Socket ,即套接字 ,是网络连接中的一个端点(end point) ,它使得应用程序能够在网络上读取和写入数据 。通过连接,不同计算机上的不同进程能够互相发送和接收数据 。如果应用 A 希望向应用 B 发送数据,A 应用需要知道 B 应用的 IP 地址以及 B 应用开放的套接字端口。在 Java 中,java.net.Socket类用来表示一个套接字 。

java.net.Socket最常用的构造方法为:public Socket(String host, int port); ,其中host表示主机名或 IP 地址 ,port表示套接字端口 。接下来,我们来看一个具体的例子 :

复制import java.io.*; import java.net.Socket; public class SocketExample { public static void main(String[] args) { try { // 创建Socket连接到本地服务器,端口号为8080 Socket socket = new Socket("127.0.0.1", 8080); // 获取输出流以发送数据 OutputStream os = socket.getOutputStream(); PrintWriter out = new PrintWriter(new OutputStreamWriter(os), true); // 获取输入流以接收数据 BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); // 发送HTTP请求 out.println("GET /index.jsp HTTP/1.1"); out.println("Host: localhost:8080"); out.println("Connection: Close"); out.println(); // 结束请求头 // 读取并输出响应 StringBuilder response = new StringBuilder(); String line; while ((line = in.readLine()) != null) { response.append(line).append("\n"); } // 输出响应内容 System.out.println(response.toString()); // 关闭流和socket连接 in.close(); out.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); } } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.

这个示例代码做了以下几点 :

连接到本地服务器的 8080 端口。通过输出流发送 HTTP 请求。(通过socket.getOutputStream()方法可以发送数据)通过输入流读取服务器响应。(通过socket.getInputStream()方法可以读取数据。)关闭连接和流。ServerSocket

Socket 表示一个客户端套接字,每次需要发送或接收数据时,都需要创建一个新的 Socket 。相对而言 ,服务器端的应用程序需要考虑更多因素,因为服务器需要随时待命  ,无法预测何时会有客户端连接 。为此 ,在 Java 中 ,我们使用java.net.ServerSocket来表示服务器端的套接字。

与 Socket 不同,ServerSocket 需要等待客户端的连接请求 。一旦有客户端连接  ,ServerSocket 会创建一个新的 Socket 与客户端进行通信。

ServerSocket 提供了多种构造方法,我们可以举一个常用的例子 。

复制import java.io.*; import java.net.*; public class ServerSocketExample { public static void main(String[] args) { try { // 创建ServerSocket对象,绑定到端口8080 ,连接请求队列长度为1,仅绑定到指定的本地IP地址 InetAddress bindAddress = InetAddress.getByName("127.0.0.1"); ServerSocket serverSocket = new ServerSocket(8080, 1, bindAddress); System.out.println("Server is listening on port 8080, bound to " + bindAddress); // 等待客户端连接 Socket clientSocket = serverSocket.accept(); System.out.println("Client connected!"); // 获取输入流以接收客户端数据 BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); // 获取输出流以发送数据到客户端 PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true); // 读取客户端发送的请求 String inputLine; while ((inputLine = in.readLine()) != null) { System.out.println("Received: " + inputLine); if (inputLine.isEmpty()) { break; // 请求头结束,退出循环 } } // 发送HTTP响应到客户端 out.println("HTTP/1.1 200 OK"); out.println("Content-Type: text/plain"); out.println("Connection: close"); out.println(); // 结束响应头 out.println("Hello, client!"); // 响应体内容 // 关闭流和socket连接 in.close(); out.close(); clientSocket.close(); serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.

这个示例代码完成了以下步骤 :

创建ServerSocket实例:8080是服务器监听的端口  。1是连接请求队列的长度,即最大等待连接数 。InetAddress.getByName("127.0.0.1")指定了绑定的本地 IP 地址 ,确保服务器只接受来自本地的连接。等待客户端连接  :serverSocket.accept()方法阻塞,直到有客户端连接进来。处理客户端连接 :读取客户端请求并打印 。发送一个简单的 HTTP 响应回客户端。清理资源 :关闭流和套接字以释放资源。

HttpServer

我们来看一个具体的例子  :

HttpServer表示一个服务器端的入口 ,它提供了一个main方法,并在 8080 端口上持续监听,直到有客户端建立连接。当客户端连接到服务器时  ,服务器通过生成一个 Socket 来处理该连接 。

复制import java.io.*; import java.net.*; public class HttpServer { /** * WEB_ROOT 是存放 HTML 和其他文件的目录  。 * 对于这个包  ,WEB_ROOT 是工作目录下的 "webroot" 目录 。 * 工作目录是从运行 `java` 命令时的文件系统位置。 */ public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot"; // 关闭命令的标识 private static final String SHUTDOWN_COMMAND = "/SHUTDOWN"; // 标记是否接收到关闭命令 private boolean shutdown = false; public static void main(String[] args) { // 创建 HttpServer 实例并启动等待请求 HttpServer server = new HttpServer(); server.await(); } /** * 等待客户端连接并处理请求 */ public void await() { ServerSocket serverSocket = null; int port = 8080; // 服务器监听的端口号 try { // 创建 ServerSocket 绑定到指定的端口和 IP 地址 serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1")); } catch (IOException e) { e.printStackTrace(); System.exit(1); // 如果创建 ServerSocket 失败 ,则退出程序 } // 循环等待并处理请求 while (!shutdown) { Socket socket = null; InputStream input = null; OutputStream output = null; try { // 等待客户端连接 socket = serverSocket.accept(); // 获取客户端请求的输入流和响应的输出流 input = socket.getInputStream(); output = socket.getOutputStream(); // 创建 Request 对象并解析请求 Request request = new Request(input); request.parse(); // 创建 Response 对象并设置请求 Response response = new Response(output); response.setRequest(request); // 发送静态资源响应 response.sendStaticResource(); // 关闭与客户端的连接 socket.close(); // 检查请求的 URI 是否为关闭命令 shutdown = request.getUri().equals(SHUTDOWN_COMMAND); } catch (Exception e) { e.printStackTrace(); // 处理异常并继续等待下一个请求 continue; } } // 关闭服务器套接字 try { serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.71.72.73.74.75.76.77.78.79.80.81.82.83. Request 对象

Request对象主要完成以下几项工作:

解析请求数据 :处理客户端发送的所有请求数据。解析 URI :从请求数据的第一行中提取和解析 URI  。 复制import java.io.*; public class Request { // 输入流 ,用于读取客户端发送的请求数据 private InputStream input; // 存储请求的 URI(统一资源标识符) private String uri; /** * 构造函数,初始化 Request 对象 * @param input 输入流,用于读取客户端请求数据 */ public Request(InputStream input) { this.input = input; } /** * 解析客户端请求 */ public void parse() { // 创建一个 StringBuffer 用于存储从输入流读取的请求数据 StringBuffer request = new StringBuffer(2048); int i; byte[] buffer = new byte[2048]; // 缓冲区大小为2048字节 try { // 从输入流读取数据到缓冲区 i = input.read(buffer); } catch (IOException e) { e.printStackTrace(); // 处理读取错误 i = -1; // 读取失败 } // 将缓冲区中的字节转换为字符,并追加到 request 中 for (int j = 0; j < i; j++) { request.append((char) buffer[j]); } // 输出请求内容到控制台 System.out.print(request.toString()); // 从请求内容中解析 URI uri = parseUri(request.toString()); } /** * 从请求字符串中提取 URI * @param requestString 请求的字符串 * @return 提取的 URI */ private String parseUri(String requestString) { int index1, index2; // 查找第一个空格的位置 ,标记请求方法的结束 index1 = requestString.indexOf( ); if (index1 != -1) { // 查找第二个空格的位置 ,标记请求 URI 的结束 index2 = requestString.indexOf( , index1 + 1); if (index2 > index1) { // 提取 URI 部分 return requestString.substring(index1 + 1, index2); } } // 如果未找到有效的 URI ,返回 null return null; } /** * 获取解析出的 URI * @return 请求的 URI */ public String getUri() { return uri; } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.71.72.73.74.75.76.77.78. Response 对象

Response主要负责向客户端发送文件内容(如果请求的 URI 指向的文件存在) 。

复制import java.io.*; public class Response { // 缓冲区的大小,用于读取文件内容 private static final int BUFFER_SIZE = 1024; // 请求对象 Request request; // 输出流 ,用于将响应数据写入客户端 OutputStream output; /** * 构造函数 ,初始化 Response 对象 * @param output 输出流 ,用于发送响应数据到客户端 */ public Response(OutputStream output) { this.output = output; } /** * 设置请求对象 * @param request 请求对象 */ public void setRequest(Request request) { this.request = request; } /** * 发送静态资源(如 HTML 文件)的响应 * @throws IOException 如果发生 I/O 错误 */ public void sendStaticResource() throws IOException { byte[] bytes = new byte[BUFFER_SIZE]; // 创建缓冲区 FileInputStream fis = null; // 文件输入流 try { // 获取请求 URI 对应的文件 File file = new File(HttpServer.WEB_ROOT, request.getUri()); if (file.exists()) { // 如果文件存在,读取文件内容并发送到客户端 fis = new FileInputStream(file); int ch = fis.read(bytes, 0, BUFFER_SIZE); // 读取文件内容到缓冲区 while (ch != -1) { output.write(bytes, 0, ch); // 写入输出流 ch = fis.read(bytes, 0, BUFFER_SIZE); // 继续读取文件内容 } } else { // 如果文件不存在,发送404错误响应 String errorMessage = "HTTP/1.1 404 File Not Found\r\n" + "Content-Type: text/html\r\n" + "Content-Length: 23\r\n" + "\r\n" + "<h1>File Not Found</h1>"; output.write(errorMessage.getBytes()); // 发送错误响应 } } catch (Exception e) { // 捕获并打印异常 System.out.println(e.toString()); } finally { // 确保文件输入流被关闭 if (fis != null) { fis.close(); } } } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.

总结

通过上述例子 ,我们惊喜地发现 ,在 Java 中实现一个 Web 服务器其实简单明了 ,代码也非常清晰!

既然我们能够如此轻松地实现一个 Web 服务器,那为何还需要 Tomcat 呢 ?它为我们提供了哪些组件和特性?这些组件又是如何组装起来的 ?后续章节将逐层解析这些问题 。

让我们共同期待接下来的深入分析 !

滇ICP备2023006006号-38