1. 首页 > 快讯

深入浅出 Tomcat 源码解析

其实深入浅出 Tomcat 源码解析的问题并不复杂,但是又很多的朋友都不太了解,因此呢,今天小编就来为大家分享深入浅出 Tomcat 源码解析的一些知识,希望可以帮助到大家,下面我们一起来看看这个问题的分析吧!

首先,Tomcat的内部实现极其复杂,涵盖了很多组件。我们将在后续章节中深入研究这些细节。其次,本章将带领你自己搭建一个Web服务器。

接下来让我们一起来实现一个简单的Web服务器。

(【注】:参见书《How Tomcat Works》)

什么是 Http

HTTP是一种协议,全称是超文本传输协议。它使网络服务器和浏览器能够通过互联网传输和接收数据。它是一种请求/响应通信机制。 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.1Accept: application/jsonAccept-Encoding: gzip, deflate, br, zstdAccept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en -US;q=0.6授权: 持有者eyJhbGiOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMywidXNlcl9uYW1lIjoicWluZ3l1Iiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTcyMzkyMzgyMy wiYX V0aG9yaXRpZXMiOlsiNDQiLCIzOCJdLCJqdGkiOiIwMzBlMjJLOC0xYTk2LTRkOWQtoTY5ZC0zYzA4ZGNjOTVkNTQiLCJjbGllbnRfaWQiOiJxbXMtYWRtaW4iLCJ1c2VybmFtZSI6InFpbm d5dS J9.EAlw27ZlHSULReScmD3Au740bNDc0zP8r4FfrDswUMLBheEzfEDp68skbhdqn3LWm3o6wpAcYq6lIOsZn2n6SLyPTh2MrhyiU4v6og6UasJ-DnajPyQ8f1RvM-YjLilXira3KxSFR0Q ITsc7IH_XQ JKJOI5ipYt3hwb44FITRqyAZk7usnTmWaTvuzTGKCkhO05Yi1b-U8N-6y22Gn6AkGBgABkiXceiq6Uv9ZXj7E2dPGBEpyASrr-Zop2wPCgpl8BxHp0adoBcEophMakEj7btRhXh7f4vX MxdnO6M qT3gZI94y8c-Hp44hZlhnkzs7EA2JyG8vf22TDDLiLTCxgConnection: keep-aliveContent-Length: 64Content-Type: application/json;字符集=UTF-8Cookie: JSESSIONID=8757AA1D1D00449F8 B37FFFE3C50F00AHost: note.clubsea.cnOrigin: https://note.clubsea.cnReferer: https://note.clubsea.cn/Sec-Fetch-Dest:emptySec-Fetch-Mode: corsSec-Fetch-Site: 同源User-Agent: Mozilla/5.0 (Macintosh ; Intel Mac OS X 10_15_7) AppleWebKit/537 .36 (KHTML,如Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0access-control-allow-credentials: truelang: zh-cnsec-ch-ua: '不)A ;Brand';v='99', 'Microsoft Edge';v='127', 'Chromium';v='127'sec-ch-ua-mobile:0sec-ch-ua-platform: 第一行' macOS 的数据包含请求方法、URI、协议和版本。本例中,方法为POST,URI为/api/gateway/test,协议为HTTP/1.1,协议版本为1.1。各部分之间用空格分隔。

请求头从第二行开始,使用英文冒号(:)分隔键和值。请求头和请求体内容之间用空行分隔。在此示例中,请求正文是表单数据。

http 协议-响应

与HTTP协议请求类似,响应也由三部分组成:

响应行:包括协议、状态码和状态描述,如HTTP/1.1 200 OK。响应头部:包含各种元数据信息,例如内容类型、服务器信息、日期等,用于描述服务器和响应相关信息。响应主体:传输实际数据的部分,如网页内容或文件数据。 HTTP/1.1 200 OKContent-Type: application/jsonTransfer-Encoding: chunkedConnection: keep-aliveServer: nginxDate: 2024 年8 月17 日星期六15:44:03 GMTAccess-Control-Allow-Origin: https://note.clubsea。 cnAccess-Control-Allow-Credentials: trueAccess-Control-Expose-Headers: *Access-Control-Max-Age: 18000LX-Content-Type-Options: nosniffX-XSS-Protection: 1; mode=blockCache-Control: 无缓存、无存储、max-age=0、必须revalidatePragma: no-cacheExpires: 0X-Frame-Options3336 0 DENYReferrer-Policy: no-referrerAccess-Control-Allow-Origin: *Access-Control-Allow- Credentials: trueAccess-Control-Allow-Methods: GET、POST、OPTIONSAccess-Control-Allow-Headers: token、DNT、X-Mx-ReqToken、Keep - Alive、User-Agent、XRequested-WithStrict-Transport-Security: max-age=15768000第一行HTTP/1.1 200 OK 表示协议、状态码和状态描述。接下来是响应标头。响应标头和正文内容由空行分隔。

什么是 Socket

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

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

