
简介这份资源是一个用Java实现的轻量级HTML服务器面向希望理解HTTP协议底层原理、Socket编程与线程池应用的Java学习者。它基于Socket通信、线程池、输入输出流及简易HTTP协议构建仅由两个核心类文件组成麻雀虽小五脏俱全适合作为网络编程入门练手或课程设计参考。压缩包共7个文件包含2个java源码、2个jar包、2个html页面和1个使用说明txt整体仅26KB源码与可执行包齐备便于直接运行或二次打包。启动时在DOS窗口进入jar所在目录执行java -Djava.ext.dirs. httpserver.HttpServer [路径] [端口]即可路径与端口可选默认端口1234首页支持index.html。将html文件放入目录并重命名为index.html后浏览器访问http://localhost:1234即可查看效果。编译环境为JDK1.6最低需JDK1.5以支持内置线程池。目前已有298人学习适合想深入理解HTTP服务实现细节的开发者研读源码并动手改造。1. 一个 Java 文件跑起静态站点这套 HTML 服务器到底能顶什么用很多人第一次听到「Java HTML 服务器」脑子里浮现的是 Tomcat、Jetty 那一整套容器配置 web.xml、打 war 包、丢进 webapps 目录折腾半天才看到一个 Hello World。但真实场景里我们经常遇到的诉求其实特别朴素手上有一堆写好的 HTML、CSS、JS 静态文件想用 Java 快速起一个能对外提供 HTTP 服务的进程不引入任何第三方依赖一个类、几十行代码就能跑起来。这份「简单的 JAVA HTML 服务器」资源解决的正是这个场景——它用 JDK 自带的com.sun.net.httpserver.HttpServer或者原生ServerSocket手写 HTTP 协议解析把静态目录映射成可访问的 URL适合做本地预览、内网工具页、教学演示、接口联调时的临时静态托管。它不适合谁不适合要上生产、要扛并发、要玩 HTTPS 双向认证、要接 Spring 生态的人那些场景老老实实上成熟容器。但如果你只是想验证一段前端代码、给同事临时分享一个报表页面、或者在离线环境里跑一个不依赖任何 jar 包的迷你服务这套东西的价值就出来了零依赖、可读、可改、可嵌入。下面我按「它怎么工作 → 怎么落地 → 坑在哪 → 怎么进阶」的顺序把这份资源拆开讲透。2. 从 ServerSocket 到 HttpServer两条实现路线怎么选2.1 原生 ServerSocket 手写 HTTP 解析最原始的做法是自己监听端口、接收连接、读字节流、按 HTTP 协议格式解析请求行和请求头然后拼响应。这条路线的价值不在于性能而在于你能彻底看清 HTTP 报文长什么样。核心逻辑是ServerSocket绑定端口后进入accept()循环每个连接交给一个线程处理从InputStream里按行读取第一行就是GET /index.html HTTP/1.1这样的请求行。// 原生 ServerSocket 版最小可运行的静态文件服务器核心 ServerSocket server new ServerSocket(8080); System.out.println(listening on 8080); while (true) { Socket socket server.accept(); // 阻塞等待连接 new Thread(() - { try (BufferedReader in new BufferedReader( new InputStreamReader(socket.getInputStream())); OutputStream out socket.getOutputStream()) { String requestLine in.readLine(); // 只取请求行够用 if (requestLine null) return; String path requestLine.split( )[1]; // 拿到 /index.html if (/.equals(path)) path /index.html; File file new File(webroot path); // 映射到本地目录 if (!file.exists()) { out.write(HTTP/1.1 404 Not Found\r\n\r\n.getBytes()); return; } byte[] body Files.readAllBytes(file.toPath()); String header HTTP/1.1 200 OK\r\n Content-Length: body.length \r\n Content-Type: guessType(path) \r\n\r\n; out.write(header.getBytes()); out.write(body); } catch (IOException e) { e.printStackTrace(); } finally { try { socket.close(); } catch (IOException ignored) {} } }).start(); }这段代码里几个参数值得说清楚。端口8080是约定俗成的开发端口改成 80 需要管理员权限。webroot是静态资源根目录所有请求路径都拼在它后面这里必须做路径穿越防护否则../../etc/passwd就能读到系统文件。Content-Type靠guessType根据扩展名推断.html给text/html.css给text/css.js给application/javascript给错了浏览器会拒绝执行。Content-Length必须和实际字节数一致否则浏览器会一直等或者截断。这条路线的好处是零依赖、逻辑透明JDK 装好就能编译运行。坏处也很明显HTTP/1.1 的 keep-alive、分块传输、Range 请求、并发连接管理全得自己处理稍微复杂一点就翻车。我一般只在教学或者需要极致可控的嵌入式场景用它。2.2 用 JDK 自带 HttpServer 省掉协议解析JDK 6 之后自带了com.sun.net.httpserver.HttpServer虽然藏在jdk.httpserver模块里、文档不多但它把 HTTP 协议解析、连接管理、线程池都封装好了你只需要注册一个HttpHandler处理路径映射。这是这份资源里更推荐的落地方式代码量能砍掉一半以上。// HttpServer 版注册 handler静态目录映射 HttpServer server HttpServer.create(new InetSocketAddress(8080), 0); server.createContext(/, exchange - { String path exchange.getRequestURI().getPath(); if (/.equals(path)) path /index.html; File file new File(webroot, path).getCanonicalFile(); // 关键校验规范化后的路径仍在 webroot 内防穿越 if (!file.getPath().startsWith(new File(webroot).getCanonicalPath())) { send(exchange, 403, Forbidden.getBytes()); return; } if (!file.exists() || file.isDirectory()) { send(exchange, 404, Not Found.getBytes()); return; } byte[] body Files.readAllBytes(file.toPath()); exchange.getResponseHeaders().set(Content-Type, guessType(path)); exchange.sendResponseHeaders(200, body.length); exchange.getResponseBody().write(body); exchange.close(); }); server.setExecutor(Executors.newFixedThreadPool(8)); // 线程池别用默认 server.start();createContext(/)表示拦截所有路径exchange对象封装了请求和响应。sendResponseHeaders(200, body.length)里的第二个参数是响应体长度传 0 表示未知长度、走分块传输传 -1 表示无响应体。setExecutor这一步很多人漏掉默认执行器是单线程的一个慢请求就能把整个服务卡死换成固定线程池是最低成本的修复。路径穿越防护用getCanonicalFile()规范化后再比对前缀比字符串contains(..)靠谱得多因为 URL 编码、符号链接都能绕过简单检查。两条路线怎么选要教学、要完全掌控协议细节用 ServerSocket要快速落地、代码可维护用 HttpServer。这份资源两种都有参考实现实际项目里我九成情况选后者。3. 静态资源映射与 MIME 类型让浏览器正确渲染3.1 目录映射与默认首页规则静态服务器的核心是「URL 路径 → 本地文件路径」的映射。规则看着简单细节全是坑。根路径/要映射到index.html这是 Web 服务器的通用约定但如果你目录里放的是index.htm或者default.html就得按优先级依次尝试。目录请求比如/docs/要么返回该目录下的默认首页要么生成文件列表页要么直接 403三种策略各有适用场景。// 默认首页按优先级探测 String[] defaults {index.html, index.htm, default.html}; File target new File(webroot, path); if (target.isDirectory()) { for (String name : defaults) { File candidate new File(target, name); if (candidate.exists()) { target candidate; break; } } }这段逻辑要放在路径穿越校验之后、文件读取之前。webroot建议用绝对路径初始化相对路径在不同启动目录下行为不一致是排查问题时最常见的玄学来源。我习惯在启动时打印一行webroot resolved to: xxx出问题时一眼就能看出根目录对不对。3.2 MIME 类型表与字符编码浏览器靠Content-Type决定怎么处理响应体。给错了CSS 不生效、JS 报语法错误、图片显示成乱码。JDK 自带的Files.probeContentType()在部分系统上返回 null 或者不准生产可用的做法是维护一张扩展名到 MIME 的映射表。扩展名MIME 类型常见翻车点.htmltext/html; charsetUTF-8漏 charset 导致中文乱码.csstext/css给成 text/plain 样式不生效.jsapplication/javascript给成 text/javascript 部分浏览器警告.jsonapplication/json给成 text/plain 前端解析失败.pngimage/png二进制文件被当文本处理损坏.svgimage/svgxml漏 xml 后缀不渲染.woff2font/woff2字体加载失败控制台报 CORS字符编码这块HTML 文件建议在响应头里显式带charsetUTF-8同时 HTML 内部meta charsetUTF-8也要写双保险。二进制文件图片、字体、压缩包必须用字节流读写一旦用Reader/Writer按字符处理文件就废了。这是新手最容易踩的坑之一现象是图片能下载但打不开、字体加载报错。提示MIME 表不用追求大而全覆盖你项目里实际用到的十几种就够剩下的统一给application/octet-stream让浏览器走下载。4. 并发、路径安全与常见故障排查4.1 线程模型与连接数控制原生 ServerSocket 版如果每个连接new Thread()几百个并发就能把线程数打爆内存飙升、上下文切换开销巨大。正确做法是用线程池核心线程数按CPU 核数 × 2起步队列有界拒绝策略明确。HttpServer 版通过setExecutor传入线程池效果一样。// 有界队列 明确拒绝策略避免请求堆积拖垮进程 ThreadPoolExecutor pool new ThreadPoolExecutor( 4, // 核心线程 16, // 最大线程 60L, TimeUnit.SECONDS, // 空闲回收时间 new ArrayBlockingQueue(100), // 有界队列防止无限堆积 new ThreadPoolExecutor.AbortPolicy() // 满了直接拒绝快速失败 );参数怎么定核心线程数看你的请求里有没有阻塞 IO纯静态文件读取是 IO 密集型可以适当放大。队列必须有界无界队列在流量突增时会吃光内存这是血泪经验。拒绝策略选AbortPolicy让客户端拿到明确失败比默默排队到超时体验好。4.2 路径穿越与目录列表泄露路径穿越是静态服务器最经典的安全问题。攻击者构造/../../../etc/passwd或者 URL 编码变体/..%2f..%2f如果你的拼接逻辑没做规范化校验就能读到 webroot 之外的文件。防护要点先getCanonicalFile()规范化再判断结果路径是否以 webroot 的规范化路径为前缀两个条件缺一不可。目录列表泄露是另一个常被忽视的点。当请求的路径是个目录且没有默认首页时如果你的实现返回了目录下所有文件名等于把内部结构暴露给所有人。除非你就是想做文件分享站否则目录请求一律返回 403 或 404。4.3 常见故障排查清单现象浏览器一直转圈请求不返回。原因通常是响应头里Content-Length和实际写入字节数不一致或者忘了调用exchange.close()。解决核对长度、确保每个分支都关闭 exchange。现象中文文件名或中文内容乱码。原因是响应头没带 charset或者读取文件时用了平台默认编码。解决统一 UTF-8响应头显式声明读文件用Files.readAllBytes走字节。现象端口被占用启动报 BindException。原因是 8080 被其他进程占了。解决netstat -ano | findstr 8080Windows或lsof -i:8080Linux找到进程换端口或杀掉。现象本地能访问局域网其他机器访问不了。原因是绑定地址写成了127.0.0.1而不是0.0.0.0或者防火墙没放行端口。解决new InetSocketAddress(8080)默认绑所有网卡确认防火墙入站规则放行对应端口。现象大文件下载中断。原因是没处理 Range 请求或者一次性把整个文件读进内存导致 OOM。解决流式传输用Files.copy或缓冲区循环读写需要断点续传就实现 Range 头解析。注意排查顺序永远是「先看日志有没有异常 → 再看端口和绑定地址 → 再看路径映射 → 最后看响应头」从外到内别一上来就怀疑代码逻辑。5. 进阶技巧把这份服务器改造成能用的工具5.1 加一层简单的访问日志没有日志的服务器就是黑匣子出了问题只能靠猜。加访问日志成本极低在 handler 入口记录时间、客户端 IP、方法、路径、响应码、耗时输出到控制台或文件。long start System.currentTimeMillis(); // ... 处理请求 ... long cost System.currentTimeMillis() - start; System.out.printf(%s %s %s %d %dms%n, exchange.getRemoteAddress().getAddress().getHostAddress(), exchange.getRequestMethod(), exchange.getRequestURI().getPath(), exchange.getResponseCode(), cost);getRemoteAddress()拿到客户端地址getResponseCode()在sendResponseHeaders之后才有值。耗时统计能帮你快速定位是哪个资源慢。这套日志格式借鉴了 Nginx 的 combined 格式熟手一眼就能读懂。5.2 支持 Range 请求实现断点续传视频、大文件下载场景浏览器会发Range: bytes0-1023这样的请求头服务器要返回 206 Partial Content 和Content-Range头。不实现的话视频没法拖动进度条大文件下载中断后只能重来。String range exchange.getRequestHeaders().getFirst(Range); if (range ! null range.startsWith(bytes)) { String[] parts range.substring(6).split(-); long start Long.parseLong(parts[0]); long end parts.length 1 !parts[1].isEmpty() ? Long.parseLong(parts[1]) : file.length() - 1; long len end - start 1; exchange.getResponseHeaders().set(Content-Range, bytes start - end / file.length()); exchange.getResponseHeaders().set(Accept-Ranges, bytes); exchange.sendResponseHeaders(206, len); try (RandomAccessFile raf new RandomAccessFile(file, r)) { raf.seek(start); byte[] buf new byte[8192]; long remaining len; while (remaining 0) { int read raf.read(buf, 0, (int) Math.min(buf.length, remaining)); if (read -1) break; exchange.getResponseBody().write(buf, 0, read); remaining - read; } } }RandomAccessFile.seek(start)直接跳到起始位置避免读整个文件。Accept-Ranges: bytes告诉浏览器支持范围请求。边界情况要处理start超过文件长度返回 416end超过文件长度要截断到length - 1。这套逻辑写对了视频拖动、下载器多线程下载都能正常工作。5.3 用配置文件替代硬编码端口、webroot、线程数、默认首页这些参数写死在代码里每次改都要重新编译不现实。最简单的做法是读一个server.properties用Properties类加载启动时解析。Properties props new Properties(); try (InputStream is new FileInputStream(server.properties)) { props.load(is); } int port Integer.parseInt(props.getProperty(port, 8080)); String webroot props.getProperty(webroot, ./webroot); int threads Integer.parseInt(props.getProperty(threads, 8));getProperty的第二个参数是默认值配置文件缺失或某项没写时兜底。这样同一份代码换个配置文件就能跑不同环境部署时不用动代码。5.4 验证清单上线前走一遍改完代码别急着交付按这个清单过一遍根路径能返回首页不存在的路径返回 404 而不是异常堆栈../路径穿越被拦截返回 403中文 HTML 不乱码CSS、JS、图片 MIME 正确并发压测下线程数稳定不飙升大文件下载不 OOM局域网其他机器能访问日志有输出且格式可读。这九条走完这套简单的 Java HTML 服务器就真正能用了。从那以后我每次改完这类服务器代码都会强制走一遍这份清单尤其是路径穿越和 MIME 这两项翻车次数最多。希望帮到你。本文还有配套的精品资源点击获取