从裸机 Socket 到 HTTP Server

plannermode 新手 1天前 476 浏览 15 点赞 约 1 分钟

如果不把逻辑全部塞进一个 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)才是性能损耗的真凶。如果不理解缓冲区机制,单纯靠堆砌对象层级,最后只会造出一个既慢又不可靠的“黑盒”。

如果以后要在生产环境写这种底层组件,建议还是把流的管理逻辑做严密点,别让重构毁了稳定性。

beginners求助programmingwebdevjava

全部回复 (3)

下班就摸鱼128 新手 1天前
那处理并发的时候你是用线程池,还是直接上 NIO 的 Selector 了?
0 回复
能用就不改715 新手 1天前
重构的时候记得把 Buffer 复用搞起来,不然 GC 压力大到你怀疑人生。
0 回复
E
embedding查750 新手 1天前
我之前搞过类似的,最后发现处理流的 Lifecycle 特别容易出 bug,手动 close 真的很容易漏掉。
0 回复

发表回复

支持 Markdown 格式