一、前言
此项目用纯java手写了一个HTTP服务器(没有用Spring/Tomcat等框架),配合一个前端登录页面,实现了完整的前后端交互:
**后端:**监听9999端口,接收浏览器请求,返回HTML静态文件或JSON数据
**前端:**一个美观的登录界面,通过fetch API向后端发送登录请求
核心价值:从零理解HTTP协议和Web服务器的工作原理
二、后端Server.java核心知识点
2.1 HTTP 协议基础
浏览器和服务器之间通信使用 HTTP 协议,本质就是按特定格式发送的纯文本。
一个 HTTP 请求长这样:
POST /login HTTP/1.1 ← 请求行(方法 + 路径 + 协议版本)
Host: localhost:9999 ← 请求头(多个,每行一个 key: value)
Content-Type: application/x-www-form-urlencoded
Content-Length: 29
← 空行(分隔头和体)
username=admin&password=123456 ← 请求体(POST 才有)
一个 HTTP 响应长这样:
HTTP/1.1 200 OK ← 状态行(协议 + 状态码 + 状态文本)
Content-Type: application/json ← 响应头
Content-Length: 52
← 空行
{"success":true,"message":"登录成功"} ← 响应体
关键规则 :每行末尾是 \r\n(回车+换行),头和体之间有一个空行 (就是 \r\n\r\n)。
2.2 服务器启动流程
java
serverSocket = new ServerSocket(9999); // 1. 绑定 9999 端口
while (true) {
Socket socket = serverSocket.accept(); // 2. 阻塞等待浏览器连接
new Thread(new RequestHandler(socket)).start(); // 3. 每个连接开一个线程处理
}
知识点:
ServerSocket 是服务端的 Socket,accept() 会阻塞(停住等待),直到有客户端连接
while(true) 让服务器持续运行,不断接收新连接
new Thread(...).start() --- 每来一个请求就开一个新线程,这样多个浏览器可以同时访问(并 发)
2.3 请求解析(RequestHandler.run())
这是最值得学习的部分------手动解析 HTTP 请求的三个阶段:
第一阶段:解析请求行
java
String requestLine = reader.readLine(); // 读第一行: "POST /login HTTP/1.1"
String[] parts = requestLine.split(" ");
String method = parts[0]; // "POST"
String path = parts[1]; // "/login"
第二阶段:解析请求头
java
while ((line = reader.readLine()) != null && !line.isEmpty()) {
int colon = line.indexOf(":");
String key = line.substring(0, colon).trim(); // "Content-Length"
String val = line.substring(colon + 1).trim(); // "29"
headers.put(key, val);
}
关键 :请求头是按行读取的,遇到空行 (line.isEmpty())说明头结束了。
第三阶段:读取请求体
java
char[] body = new char[contentLength];
reader.read(body, 0, contentLength); // 按 Content-Length 读指定长度
String bodyStr = new String(body); // "username=admin&password=123456"
为什么不能直接 readLine() 读体? 因为请求体可能没有换行符,必须按 Content-Length 头指定的字节数来读。
2.4 路由分发
java
if ("POST".equals(method) && "/login".equals(path)) {
handleLogin(reader, out, contentLength); // POST /login → 登录验证
} else if ("GET".equals(method)) {
handleStaticFile(out, path); // GET → 返回静态文件
} else {
sendError(out, 405, "Method Not Allowed"); // 其他 → 405 错误
}
这就是一个极简的路由系统 :根据 HTTP 方法和路径,分发到不同的处理方法。Spring 的 @RequestMapping 底层也是这个原理,只是封装得更优雅。
2.5 GET --- 静态文件处理(handleStaticFile)
java
File file = new File(Server.rootDir, path).getCanonicalFile();
String rootCanonical = new File(Server.rootDir).getCanonicalPath();
// 防止路径穿越攻击
if (!file.getPath().startsWith(rootCanonical)) {
sendError(out, 403, "Forbidden");
return;
}
重点学习 --- 路径穿越攻击防护:
如果用户请求 GET /../../etc/passwd,不做防护的话可以读到服务器上的任意文件。getCanonicalPath() 会解析 .. 得到真实路径,然后检查它是否还在网站根目录内。
读取文件后,根据扩展名设置 Content-Type(MIME 类型):
java
private String getMimeType(String fileName) {
if (fileName.endsWith(".html")) return "text/html; charset=UTF-8";
if (fileName.endsWith(".css")) return "text/css; charset=UTF-8";
if (fileName.endsWith(".js")) return "application/javascript; charset=UTF-8";
if (fileName.endsWith(".png")) return "image/png";
// ...
return "application/octet-stream"; // 默认:二进制流
}
三、前端部分
前端部分主要由workBuddy完成,这里不作介绍