新闻详情

新闻详情

首页 / 资讯中心 / 详情

不装Tomcat!用JDK自带HttpServer实现Java静态文件服务器

发布时间:2026/9/29 17:42:06来源:尧图网络
不装Tomcat!用JDK自带HttpServer实现Java静态文件服务器
简介基于Socket原理、线程池与输入输出流实现的简易HTML服务器是一份适合Java初学者深入理解网络编程的练手资源。项目仅两个类文件分别负责服务监听与连接处理却覆盖HTTP协议解析、多线程并发及静态页面返回等完整环节能帮助读者掌握Web服务端底层的请求响应模型。压缩包共7个文件包含2个Java源码、2个可直接运行的Jar包、2个HTML测试页面及1个TXT说明文档整体仅26KB结构小巧便于逐个研读。目前已有298人学习下载作者随包附送源码具备JDK1.5以上环境即可运行也可用源码自行重新打包启动后通过命令指定路径与端口浏览器访问http://localhost:1234即可查看默认首页效果。尤其适合学习Socket编程、HTTP协议和线程池应用的开发者作为入门参考是理解小型网络服务器的经典示例。1. 简单的JAVA HTML服务器不装Tomcat怎么把静态站点跑起来绝大多数人一提到“Java服务器”第一反应就是Tomcat、Spring Boot再不济也得是个Jetty。但这次拆的这份资源走的是另一条路它只依赖JDK自带的东西不引入任何第三方框架用com.sun.net.httpserver.HttpServer手写了一个能托管HTML、CSS、JS、图片的静态文件服务器。反直觉的点在于你不需要装Tomcat甚至不需要配web.xml一个main方法跑起来就能把静态页面目录对外开放。对正在做Java课程设计、想搞明白HTTP请求是怎么被处理的从业者来说它是把“HTTP协议 Java基础”落到实处的绝佳样本尤其适合想应付“Java基础面试题里服务器部分”的人作为上手练习。2. 服务器核心选型JDK自带HttpServer与手写Socket的取舍2.1 为什么推荐com.sun.net.httpserver零依赖托底这份资源的实现基础是JDK自带的com.sun.net.httpserver包。它在jdk.httpserver模块里从Java 6开始就存在Java 9模块化之后被收进了标准JDK只是平时用的人少很多人根本不知道有这么个东西。它的价值在于你不用去下载Tomcat、不用配CATALINA_HOME、不用管server.xml写一个类就能启动HTTP服务。它的工作模型很简单HttpServer.create绑定一个IP和端口createContext绑定一个URL路径前缀每次有请求进来就回调你注册的handler。handler里能拿到HttpExchange对象通过它读请求头、请求体写响应头和响应体。整个过程把“Java怎么处理HTTP请求”完整暴露在你面前——这比对着Spring Boot的黑匣子看RestController要直观得多。我之前用这套写过内部工具的本地面板部署时不用动Tomcat丢到服务器上java -jar就完事。对学习场景来说它还有一层好处代码路径短从请求进入到响应返回每一步都能接上System.out.println看日志排查问题的成本极低。2.2 手写ServerSocket服务器的边界什么时候才真的需要有人会问既然要学原理为什么不直接用ServerSocket自己解析HTTP报文我确实见过很多教程手写一个几百行的HTTP服务器用BufferedReader.readLine去解析请求行和Header。那种写法不是不行而是它有一个很现实的问题HTTP协议的边界情况太多了。比如请求行的格式是GET /index.html HTTP/1.1但Header可能有分块传输、Keep-Alive连接复用、Expect: 100-continue还有URL里的百分号编码。你手写解析Content-Length时少处理一个边界——比如请求头跨多行、空行后面直接跟二进制体——整个解析就会错位。com.sun.net.httpserver帮你兜住了协议解析的底你可以把精力放在业务逻辑和资源管理上。那ServerSocket什么时候值得写我一般会在两类场景建议手写一是你想彻底弄懂HTTP报文格式愿意为“学到东西”付出排错时间二是你的环境受限到连JDK自带模块都用不上。否则用HttpServer是“把力气花在正确的地方”的做法。下面的对比表可以帮你判断对比项com.sun.net.httpserver手写ServerSocketHTTP协议解析自带支持请求行/Header/Body自己解析边界情况多静态文件服务需自己写资源映射需自己写资源映射依赖JDK内置模块纯JDK但工作量大适合场景学习、小型工具、快速起服务纯协议学习、极特殊环境2.3 骨架代码先用HttpServer把请求路径和资源目录对上先看一段最核心的启动代码后面所有功能都是在这个骨架上长出来的import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpExchange; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.file.Path; import java.nio.file.Paths; public class SimpleFileServer { public static void main(String[] args) throws IOException { // 1. 监听端口0表示用默认线程池 int port 8080; // 2. webroot目录下的文件就是要对外公开的内容 Path root Paths.get(webroot).toAbsolutePath(); HttpServer server HttpServer.create(new InetSocketAddress(port), 0); // 3. /前缀下的所有请求都交给handler处理 server.createContext(/, exchange - handle(exchange, root)); server.start(); System.out.println(static server started: http://127.0.0.1: port); } private static void handle(HttpExchange exchange, Path root) throws IOException { // 先随便返回一个固定内容验证服务能通 String html h1simple java html server/h1; byte[] bytes html.getBytes(UTF-8); exchange.getResponseHeaders().set(Content-Type, text/html; charsetutf-8); exchange.sendResponseHeaders(200, bytes.length); exchange.getResponseBody().write(bytes); exchange.close(); } }逻辑说明HttpServer.create的第一个参数是绑定的IP和端口我用InetSocketAddress(port)绑定了所有网卡的8080端口第二参数是线程池大小传0表示使用HttpServer的默认实现。createContext(/, handler)表示根路径下的所有请求都交给这个handler——想给/api单独开一个处理逻辑就再createContext(/api, ...)一次不用改其他代码。exchange.sendResponseHeaders(200, bytes.length)这个调用比较关键第二个参数必须是响应体长度如果传0会走chunked编码传-1表示没有响应体。exchange.close()别漏它负责把连接状态收尾不调用的话连接可能挂住客户端很久才收到响应。这段代码跑起来后浏览器访问http://127.0.0.1:8080/就能看到一行h1文本说明这台“JAVA HTML服务器”已经活了。接下来要做的就是把“固定内容”换成“磁盘上的真实HTML文件”。3. 把静态文件服务做扎实MIME映射、中文路径与目录索引三件套3.1 MIME类型映射浏览器认不认你的文件格式全看这张表只返回固定文本显然不够。一个真正的HTML服务器要能把index.html读出来返回给浏览器同时把Content-Type设置正确。这个Content-Type就是MIME类型它决定了浏览器是直接渲染、弹下载框还是卡在屏幕上显示一团乱码。很多初学Java的人在这里踩坑所有文件一律返回application/octet-stream结果CSS不生效、图片不显示、SVG直接变成下载文件。我习惯的做法是维护一个扩展名到MIME的映射表import java.util.HashMap; import java.util.Map; public class MimeTypes { public static final MapString, String MIME_MAP new HashMap(); static { MIME_MAP.put(html, text/html; charsetutf-8); MIME_MAP.put(htm, text/html; charsetutf-8); MIME_MAP.put(css, text/css; charsetutf-8); MIME_MAP.put(js, application/javascript; charsetutf-8); MIME_MAP.put(json, application/json; charsetutf-8); MIME_MAP.put(png, image/png); MIME_MAP.put(jpg, image/jpeg); MIME_MAP.put(jpeg, image/jpeg); MIME_MAP.put(gif, image/gif); MIME_MAP.put(svg, image/svgxml); MIME_MAP.put(ico, image/x-icon); MIME_MAP.put(txt, text/plain; charsetutf-8); MIME_MAP.put(pdf, application/pdf); MIME_MAP.put(woff, font/woff); MIME_MAP.put(woff2, font/woff2); MIME_MAP.put(mp4, video/mp4); MIME_MAP.put(webm, video/webm); } public static String get(String fileName) { int dot fileName.lastIndexOf(.); if (dot 0 || dot fileName.length() - 1) { return application/octet-stream; } String ext fileName.substring(dot 1).toLowerCase(); return MIME_MAP.getOrDefault(ext, application/octet-stream); } }逻辑说明取扩展名时用lastIndexOf(.)而不是indexOf(.)这是为了处理a.b.html这种文件名——点号出现多次时只取最后一个点后面那段。getOrDefault是兜底遇到不认识的文件类型就给标准的二进制流MIME至少保证浏览器能下载而不是直接打开乱码。参数说明HTML和CSS、JS这三个文本类资源我都加上了charsetutf-8因为现代页面基本都是UTF-8编码图片和字体不加charset那是二进制资源加了反而可能让浏览器解析出错。我见过有人把所有MIME都写成text/html页面上引用CSS后整个页面变成纯文本一片乱码——那就是MIME兜底策略不对。3.2 中文路径和URL解码Java基础里的编码问题在服务器端翻车静态服务器最典型的翻车场景就是中文文件名。假设webroot/templates/登录页.html里放了一个页面浏览器访问/templates/登录页.html注意这个地址在网络上传输的时候不是原样传的浏览器会先做百分号编码——汉字变成%E7%99%BB%E5%BD%95%E9%A1%B5之类的串。这里有两个坑。第一如果你直接拿exchange.getRequestURI().getPath()的结果去拼文件路径不同JDK版本行为不一致得到的可能是已经解码的字符串也可能带着%符号。我的做法是统一用getRawPath()拿到原始编码路径再手动解码一次这样行为可预期import java.net.URLDecoder; private static String decodeRequestPath(HttpExchange exchange) throws IOException { String rawPath exchange.getRequestURI().getRawPath(); return URLDecoder.decode(rawPath, UTF-8); }逻辑说明getRawPath()返回的是HTTP请求里原始的样子比如/templates/%E7%99%BB%E5%BD%95%E9%A1%B5.html。URLDecoder.decode把它转换成/templates/登录页.html。这里最容易犯的错误是解码两次——如果某个环节已经把路径解码了你又解一次%后面的字符会对不上直接抛IllegalArgumentException: URLDecoder: Illegal hex characters。第二个坑藏在文件系统层面。如果你在Windows上测试中文文件名本身没问题问题出在Java字符串和文件系统编码的转换上。推荐直接把静态资源根目录用Paths.get(webroot).toAbsolutePath()转成绝对路径后续用Path.resolve拼接全程不经过File字符串拼接能避开大部分编码问题。我在后文避坑章节会再展开一个具体案例。3.3 目录索引和404访问http://127.0.0.1:8080/时该发生什么访问根路径/时后面没有具体文件名服务器应该去找index.html找不到就列目录或者给404。这个逻辑不写的话访问根路径会直接报错或者返回空内容。完整的文件分发逻辑我一般这样写import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.io.IOException; private static void serveFile(HttpExchange exchange, Path root) throws IOException { String decodedPath decodeRequestPath(exchange); // 拼接时要防止路径穿越先去掉开头的/ Path target root.resolve(decodedPath.substring(1)).normalize(); // 校验拼接后的路径必须仍在root目录之下 if (!target.startsWith(root)) { sendResponse(exchange, 403, Forbidden); return; } if (Files.isDirectory(target)) { // 目录优先找index.html没有则生成简易目录列表 Path index target.resolve(index.html); if (Files.isRegularFile(index)) { writeFile(exchange, index); } else { listDirectory(exchange, target, root); } } else if (Files.isRegularFile(target)) { writeFile(exchange, target); } else { sendResponse(exchange, 404, Not Found); } }逻辑说明decodedPath.substring(1)是为了去掉路径开头的/让resolve正确处理相对关系.normalize()会把../这类路径片段折叠成实际路径。这里必须强调normalize()之后一定要用startsWith(root)再校验一次——用户请求/../../etc/passwd这种恶意路径时normalize()会把路径折叠到root之外如果不拦截服务器就把系统文件泄露出去了。这是每个写文件服务器的人都要过的安全关卡。文件读出来之后设置Content-Type、Content-Length然后把字节流写回private static void writeFile(HttpExchange exchange, Path file) throws IOException { String contentType MimeTypes.get(file.getFileName().toString()); exchange.getResponseHeaders().set(Content-Type, contentType); long size Files.size(file); exchange.sendResponseHeaders(200, size); Files.copy(file, exchange.getResponseBody()); exchange.close(); }逻辑说明Files.copy(file, exchange.getResponseBody())是JDK提供的神仙API直接把文件内容拷贝到响应流省去手动开InputStream循环读写的代码。sendResponseHeaders的第二个参数传文件字节数告诉浏览器这是一个固定长度的响应收到这个长度后连接可以复用比chunked编码节省一次额外的包交互。4. 实战避坑端口占用、目录穿越、中文乱码的四个翻车记录4.1 端口被占用8080起不来进程却查不到现象启动时抛java.net.BindException: Address already in use: bind但任务管理器里看不到Java进程。原因8080被其他程序占了或者上一次启动的服务器进程没退出——Java GUI窗口关了不代表JVM停了特别是你从IDE里直接右键Run又关掉控制台的情况子进程会变成孤儿进程继续占端口。Windows下任务管理器默认不显示此类后裔进程所以“查不到”很正常。解决Windows执行netstat -ano | findstr 8080看LISTENING那一行的最后一列PIDLinux执行lsof -i:8080或ss -tulpn | grep 8080。拿到PID后taskkill /F /PID 1234然后再启动。更稳的做法是让端口可配置启动时用Integer.parseInt(System.getProperty(server.port, 8080))读取这样换端口不用改代码重编译。4.2 中文文件名请求404日志里路径已经乱码现象浏览器访问/测试.html服务器log里打出来的路径变成/?/??.html或者一串%混着乱码文件实际存在却请求不到。原因这是两层问题叠在一起。第一层是请求路径没做标准解码直接拿了原始字节串当文件名用第二层是文件系统名和你解码后字符串的编码不一致。最常见的是路径在URLDecoder解码时用了平台默认编码——Windows中文系统默认GBK你指定UTF-8时本来没问题但如果文件名是从别处拷贝过来、本身已经被转成GBK字节再按UTF-8解码就会乱。解决全链路固定UTF-8。请求进来统一走URLDecoder.decode(rawPath, UTF-8)不要相信getPath()的默认行为静态资源根目录用Path对象管理如果想彻底避开系统编码干扰在java启动命令上加-Dfile.encodingUTF-8 -Dsun.jnu.encodingUTF-8。加完这两参数后重启乱码基本消失。4.3 目录穿越访问..能读到webroot之外的系统文件现象请求/../../../../etc/passwd服务端不回405反而把文件内容返回了页面直接展示root:x:0:0:root:/root:/bin/bash之类的内容。原因这是最危险的一类漏洞。处理逻辑里如果直接root.resolve(decodedPath)然后读取..会被resolve解析成上级目录一路往上翻就能越出webroot。很多人觉得这是老掉牙的攻击但我在不少课程设计源码里都见过这种写法——学生的作业里普遍缺normalize校验。解决三个步骤缺一不可。第一步target root.resolve(path).normalize()第二步if (!target.startsWith(root))直接拒绝第三步才是判断文件是否存在。注意startsWith比较的是Path对象不是字符串前缀——Path.startsWith按路径组成部分比较/webroot2不会被误判成/webroot的子路径但字符串startsWith(/webroot)会所以一定要用前者。这个校验同样要放在所有分支的最前面任何文件读取之前先过这一关。4.4 图片能返回但页面样式全丢Content-Type是罪魁祸首现象HTML页面能打开但里面link relstylesheet hrefstyle.css引入的CSS完全不生效F12看Network面板CSS请求状态200但Response Headers里Content-Type是application/octet-stream。原因服务器实现时偷懒把所有文件都统一返回application/octet-stream浏览器拿到这个MIME会认为“这是一个不知道类型的下载流”对link引用的样式表不会按CSS解析。资源不是没返回是浏览器不知道它是什么类型。解决用上面写的MimeTypes.get(fileName)按扩展名分发MIME重点确认css、js、svg、woff2这四类现代页面必备的类型在表里。补充验证方法启动服务后直接curl -I http://127.0.0.1:8080/style.css看返回头里Content-Type是不是text/css。如果是其他东西先查映射表有没有配再查扩展名提取逻辑——lastIndexOf没写对时会把style.css的扩展名取成css之外的值。5. 进阶小技巧给HTML服务器加上If-Modified-Since缓存判断静态服务器只做到“返回文件”级别还差一口气生产环境里浏览器每次刷新都重新下载所有资源页面会明显变慢。加上缓存判断让浏览器在文件没变化时直接用本地缓存可以省掉大部分重复传输。核心思路是HTTP协议里的条件请求浏览器请求时带If-Modified-Since头服务器对比文件最后修改时间如果文件没变就返回304浏览器自动走缓存。import java.time.Instant; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; private static final DateTimeFormatter HTTP_DATE DateTimeFormatter.ofPattern(EEE, dd MMM yyyy HH:mm:ss GMT) .withLocale(Locale.ENGLISH) .withZone(ZoneOffset.UTC); private static boolean notModified(HttpExchange exchange, Path file) throws IOException { String since exchange.getRequestHeaders().getFirst(If-Modified-Since); if (since null) { return false; } long fileMtime Files.getLastModifiedTime(file).toMillis(); try { long sinceMtime Instant.from(HTTP_DATE.parse(since)).toEpochMilli(); // 文件修改时间按秒比较HTTP日期只精确到秒 return fileMtime / 1000 * 1000 sinceMtime; } catch (DateTimeParseException e) { return false; } }逻辑说明Files.getLastModifiedTime(file).toMillis()拿到文件修改时间戳和请求头里的时间比较。比较时我把文件时间戳做了/ 1000 * 1000的截断因为HTTP日期格式只精确到秒如果直接用毫秒比较文件刚好在这秒内被修改时会出现“明明改了却返回304”的误判。HTTP_DATE是HTTP标准日期格式EEE, dd MMM yyyy HH:mm:ss GMT注意Locale.ENGLISH不能省——中文环境下不指定Locale会导致星期和月份解析失败。在writeFile之前插一行判断if (notModified(exchange, file)) { exchange.sendResponseHeaders(304, -1); exchange.close(); return; }参数说明sendResponseHeaders(304, -1)的第二个参数传-1表示没有响应体这是304响应的固定写法——浏览器收到304后不会读body如果传了长度反而可能让浏览器卡住等一个永远等不到的内容。我把这段放进writeFile的入口做前置拦截保证所有文件类型都走缓存判断包括图片和字体文件。我那段时间做实操练习每次给服务器加缓存逻辑都会犯同一个错忘了截断到秒级结果改了个文件刷新页面一直看到的还是旧版排查了半天才发现是毫秒比较在作祟。从那以后我每次给静态资源加缓存都强制走一遍先看Files.getLastModifiedTime输出再格式化请求头里的时间戳最后用304确认。这套顺序保了我后面做所有本地服务面板的命希望也能帮你在“简单的JAVA HTML服务器”这个练习上少走几趟弯路。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

