从零构建简易 Web 服务器:理解 Tomcat 工作原理的实践指南
作为后端开发人员,Web 服务器是日常工作中高频使用的工具,而 Tomcat 作为主流的 Web 服务器框架,其内部机制值得深入探究。本文将从 HTTP 协议基础出发,通过亲手构建一个简易 Web 服务器,帮助理解 Web 服务器的核心工作原理,为掌握 Tomcat 打下基础(参考自《How Tomcat Works》一书)。
一、Web 服务器的核心基础:HTTP 协议与 Socket 通信
1. HTTP 协议:请求与响应的规范
HTTP(超文本传输协议)是 Web 服务器与浏览器之间的数据传输规范,基于 TCP 协议实现请求 / 响应模式,由客户端主动发起请求,服务器处理后返回响应。其版本已从 0.9 演进至 2.x,功能不断丰富。
(1)HTTP 请求结构
一个完整的 HTTP 请求包含三部分:
示例请求:
plaintext
POST /api/gateway/test HTTP/1.1
Accept: application/json
Content-Type: application/json; charset=UTF-8
Host: note.clubsea.cn
Content-Length: 64
{"id":1,"name":"test"} // 请求主体(与头部以空行分隔)(2)HTTP 响应结构
响应与请求对应,同样包含三部分:
示例响应:
plaintext
HTTP/1.1 200 OK
Content-Type: application/json
Server: nginx
Content-Length: 30
{"code":0,"message":"success"} // 响应主体(与头部以空行分隔)2. Socket:网络通信的端点
Socket(套接字)是网络中不同进程间通信的端点,客户端与服务器通过 Socket 建立连接并传输数据。在 Java 中,java.net.Socket代表客户端套接字,java.net.ServerSocket代表服务器套接字。
(1)客户端 Socket 示例
通过 Socket 向服务器发送 HTTP 请求:
java
运行
import java.io.*;import java.net.Socket;public class SocketClientExample {
public static void main(String[] args) throws IOException {
// 连接本地8080端口的服务器
Socket socket = new Socket("127.0.0.1", 8080);
// 获取输出流发送请求
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
out.println("GET /index.html HTTP/1.1");
out.println("Host: localhost:8080");
out.println("Connection: Close");
out.println(); // 空行表示请求头部结束
// 获取输入流接收响应
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
String line;
while ((line = in.readLine()) != null) {
System.out.println(line);
}
// 关闭资源
in.close();
out.close();
socket.close();
}}(2)服务器端 ServerSocket 示例
服务器通过 ServerSocket 监听端口,接收客户端连接并处理:
java
运行
import java.io.*;import java.net.ServerSocket;import java.net.Socket;public class ServerSocketExample {
public static void main(String[] args) throws IOException {
// 绑定8080端口,监听客户端连接
ServerSocket serverSocket = new ServerSocket(8080, 1,
InetAddress.getByName("127.0.0.1"));
System.out.println("服务器启动,监听8080端口...");
// 阻塞等待客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接");
// 读取客户端请求
BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream())
);
String requestLine;
while ((requestLine = in.readLine()) != null && !requestLine.isEmpty()) {
System.out.println("收到请求:" + requestLine);
}
// 发送响应
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
out.println("HTTP/1.1 200 OK");
out.println("Content-Type: text/plain");
out.println("Connection: Close");
out.println();
out.println("Hello, Client!");
// 关闭资源
in.close();
out.close();
clientSocket.close();
serverSocket.close();
}}二、动手实现简易 Web 服务器
一个基础的 Web 服务器需具备以下功能:监听端口、接收客户端连接、解析 HTTP 请求、处理请求(如返回静态资源)、发送 HTTP 响应。我们将通过三个核心类实现这一流程:HttpServer(服务器主类)、Request(请求解析)、Response(响应处理)。
1. 服务器主类:HttpServer
负责启动服务器、监听端口、循环处理客户端连接。
java
运行
import java.io.*;import java.net.*;public class HttpServer {
// 静态资源存放目录(当前工作目录下的webroot文件夹)
public static final String WEB_ROOT =
System.getProperty("user.dir") + File.separator + "webroot";
// 关闭服务器的命令URI
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
// 标记是否需要关闭服务器
private boolean shutdown = false;
public static void main(String[] args) {
HttpServer server = new HttpServer();
server.await(); // 启动服务器并等待请求
}
/**
* 监听端口,循环处理客户端连接
*/
public void await() {
ServerSocket serverSocket = null;
int port = 8080; // 监听端口
try {
// 绑定端口和本地IP
serverSocket = new ServerSocket(port, 1,
InetAddress.getByName("127.0.0.1"));
} catch (IOException e) {
e.printStackTrace();
System.exit(1); // 启动失败则退出
}
// 循环处理请求,直到收到关闭命令
while (!shutdown) {
Socket socket = null;
InputStream input = null;
OutputStream output = null;
try {
// 等待客户端连接(阻塞方法)
socket = serverSocket.accept();
input = socket.getInputStream();
output = socket.getOutputStream();
// 解析请求
Request request = new Request(input);
request.parse();
// 处理响应
Response response = new Response(output);
response.setRequest(request);
response.sendStaticResource(); // 发送静态资源
// 关闭连接
socket.close();
// 检查是否为关闭命令
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
} catch (Exception e) {
e.printStackTrace();
continue;
}
}
// 关闭服务器
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}}2. 请求解析类:Request
负责从输入流中读取 HTTP 请求数据,并解析出 URI。
java
运行
import java.io.*;public class Request {
private InputStream input; // 客户端输入流
private String uri; // 解析出的URI
public Request(InputStream input) {
this.input = input;
}
/**
* 解析HTTP请求,提取URI
*/
public void parse() {
// 读取请求数据
StringBuffer requestBuffer = new StringBuffer(2048);
byte[] buffer = new byte[2048];
int bytesRead;
try {
bytesRead = input.read(buffer);
} catch (IOException e) {
e.printStackTrace();
bytesRead = -1;
}
// 将字节转换为字符串
for (int i = 0; i < bytesRead; i++) {
requestBuffer.append((char) buffer[i]);
}
System.out.println("请求内容:\n" + requestBuffer.toString());
// 解析URI
uri = parseUri(requestBuffer.toString());
}
/**
* 从请求字符串中提取URI(格式:方法 URI 协议版本)
*/
private String parseUri(String requestString) {
int index1 = requestString.indexOf(' '); // 第一个空格(方法结束)
int index2 = requestString.indexOf(' ', index1 + 1); // 第二个空格(URI结束)
if (index1 != -1 && index2 > index1) {
return requestString.substring(index1 + 1, index2);
}
return null;
}
public String getUri() {
return uri;
}}3. 响应处理类:Response
负责根据请求的 URI 查找静态资源,并向客户端发送 HTTP 响应(包括文件内容或错误信息)。
java
运行
import java.io.*;public class Response {
private static final int BUFFER_SIZE = 1024; // 缓冲区大小
private Request request; // 关联的请求对象
private OutputStream output; // 客户端输出流
public Response(OutputStream output) {
this.output = output;
}
public void setRequest(Request request) {
this.request = request;
}
/**
* 发送静态资源(如HTML、CSS文件)
*/
public void sendStaticResource() throws IOException {
byte[] buffer = 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 bytesRead = fis.read(buffer, 0, BUFFER_SIZE);
while (bytesRead != -1) {
output.write(buffer, 0, bytesRead);
bytesRead = fis.read(buffer, 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



