ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JDK内置HttpServer实战:一个Java文件实现轻量HTML静态服务器

JDK内置HttpServer实战:一个Java文件实现轻量HTML静态服务器 简介这份资源是一个用Java实现的轻量级HTML服务器面向希望理解HTTP协议与Socket编程的Java初学者及进阶开发者。它基于Socket原理、线程池、输入输出流和简易HTTP协议构建仅由两个核心类文件组成麻雀虽小五脏俱全适合用来学习服务器底层通信机制或作为小型静态页面服务工具。压缩包共7个文件包含2个html页面、2个java源码、2个jar包和1个说明文档整体仅26KB体积小巧便于携带与阅读。其中java源码可供自行修改并重新打包jar包可直接运行html文件用于演示首页效果说明文档则交代了启动命令与参数含义。目前已有298人学习下载。读者可通过源码掌握线程池调度与HTTP请求解析思路并借助默认端口与首页文件快速验证服务效果是理解Java网络编程的实用入门素材。1. 一个 Java 文件跑起 HTML 服务器为什么值得亲手写一遍很多人第一次听到「简单的 JAVA HTML 服务器」脑子里浮现的是 Tomcat、Spring Boot 那一整套重家伙。但真实场景里你经常只需要一个能返回 HTML 页面的进程给前端同学做本地联调、给内网工具挂个静态页面、临时验证一段!doctype html html langzh-cn head meta charsetutf-8的渲染效果。这时候拉一个完整框架配置比代码还长纯属杀鸡用牛刀。标题里的「简单」不是偷懒而是刻意收窄边界只用 JDK 自带的com.sun.net.httpserver.HttpServer不引第三方依赖一个.java文件编译即用。它解决的是「最小可用的 HTTP 静态资源服务」——能解析请求行、能按路径找文件、能回正确的 Content-Type、能处理 404。适合刚学完 java 基础想搞懂 HTTP 到底怎么跑的人也适合手边缺个轻量静态服务器的 java 工程师。下面把我实际写过的版本拆开讲包括参数怎么设、坑在哪。2. 从零写一个 HttpServer路由、静态文件与 MIME 处理2.1 为什么选 JDK 内置 HttpServer 而不是引框架选型理由很直接com.sun.net.httpserver.HttpServer从 JDK 6 起就在rt.jar里属于 JDK 自带但不在java.*命名空间下的 API。它不需要 Maven 拉包不需要 web.xml不需要容器启动流程。代价是它很原始——没有注解路由、没有模板引擎、没有 session 管理。但对于「返回 HTML」这个目标这些恰好都不需要。对比几个常见替代方案Python 的http.server一行命令就能起但你要的是 Java 生态里的东西Nginx 性能强但改一行响应逻辑就得重载配置Spring Boot 功能全但一个 hello world 的 jar 就几十 MB。内置 HttpServer 的定位在中间进程内可编程零依赖启动毫秒级。常见做法是把它当作工具类嵌入到已有 Java 程序里比如给一个桌面应用加个本地预览端口。需要提前说清的边界这个 API 在部分 JDK 发行版里被标记为内部 API模块化之后JDK 9如果开了强封装可能报警告。实测在 JDK 8 到 JDK 17 的常规发行版上直接javac编译运行没问题JDK 21 上也能跑只是别指望它进jlink的精简镜像。生产环境高并发场景不要用它它的线程模型是每请求一线程可换线程池扛不住压测。2.2 最小可运行版本30 行代码跑通第一个 HTML 响应先上能跑的最小骨架。新建SimpleHtmlServer.java内容如下import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpExchange; import java.io.OutputStream; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; public class SimpleHtmlServer { public static void main(String[] args) throws IOException { // 绑定端口 8000backlog 设为 0 表示用系统默认 HttpServer server HttpServer.create(new InetSocketAddress(8000), 0); // 注册根路径处理器 server.createContext(/, new RootHandler()); // 不设线程池则使用默认实现串行处理 server.start(); System.out.println(Server started at http://localhost:8000); } static class RootHandler implements HttpHandler { Override public void handle(HttpExchange exchange) throws IOException { String html !doctype htmlhtml lang\zh-cn\head meta charset\utf-8\titleHello/title/head bodyh1Java HTML Server/h1/body/html; byte[] bytes html.getBytes(StandardCharsets.UTF_8); // 必须先设置响应头再调用 sendResponseHeaders exchange.getResponseHeaders().set(Content-Type, text/html; charsetutf-8); exchange.sendResponseHeaders(200, bytes.length); try (OutputStream os exchange.getResponseBody()) { os.write(bytes); } } } }编译运行两条命令javac SimpleHtmlServer.java java SimpleHtmlServer浏览器打开http://localhost:8000就能看到页面。逻辑说明createContext(/)把根路径及所有子路径都交给同一个 handler这是后面做静态文件路由的基础。sendResponseHeaders(200, bytes.length)的第二个参数是响应体长度必须和实际写入字节数一致否则浏览器会一直等或提前截断。参数说明InetSocketAddress(8000)里的端口可换成任意空闲端口create的第二个参数是 TCP backlog设 0 走默认内网小工具够用。提示exchange.getResponseBody()拿到的流必须关闭用 try-with-resources 最稳。忘了关会导致连接不释放几次刷新后浏览器就卡住。2.3 静态文件路由把 URL 路径映射到磁盘文件真正要当 HTML 服务器用得能返回磁盘上的.html、.css、.js。核心是把请求路径转成文件路径并防目录穿越。下面这段替换掉上面的RootHandlerimport java.nio.file.*; static class StaticHandler implements HttpHandler { private final Path root; StaticHandler(Path root) { this.root root.toAbsolutePath().normalize(); } Override public void handle(HttpExchange exchange) throws IOException { String uriPath exchange.getRequestURI().getPath(); if (uriPath.equals(/)) uriPath /index.html; // 拼接并规范化防止 ../ 穿越 Path target root.resolve(uriPath.substring(1)).normalize(); if (!target.startsWith(root) || !Files.isRegularFile(target)) { sendText(exchange, 404, 404 Not Found); return; } byte[] bytes Files.readAllBytes(target); String mime guessMime(target.toString()); exchange.getResponseHeaders().set(Content-Type, mime); exchange.sendResponseHeaders(200, bytes.length); try (OutputStream os exchange.getResponseBody()) { os.write(bytes); } } private static String guessMime(String name) { if (name.endsWith(.html)) return text/html; charsetutf-8; if (name.endsWith(.css)) return text/css; charsetutf-8; if (name.endsWith(.js)) return application/javascript; charsetutf-8; if (name.endsWith(.png)) return image/png; if (name.endsWith(.json)) return application/json; charsetutf-8; return application/octet-stream; } private static void sendText(HttpExchange ex, int code, String body) throws IOException { byte[] b body.getBytes(StandardCharsets.UTF_8); ex.getResponseHeaders().set(Content-Type, text/plain; charsetutf-8); ex.sendResponseHeaders(code, b.length); try (OutputStream os ex.getResponseBody()) { os.write(b); } } }注册时改成server.createContext(/, new StaticHandler(Paths.get(./www)))然后在同级目录建www/index.html。逻辑说明normalize()会把..折叠掉再用startsWith(root)二次校验这是防目录穿越的标准双保险。参数说明root建议用绝对路径相对路径在不同工作目录下启动会指向不同位置这是新手最常翻车的地方。MIME 判断用后缀名硬匹配够用但不完整.svg、.woff2这类按需补。2.4 用线程池扛住并发Executor 参数怎么调默认实现是串行处理请求一个慢请求会堵住后面所有请求。加线程池只要一行import java.util.concurrent.Executors; // 固定 8 个线程适合内网小工具 server.setExecutor(Executors.newFixedThreadPool(8));参数说明线程数不是越大越好。这个服务器多用于内网、开发机8 到 16 足够。设成Executors.newCachedThreadPool()在突发流量下会创建大量线程反而拖慢。如果只是自己用不设 executor 也行但要知道它是串行的。判断依据打开浏览器开发者工具同时请求多个资源如果响应时间线性叠加说明还在串行。3. 避坑与排查那些让服务器「看起来没反应」的细节3.1 端口被占用却报了个看不懂的异常现象启动时抛BindException: Address already in use但你不确定谁占了。原因8000 是常见调试端口可能被别的进程或上一次没退干净的实例占着。解决Linux/macOS 用lsof -i :8000Windows 用netstat -ano | findstr :8000找到 PID 再处理或者干脆换端口。我一般写个循环从 8000 往上试避免手动改。3.2 中文页面乱码浏览器显示问号现象HTML 里有中文页面显示成???或方块。原因两个地方都可能出问题——getBytes()没指定字符集默认走平台编码或者响应头Content-Type没带charsetutf-8。解决getBytes(StandardCharsets.UTF_8)和text/html; charsetutf-8两处都要写全。只改一处仍会乱码这是血泪经验。3.3 改了 HTML 刷新没变化现象编辑了www/index.html浏览器刷新还是旧内容。原因浏览器缓存或者你读的是另一个目录的文件。解决先开无痕窗口或强制刷新CtrlShiftR再确认root路径。Files.readAllBytes每次都读磁盘不存在服务端缓存问题所以八成是路径或浏览器缓存。3.4 请求路径带查询参数时文件找不到现象访问/page.html?name1返回 404。原因getRequestURI().getPath()不含查询串但如果用了toString()就会带上?name1拼出来的文件名自然不存在。解决坚持用getPath()需要解析参数再单独处理getRawQuery()。这个坑在写路由时特别隐蔽。3.5 大文件响应内存暴涨现象返回一个几十 MB 的视频或图片JVM 内存飙升。原因Files.readAllBytes一次性把整个文件读进内存。解决小文件无所谓大文件改用Files.copy(target, exchange.getResponseBody())流式传输响应头长度用Files.size(target)。参数上注意sendResponseHeaders的长度必须准确流式传输时先取文件大小。4. 进阶技巧把静态服务器变成可复用的调试工具写到能跑只是及格真正让它好用要再往前一步。我习惯把它做成一个带命令行参数的通用工具java SimpleHtmlServer --port 8080 --root ./www这样换项目不用改代码。解析参数用最朴素的for循环遍历args即可别为这点需求引 Apache Commons CLI。一个具体技巧是加「目录列表」当请求路径是目录且没有index.html时用Files.list生成一个简单的链接列表页。这在浏览一堆生成的 HTML 报告时特别方便。注意对文件名做 HTML 转义否则文件名里的会破坏页面结构。验证方法上我一般用curl -I http://localhost:8000/index.html看响应头是否正确用curl -o /dev/null -s -w %{http_code}\n http://localhost:8000/nope验证 404 分支。比开浏览器点来点去快得多。压测就用ab -n 1000 -c 20 http://localhost:8000/看有没有连接泄漏——如果ab报Connection reset多半是响应流没关或长度不对。参数建议值说明port8000 起避开 80/8080 等常被占端口backlog0走系统默认内网够用线程池大小8~16超过 32 收益递减root绝对路径相对路径易踩工作目录坑最后说个习惯我每次写完这类小服务器都会在main里加一行打印实际绑定的端口和根目录因为「我以为它监听的是这个端口」是排查时最大的黑匣子。这个方案值不值得做如果你一年里超过三次需要临时起个静态服务就值得把它存成一个文件下次直接java跑起来。希望帮到你。本文还有配套的精品资源点击获取
返回列表