Java 解析 HTTP 请求时为何会阻塞,如何正确判断请求结束?
结论
这个问题不需要靠改用非阻塞 socket 来解决。关键是根据 HTTP 协议判断请求何时完整,并让解析器处理 read 实际读到的所有字节。InputStream.read(buffer) 不保证一次填满缓冲区,也不会因为缓冲区够大就自动解析完整请求。
在你给出的循环里,input.read(buffer) 会等到有数据可读或遇到 EOF。只要 request.isDone() 仍为 false,代码就会继续从连接读取。HTTP/1.1 连接可以保持打开,因此不能用“对端关闭连接”来判断请求结束。
为什么缓冲区大小会影响现象
以 curl -v http://localhost:5000/hello 为例,客户端发送的 HTTP 请求包含请求行和请求头。对于没有请求体的请求,收到头部结束标记 \r\n\r\n 后,请求就完整了。连接是否继续保持打开,不影响当前请求是否结束。
缓冲区较小时,数据可能分几次到达,解析器也会有多次机会处理数据。缓冲区较大时,一次 read 可能取走多行。如果 parseRequest(buffer, n) 每次只处理一行,或者处理到某个边界后没有保留并继续处理本次读取中的剩余字节,下一轮循环就会等待 socket 上的新数据。curl 已经发完请求并保持连接时,这次读取可能一直阻塞。
这种现象通常说明解析器没有正确消费输入,或没有正确判断请求是否完成。由于没有提供 parseRequest 的实现,目前无法确定具体是哪一处出了问题。
处理方式
让解析器处理本次读取实际获得的 n 个字节,并保留尚未处理的数据。请求行、请求头和可能存在的请求体都可能跨多次读取到达;同一次读取中也可能包含多个协议行,解析器需要处理这两种情况。
byte[] buffer = new byte[1024];
while (!request.isDone()) {
int n = input.read(buffer);
if (n == -1) {
// 对端关闭连接;如果请求仍不完整,应按解析器设计报告不完整请求。
break;
}
// 伪代码:将 buffer[0..n) 中的全部字节交给有状态解析器。
// 解析器应保存尚未消费完的数据,并依据 HTTP 消息边界更新完成状态。
parser.consume(buffer, 0, n);
}
parser.consume 是说明用的占位名称,不是 Java 标准 API。你需要根据自己的解析器接口实现相应逻辑。可以重点检查 parseRequest 是否遇到第一行就返回,以及调用方是否丢弃了同一批数据中的剩余部分。
请求是否完成,应由 HTTP 消息边界决定:头部以空行结束;如果请求包含请求体,则按 Content-Length 指定的长度读取,或依据 Transfer-Encoding: chunked 的分块结束规则读取。不能只检查请求行,也不能把连接关闭作为正常请求唯一的结束条件。
超时能解决什么
可以为 socket 设置读取超时,避免异常情况下无限等待,例如使用 Socket.setSoTimeout(...)。超时只能限制等待时间。如果解析器没有正确识别请求边界,请求仍然不会因此变完整,还需要处理超时和不完整请求。
也不要用 InputStream.available() 判断请求是否读完。它只能反映当前无需阻塞即可读取的字节数,不能用来判断 HTTP 请求的边界或完整性。
备注:内容仅供参考。
