从裸机 Socket 到 HTTP Server
main 方法里,Java 的 I/O 陷阱可能比你想象中深得多。最近在尝试脱离 Spring 和 Tomcat,纯用 java.net 从零构建一个原生的 HTTP Server。第一周的代码写得挺“脏”,但逻辑简单粗暴,每次处理完请求都会手动调用 pw.flush() 和 client.close(),一切运行正常。
但在进行架构重构,试图把连接处理逻辑剥离到 ClientWorker 类时,我踩了一个极其隐蔽的坑。
当时的代码逻辑是这样的:
BufferedReader br = new BufferedReader(
new InputStreamReader(client.getInputStream())
);
String line;
if ((line = br.readLine()) != null) {
String res = Dispatcher.Rout(line);
PrintWriter pw = new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(client.getOutputStream())
)
);
pw.println(res);
}
client.close();编译没报错,运行也没异常,甚至连 accept 都能正常接收连接。但诡异的是,浏览器端会一直处于 Loading 状态,死活等不到任何响应。
排查了半天发现,问题出在重构时“顺手”漏掉了 pw.flush()。
在 Java 的 I/O 设计里,这种嵌套构造器:PrintWriter -> BufferedWriter -> OutputStreamWriter -> Socket.getOutputStream()
每一层其实都是为了处理效率。BufferedWriter 会在内存里开辟缓冲区,它并不会在你调用 println 的瞬间就把数据发给内核,而是等攒够了或者被显式触发时才执行真正的 I/O 操作。
我当时以为直接 client.close() 就能把流关掉,结果由于没有触发最后的 flush,数据全堆在内存缓冲区里,随着 Socket 的关闭直接消失了,或者说由于没有完成最后的 handoff,内核根本没收到发送指令。
对于我们做 DevOps 或系统运维的人来说,这种 I/O 跨越用户态到内核态的开销(Context Switch)才是性能损耗的真凶。如果不理解缓冲区机制,单纯靠堆砌对象层级,最后只会造出一个既慢又不可靠的“黑盒”。
如果以后要在生产环境写这种底层组件,建议还是把流的管理逻辑做严密点,别让重构毁了稳定性。