南京擅长和公检机关沟通的刑辩律师专业公司推荐,广受信赖口碑好 2026/9/29 19:49:47

南京擅长和公检机关沟通的刑辩律师专业公司推荐,广受信赖口碑好

在南京找一位懂公检办案流程、能在关键节点有效沟通的刑辩律师,是很多遭遇刑事纠纷的当事人和家属最迫切的需求。刑事辩护从侦查阶段的取保候审申请,到审查起诉阶段的不起诉沟通,再到审判阶段的法律适用辩论,每一个环节都离不开律…

阅读更多 →
TensorFlow 2024实战指南:从安装到部署的核心技术解析 2026/9/29 19:49:47

TensorFlow 2024实战指南:从安装到部署的核心技术解析

1. 这个题目为什么值得写:TensorFlow 是什么、能做什么、适合谁TensorFlow 是目前全球使用最广泛的深度学习框架之一,核心价值在于把“训练神经网络”这件事从理论变成了可落地的工业级流水线。很多新手第一次接触深度学习,装环境装到崩溃、跑…

阅读更多 →
ZYNQ视频输出链路:VTC与Video Out IP协同配置深度解析 2026/9/29 19:49:47

ZYNQ视频输出链路:VTC与Video Out IP协同配置深度解析

调试ZYNQ的视频输出通路时,Video Out IP和Video Timing Controller IP这对组合总是绕不开的。我之前做一块7020的HDMI输出板卡,现象是画面整体右移、底部出彩条,排查了一下午才发现是两边的时序参数口径不一致:VTC还在按1280x720的…