import java.io.*;import java.net.Socket;public class SocketExample { public static void main(String[] args) { try { //创建一个Socket 连接本地服务器,端口号为8080 Socket 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: 关闭');输出.println(); //结束请求头//读取并输出响应StringBuilder response=new StringBuilder();串线; while ((line=in.readLine()) !=null ) { response.append(line).append('\n'); } //输出响应内容System.out.println(response.toString()); //关闭流和套接字连接in.close();输出.close();套接字.close(); } catch (IOException e) { e.printStackTrace();此示例代码执行以下操作:

连接到本地服务器的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, 绑定地址); System.out.println('服务器正在监听8080端口,绑定到'+bindAddress); //等待客户端连接Socket clientSocket=serverSocket.accept(); System.out.println('客户端已连接!'); //获取输入流接收客户端数据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()) { 中断; //请求头结束,退出循环} } //向客户端发送HTTP响应out.println('HTTP/1.1 200 OK'); out.println('Content-Type: 文本/纯文本'); out.println('Connection: 关闭');输出.println(); //结束响应头out.println('Hello, client!'); //响应正文内容//关闭流和套接字连接in.close();关闭(); clientSocket.close();服务器套接字。关闭(); } catch (IOException e) { e.printStackTrace(); } }}此示例代码完成以下步骤:

创建一个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();服务器.await(); } /** * 等待客户端连接并处理请求*/public void wait() { ServerSocket serverSocket=null;国际端口=8080; //服务器监听的端口号try { //创建一个ServerSocket 并将其绑定到指定的端口和IP 地址serverSocket=new ServerSocket(port , 1, InetAddress.getByName('127.0.0.1')); } catch (IOException e) { e.printStackTrace();系统.退出(1); //如果创建ServerSocket失败,则退出程序} //循环等待并处理请求while (!shutdown) { Socket socket=null;输入流输入=空; OutputStream 输出=null; try { //等待客户端连接socket=serverSocket.accept(); //获取客户端请求的输入流和Response输出流input=socket.getInputStream();输出=socket.getOutputStream(); //创建Request对象并解析请求Request request=new Request(input);请求.parse(); //创建一个Response 对象并设置它Request Response response=new Response(output);响应.setRequest(请求); //发送静态资源响应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(); } }}

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);整数我;字节[]缓冲区=新字节[2048]; //缓冲区大小为2048 个字Section try { //从输入流读取数据到缓冲区i=input.read(buffer); } catch (IOException e) { e.printStackTrace(); //处理读取错误i=-1; //Read Fetch failed} //将缓冲区中的字节转换为字符并将其追加到请求中for (int j=0; j i; j++) { request.append((char) buffer[j]); } //将请求内容输出到控制台System.out.print(request.toString()); //从请求内容中解析URI uri=parseUri(request.toString()); } /** * 从请求字符串中提取U

RI * @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; }}

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(); } } }}

总结

通过上述例子,我们惊喜地发现,在 Java 中实现一个 Web 服务器其实简单明了,代码也非常清晰! 既然我们能够如此轻松地实现一个 Web 服务器,那为何还需要 Tomcat 呢?它为我们提供了哪些组件和特性?这些组件又是如何组装起来的?后续章节将逐层解析这些问题。 让我们共同期待接下来的深入分析!

用户评论

喜欢梅西

tomcat源码好复杂啊,这篇文章看完了能帮我看清不少吗?

    有13位网友表示赞同!

旧事酒浓

平时开发只用框架不知道咋调用的那些东西,想从底部了解下

    有20位网友表示赞同!

滴在键盘上的泪

学习tomcat源代码之前确实需要做好充足的准备工作呀

    有19位网友表示赞同!

我就是这样一个人

手撕Java Web服务器,这目标太大了!

    有14位网友表示赞同!

残花为谁悲丶

51CTO的文章讲的挺专业的,看得懂吗?需要刷几次概念知识.

    有8位网友表示赞同!

心贝

原来写Web应用还要理解底层这么繁复的设计架构啊...

    有20位网友表示赞同!

Edinburgh°南空

我要是学了源码分析,是不是可以自己造一个服务器出来哈哈哈

    有18位网友表示赞同!

放血

想了解下tomcat的请求处理流程,这篇文章挺适合的呀.

    有16位网友表示赞同!

折木

希望这篇文章能详细解释一些技术细节,比如线程池的设计什么的.

    有14位网友表示赞同!

限量版女汉子

学习技术的路上要脚踏实地一步步来,从源码分析开始也不错

    有18位网友表示赞同!

别在我面前犯贱

最近在学Java Web开发,想深入了解下tomcat的原理,看看这篇文章学习一下吧.

    有13位网友表示赞同!

致命伤

看了标题就感觉难度不小啊...

    有20位网友表示赞同!

玻璃渣子

手撕tomcat是不是要看很多文档和代码?很费时间的一件事哈.

    有14位网友表示赞同!

墨城烟柳

期待文章能提供一些具体的例子和步骤,更容易理解

    有7位网友表示赞同!

呆檬

学习源码分析能提升开发效率吗?

    有10位网友表示赞同!

一个人的荒凉

感觉这种深入的学习方法对我的职业发展很有帮助呢。

    有7位网友表示赞同!

命里缺他

希望能把这些知识运用到实际项目中去,真真切切提高技能.

    有14位网友表示赞同!

ok绷遮不住我颓废的伤あ

希望这篇文章能够让我从源码层面更全面地了解tomcat!

    有9位网友表示赞同!

本文采摘于网络,不代表本站立场,转载联系作者并注明出处:https://www.iotsj.com//kuaixun/6304.html

联系我们

在线咨询:点击这里给我发消息

微信号:666666