commit da8cd67432bca45641ac2dc152e54d8bdeeb8652 Author: maxf Date: Thu Jun 19 16:43:01 2025 +0800 从0到1实现一个web服务器 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30257a0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +*.log +*.class \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d1fe343 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +java 知识点学习 +--- + +1. [从0到1实现一个web服务器](demo1/web_server.md) + 1. 网络请求 + 2. HTTP协议 + 3. 文件读取 + 4. 动静态资源 + 5. 模板渲染 +2. `@Vaild`为什么能做到校验参数? + 1. 请求参数处理 + 2. 切面AOP + 3. 目标寻址(请求URL到目标方法ctrl) + 4. [GET] URL参数 + 5. [POST] application/x-www-form-urlencoded:表单数据 + 6. [POST] multipart/form-data:文件上传 + 7. [POST] application/json:JSON数据 + 8. [POST] text/xml:XML数据 +3. spring boot的启动原理 \ No newline at end of file diff --git a/demo1/pom.xml b/demo1/pom.xml new file mode 100644 index 0000000..073f0b1 --- /dev/null +++ b/demo1/pom.xml @@ -0,0 +1,11 @@ + + + 4.0.0 + top.yexuejc + demo1 + 1.0.0 + demo1 + 从0到1实现一个web服务器 + + diff --git a/demo1/src/main/java/top/yexuejc/demo/Request.java b/demo1/src/main/java/top/yexuejc/demo/Request.java new file mode 100644 index 0000000..e36e713 --- /dev/null +++ b/demo1/src/main/java/top/yexuejc/demo/Request.java @@ -0,0 +1,36 @@ +package top.yexuejc.demo; + +/** + * 请求对象类 + * @author maxiaofeng + * @date 2025/6/19 11:37 + */ +public class Request { + private final String method; + private final String path; + private final String body; + private final String params; + + public Request(String method, String path, String body, String params) { + this.method = method; + this.path = path; + this.body = body; + this.params = params; + } + + public String getMethod() { + return method; + } + + public String getPath() { + return path; + } + + public String getBody() { + return body; + } + + public String getParams() { + return params; + } +} diff --git a/demo1/src/main/java/top/yexuejc/demo/RequestHandler.java b/demo1/src/main/java/top/yexuejc/demo/RequestHandler.java new file mode 100644 index 0000000..3de205b --- /dev/null +++ b/demo1/src/main/java/top/yexuejc/demo/RequestHandler.java @@ -0,0 +1,219 @@ +package top.yexuejc.demo; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.Socket; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Objects; +import java.util.logging.Logger; + +/** + * 请求处理 + * + * @author maxiaofeng + * @date 2025/6/19 11:29 + */ +public class RequestHandler implements Runnable { + Logger logger = Logger.getLogger(RequestHandler.class.getName()); + private final String OS_NAME = System.getProperty("os.name"); + private final List STATIC_RESOURCES = List.of("css", "js", "img", "fonts", "favicon.ico"); + + private final Socket clientSocket; + private final String staticRoot; + private final String webRoot; + + public RequestHandler(Socket socket, String staticRoot, String webRoot) { + this.clientSocket = socket; + this.staticRoot = staticRoot; + this.webRoot = webRoot; + } + + @Override + public void run() { + try (InputStream input = clientSocket.getInputStream(); OutputStream output = clientSocket.getOutputStream()) { + + // 解析请求 + Request request = parseRequest(input); + + // 处理请求并生成响应(静态画面) + Response response = handleRequest(request); + + // 画面渲染(动态画面) + pageLive(request, response); + + // 发送响应 + sendResponse(output, response); + + } catch (Exception e) { + System.err.println("Error handling request: " + e.getMessage()); + e.printStackTrace(); + } finally { + try { + clientSocket.close(); + } catch (IOException e) { + System.err.println("Error closing client socket: " + e.getMessage()); + e.printStackTrace(); + } + } + } + + private void pageLive(Request request, Response response) { + if (!request.getPath().endsWith("html")) { + return; + } + byte[] originalBytes = response.getContent(); + // 模拟模板引擎 + // 1. 获取原始的字符串 + String originalString = new String(originalBytes, StandardCharsets.UTF_8); + + // 2. 替换字符串 + String replacedString = originalString.replace("${body}", request.getParams()); + + // 3. 将替换后的 String 转回 byte[] + response.setContent(replacedString.getBytes(StandardCharsets.UTF_8)); + } + + private Request parseRequest(InputStream input) throws IOException, URISyntaxException { + BufferedReader reader = new BufferedReader(new InputStreamReader(input)); + String requestLine = reader.readLine(); + if (requestLine == null) { + throw new IOException("Empty request"); + } + // GET /index.html?a=1 HTTP/1.1 + // Host: 127.0.0.1:8080 + // User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 + // Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange; + // v=b3;q=0.7 + + // POST /getUser HTTP/1.1 + // Content-Type: application/json + // User-Agent: PostmanRuntime/7.44.0 + // Accept: */* + // Host: 127.0.0.1:8080 + // Accept-Encoding: gzip, deflate, br + // Connection: keep-alive + // Content-Length: 10 + + logger.info("====================================================="); + logger.info(requestLine); + String[] parts = requestLine.split(" "); + if (parts.length != 3) { + throw new IOException("Invalid request line: " + requestLine); + } + + String method = parts[0]; // GET + String path = parts[1]; // /index.html + + URI url = new URI(path); + path = url.getPath(); + String params = url.getQuery(); + + + // 读取请求头 + String headerLine; + int contentLength = 0; // 输入流中字节长度 + while ((headerLine = reader.readLine()) != null && !headerLine.isEmpty()) { + logger.info(headerLine); + if (method.equals("POST") && headerLine.startsWith("Content-Length:")) { + contentLength = Integer.parseInt(headerLine.split(":")[1].trim()); + } + } + + // 读取POST请求体 + StringBuilder requestBody = new StringBuilder(); + if (method.equals("POST") && contentLength > 0) { + char[] buffer = new char[contentLength]; + reader.read(buffer, 0, contentLength); + requestBody.append(buffer); + logger.info("Body: " + requestBody); + } + logger.info("Params: " + params); + + logger.info("====================================================="); + return new Request(method, path, requestBody.toString(), params); + } + + private Response handleRequest(Request request) { + String path = request.getPath(); + + // 默认主页 + if (path.equals("/")) { + path = "/index.html"; + } + path = getAbsolutePath(path); + + // 获取classPath中的文件路径 + String classPath = Objects.requireNonNull(getClass().getClassLoader().getResource("")).getPath(); + if (OS_NAME.startsWith("Windows")) { + classPath = classPath.substring(1); + } + Path filePath = Paths.get(classPath, path); + + // 安全检查,防止目录遍历攻击 + if (!filePath.startsWith(Paths.get(classPath, webRoot)) && !filePath.startsWith(Paths.get(classPath, staticRoot))) { + return new Response(403, "Forbidden", "text/plain", "Access denied".getBytes()); + } + + // 检查文件是否存在且是普通文件 + if (Files.exists(filePath) && Files.isRegularFile(filePath)) { + try { + byte[] content = Files.readString(filePath, StandardCharsets.UTF_8).getBytes(StandardCharsets.UTF_8); + String contentType = determineContentType(filePath); + return new Response(200, "OK", contentType, content); + } catch (IOException e) { + return new Response(500, "Internal Server Error", "text/plain", "Error reading file".getBytes()); + } + } else { + return new Response(404, "Not Found", "text/plain", "Page not found".getBytes()); + } + } + + private String getAbsolutePath(String path) { + if (path.startsWith("/") && STATIC_RESOURCES.stream().anyMatch(path::endsWith)) { + return staticRoot + path; + } + return webRoot + path; + } + + private String determineContentType(Path filePath) { + String fileName = filePath.getFileName().toString(); + if (fileName.endsWith(".html")) { + return "text/html"; + } else if (fileName.endsWith(".css")) { + return "text/css"; + } else if (fileName.endsWith(".js")) { + return "application/javascript"; + } else if (fileName.endsWith(".png")) { + return "image/png"; + } else if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) { + return "image/jpeg"; + } else { + return "application/octet-stream"; + } + } + + private void sendResponse(OutputStream output, Response response) throws IOException { + String statusLine = STR.""" +HTTP/1.1 \{response.getStatusCode()} \{response.getStatusMessage()}\r +"""; + String headers = STR.""" +Content-Type: \{response.getContentType()}\r +Content-Length: \{response.getContentLength()}\r +\r +"""; + + output.write(statusLine.getBytes()); + output.write(headers.getBytes()); + output.write(response.getContent()); + output.flush(); + } +} diff --git a/demo1/src/main/java/top/yexuejc/demo/Response.java b/demo1/src/main/java/top/yexuejc/demo/Response.java new file mode 100644 index 0000000..642c872 --- /dev/null +++ b/demo1/src/main/java/top/yexuejc/demo/Response.java @@ -0,0 +1,45 @@ +package top.yexuejc.demo; + +/** + * 响应对象类 + * @author maxiaofeng + * @date 2025/6/19 11:37 + */ +public class Response { + private final int statusCode; + private final String statusMessage; + private final String contentType; + private byte[] content; + + public Response(int statusCode, String statusMessage, String contentType, byte[] content) { + this.statusCode = statusCode; + this.statusMessage = statusMessage; + this.contentType = contentType; + this.content = content; + } + + public int getStatusCode() { + return statusCode; + } + + public String getStatusMessage() { + return statusMessage; + } + + public String getContentType() { + return contentType; + } + + public byte[] getContent() { + return content; + } + + public int getContentLength() { + return content.length; + } + + public Response setContent(byte[] content) { + this.content = content; + return this; + } +} diff --git a/demo1/src/main/java/top/yexuejc/demo/WebServer.java b/demo1/src/main/java/top/yexuejc/demo/WebServer.java new file mode 100644 index 0000000..f4ebff4 --- /dev/null +++ b/demo1/src/main/java/top/yexuejc/demo/WebServer.java @@ -0,0 +1,80 @@ +package top.yexuejc.demo; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.logging.Logger; + +/** + * web服务核心 + * + * @author maxiaofeng + * @date 2025/6/19 11:13 + */ +public class WebServer { + Logger logger = Logger.getLogger(WebServer.class.getName()); + /** 端口 */ + private final int port; + /** 页面资源文件 */ + private final String webRoot; + /** 静态资源文件 */ + private final String staticRoot; + + private ServerSocket serverSocket; + private boolean isRunning; + + public WebServer() { + this(8080, "static", "web"); + } + + public WebServer(int port, String staticRoot, String webRoot) { + this.port = port; + this.staticRoot = staticRoot; + this.webRoot = webRoot; + } + + /** + * 启动服务 + */ + public void start() { + try { + serverSocket = new ServerSocket(port); + isRunning = true; + logger.info("启动Web服务 : http://127.0.0.1:" + port); + + // 阻塞式监听服务是否正常 + while (isRunning) { + try { + Socket clientSocket = serverSocket.accept(); + // 新建一个线程处理请求 + new RequestHandler(clientSocket, staticRoot, webRoot).run(); + } catch (IOException e) { + if (isRunning) { + logger.severe("创建连接异常。"); + e.printStackTrace(); + } + } + } + } catch (Throwable r) { + logger.severe("启动Web服务异常。"); + r.printStackTrace(); + } finally { + stop(); + } + } + + /** + * 停止服务 + */ + public void stop() { + isRunning = false; + try { + if (serverSocket != null) { + serverSocket.close(); + } + } catch (IOException e) { + logger.info("停止Web服务异常: " + e.getMessage()); + } + System.out.println("Web服务停止."); + } +} diff --git a/demo1/src/main/java/top/yexuejc/demo/WebServerApplication.java b/demo1/src/main/java/top/yexuejc/demo/WebServerApplication.java new file mode 100644 index 0000000..f7b53d1 --- /dev/null +++ b/demo1/src/main/java/top/yexuejc/demo/WebServerApplication.java @@ -0,0 +1,14 @@ +package top.yexuejc.demo; + +/** + * web服务器入口 + * + * @author maxiaofeng + * @date 2025/6/19 11:11 + */ +public class WebServerApplication { + public static void main(String[] args) { + System.setProperty("java.util.logging.SimpleFormatter.format", "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS [%4$s] %2$s - %5$s%6$s%n"); + new WebServer().start(); + } +} diff --git a/demo1/src/main/resources/static/css/index.css b/demo1/src/main/resources/static/css/index.css new file mode 100644 index 0000000..97f73ed --- /dev/null +++ b/demo1/src/main/resources/static/css/index.css @@ -0,0 +1,22 @@ +body { + font-family: Arial, sans-serif; + max-width: 800px; + margin: 0 auto; + padding: 20px; + color: #333; +} + +h1 { + color: #0671dc; + /* 让文字左右滚动,并且改变颜色*/ + text-shadow: 0 0 5px #333; + animation: scroll-text 5s linear infinite; + animation-fill-mode: forwards; + animation-delay: 2s; + animation-direction: alternate; + animation-iteration-count: infinite; + animation-timing-function: ease-in-out; + animation-play-state: running; + animation-name: scroll-text; + animation-duration: 5s; +} diff --git a/demo1/src/main/resources/web/index.html b/demo1/src/main/resources/web/index.html new file mode 100644 index 0000000..9d89ff0 --- /dev/null +++ b/demo1/src/main/resources/web/index.html @@ -0,0 +1,13 @@ + + + + + Simple Web Server + + + +

Welcome to Simple Web Server

+

This page is served by our custom Java web server!

+

你的请求参数是:${body}

+ + diff --git a/demo1/web_server.md b/demo1/web_server.md new file mode 100644 index 0000000..2cfb69f --- /dev/null +++ b/demo1/web_server.md @@ -0,0 +1,109 @@ +从0到1实现一个web服务器 +--- + +### 一、网络请求 + +1. 什么是网络请求? +
+ 点击展开 + 网络请求(Network Request)是指计算机(客户端)向服务器发送数据或从服务器获取数据的过程。它是互联网通信的基础,几乎所有网络应用(如网页、APP、API 调用)都依赖网络请求来交换信息。 +
+2. 网络请求的基本概念 + * 组成:客户端(Client)和服务器(Server) + * 常见的网络请求类型 + + | 请求方法 | 用途 | + |----------|------| + | **GET** | 获取数据(如加载网页、查询数据) | + | **POST** | 提交数据(如登录、上传文件) | + | **PUT** | 更新数据(如修改用户信息) | + | **DELETE** | 删除数据(如删除文章) | + | **PATCH** | 部分更新数据 | + + * 请求和响应的组成 + + | **请求(Request)** | **响应(Response)** | + |---------------------|----------------------| + | - 请求方法(GET/POST)
- URL(目标地址)
- 请求头(Headers)
- 请求体(Body,可选) | - 状态码(200/404/500)
- 响应头(Headers)
- 响应体(Body,如 JSON/HTML) | + +3. 网络请求的流程 +
+ 点击展开 + + 1. **客户端发送请求**(如浏览器访问 `https://example.com`)。 + 2. **DNS 解析**(将域名转换为 IP 地址)。 + 3. **建立 TCP 连接**(3 次握手)。 + 4. **发送 HTTP 请求**(如 `GET /index.html`)。 + 5. **服务器处理请求**(读取数据库、计算数据)。 + 6. **服务器返回响应**(如返回 HTML 或 JSON)。 + 7. **客户端解析响应**(如浏览器渲染网页)。 + 8. **关闭连接**(或保持长连接)。 +
+ +4. 网络请求的状态码 + +| 状态码 | 含义 | +|-------------------------------|-------| +| **200 OK** | 请求成功 | +| **301 Moved Permanently** | 永久重定向 | +| **404 Not Found** | 资源不存在 | +| **500 Internal Server Error** | 服务器错误 | +| **403 Forbidden** | 无权限访问 | + +### 二、WEB服务器 + +1. Web服务器的实质是什么? + * Web服务器(Web Server)的实质是一个专门处理HTTP/HTTPS请求的软件或计算机系统,它的核心职责是接收客户端(如浏览器、APP)的请求,并返回静态或动态内容(如HTML、JSON、图片等)。 +2. HTTP请求协议 + ```text + GET /index.html HTTP/1.1 + Host: www.example.com + User-Agent: Mozilla/5.0 + Accept: text/html + Connection: keep-alive + + ``` + ```text + POST /api/users HTTP/1.1 + Host: api.example.com + Content-Type: application/json + Content-Length: 45 + Authorization: Bearer abc123 + Content-Length: 10 + + {"id":"1"} + ``` + * 请求行(Request Line) + * 请求方法(GET、POST等) + * 请求URI(资源路径) + * HTTP协议版本 + * 请求头(Headers) + + |头部字段|描述| + |--------|------| + |Host|指定服务器域名| + |User-Agent|客户端信息| + |Accept|可接受的响应内容类型| + |Accept-Language|可接受的语言| + |Accept-Encoding|可接受的编码方式| + |Connection|控制连接(keep-alive/close)| + |Content-Type|请求体的MIME类型| + |Content-Length|请求体的长度| + |Authorization|认证信息| + |Cookie|客户端Cookie| + * 请求体(Body) + * `application/x-www-form-urlencoded`:表单数据 + * `multipart/form-data`:文件上传 + * `application/json`:JSON数据 + * `text/xml`:XML数据 + * HTTP版本差异 + * HTTP/1.0 每次请求建立新连接 + * HTTP/1.1 默认持久连接,支持管道化 + * HTTP/2 二进制协议,多路复用,头部压缩 + * HTTP/3 基于QUIC协议,改进传输效率 + +### 三、使用JAVA创建一个WEB服务器 +1. ServerSocket创建服务器(指定端口) +2. 等待客户端连接 +3. 处理请求(解析HTTP协议) +4. 响应请求(构建返回内容) \ No newline at end of file