阅读更多 →
阻容降压电路原理与设计:低成本220V转5V的非隔离方案 2026/9/29 19:49:46

阻容降压电路原理与设计:低成本220V转5V的非隔离方案

很多刚玩嵌入式或电子DIY的朋友,第一次拆开LED小夜灯、触摸墙壁开关或者电表模块时,大概率都会愣一下:里面没有变压器,没有开关电源那种磁芯电感,就几个电容电阻加一个整流桥,居然就把220V交流变成了5V直流…

阅读更多 →
机房POE温湿度记录仪布设四维决策法:热力、网络、供电与维护 2026/9/29 19:49:46

机房POE温湿度记录仪布设四维决策法:热力、网络、供电与维护

1. 项目背景与真实痛点:为什么POE温湿度记录仪不是“换个设备”那么简单机房巡检这事,干过五年的老运维都懂——它根本不是“每天转一圈、拍张照、填个表”这么轻松。我接手这个项目前,上一套系统是用USB温湿度探头插在工控机上,再…

阅读更多 →
物理Agent Harness:从模型竞赛到系统落地的机器人工程框架 2026/9/29 19:49:40

物理Agent Harness:从模型竞赛到系统落地的机器人工程框架

这两年做机器人相关项目的人,应该都能感受到一个很明显的变化:大家讨论的重点,正从“哪个模型更强”慢慢转向“哪套系统更稳”。物理 Agent Harness这个概念,就是在这种背景下被反复提起的——它不是某个具体算法,而是…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