Chrome提交表单时偶发不向自定义Java服务器发送数据问题求助
我看了你遇到的问题——自己实现的Java HTTP服务器处理multipart文件上传时,明明请求头里的Content-Length显示有数据,却经常读不到请求体,偶尔又能成功,完全找不到规律。这个问题大概率出在你读取请求的方式上,特别是Scanner和原始InputStream混用以及请求体读取逻辑的几个细节上。
问题根源分析
1. Scanner与InputStream混用导致的字节丢失
你用Scanner来读取请求行和请求头,然后直接用同一个InputStream去读请求体,这是核心问题。Scanner内部会维护一个缓冲区,当你调用s.next()(用\r\n\r\n作为分隔符)时,它可能会从InputStream中读取超过请求头范围的字节到自己的缓冲区里——也就是把请求体的开头部分也提前“吃掉”了。这时候后面再用is.read()去读请求体,自然就找不到那些被Scanner拿走的字节,导致读不全甚至完全读不到。
2. 依赖is.available()读取的逻辑缺陷
InputStream.available()的作用是返回当前可以无阻塞读取的字节数,它不等于剩余需要读取的总字节数。在网络场景下,请求体的字节可能是分批次到达服务器的,比如某次调用available()返回0,只是暂时没有可立即读取的数据,并不是没有更多数据了。你的代码里如果这时候判定numread=0就计数,超时后break,就会提前终止读取,导致请求体读不全。
3. 自定义超时逻辑的不可靠性
你用numzeros>100(也就是等待1秒)来判断超时,但网络传输的延迟是不确定的,有时候数据可能需要更长时间到达,而available()返回0只是暂时的,这会误判为超时,中断读取。
修复方案
方案一:统一用InputStream读取所有数据(推荐)
放弃Scanner,直接从InputStream读取字节,手动解析请求行和请求头,这样能保证整个请求的字节流完全可控,不会出现被Scanner提前读取的问题。示例代码如下:
import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.regex.Matcher; import java.util.regex.Pattern; public class HTTPRequest { public final String request, requestline, headers, data; private static final Pattern contentLengthPattern = Pattern.compile("Content-Length\\s*:\\s*(\\d+)", Pattern.CASE_INSENSITIVE); public HTTPRequest(InputStream is) throws IOException { ByteArrayOutputStream headerBuffer = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int bytesRead; int state = 0; // 状态机跟踪换行:0=正常,1=读到\r,2=读到\r\n,3=读到\r\n\r // 读取直到找到请求头结束标记\r\n\r\n while ((bytesRead = is.read(buffer)) != -1) { for (int i = 0; i < bytesRead; i++) { byte b = buffer[i]; headerBuffer.write(b); // 更新状态机 if (state == 0 && b == '\r') { state = 1; } else if (state == 1 && b == '\n') { state = 2; } else if (state == 2 && b == '\r') { state = 3; } else if (state == 3 && b == '\n') { // 找到请求头结束点,停止读取头 break; } else { state = 0; } } if (state == 3 && bytesRead > 0 && buffer[bytesRead-1] == '\n') { break; } } // 解析请求行和请求头字符串 String headerStr = new String(headerBuffer.toByteArray(), StandardCharsets.UTF_8); String[] headerLines = headerStr.split("\r\n"); requestline = headerLines[0]; // 去掉最后的空行(\r\n\r\n的部分) headers = String.join("\r\n", Arrays.copyOfRange(headerLines, 1, headerLines.length - 2)); // 提取Content-Length Matcher matcher = contentLengthPattern.matcher(headerStr); int contentLength = 0; if (matcher.find()) { contentLength = Integer.parseInt(matcher.group(1)); } // 读取请求体:直接读取指定长度的字节,阻塞等待数据到达 byte[] bodyBytes = new byte[contentLength]; int totalRead = 0; while (totalRead < contentLength) { bytesRead = is.read(bodyBytes, totalRead, contentLength - totalRead); if (bytesRead == -1) { throw new IOException("连接中断,未读取完整请求体"); } totalRead += bytesRead; } data = new String(bodyBytes, 0, totalRead, StandardCharsets.UTF_8); // 拼接完整请求 request = headerStr + data; } }
方案二:优化超时与读取逻辑(如果坚持用Scanner)
如果你一定要保留Scanner,那需要处理它的缓冲区问题(比如用PushbackInputStream把多读的字节推回去),但这种方式容易出错,不推荐。同时要优化读取逻辑:
- 不要依赖
is.available(),直接指定需要读取的剩余字节数,让read()阻塞等待数据。 - 用
Socket的setSoTimeout()来设置读取超时,比自定义计数更可靠:// 在获取Socket的InputStream之前设置超时 socket.setSoTimeout(5000); // 5秒超时 InputStream is = socket.getInputStream();
额外建议
处理multipart/form-data类型的请求时,后续还需要解析边界、文件名和文件内容,这部分逻辑比较复杂,建议参考相关HTTP规范,或者直接使用成熟的库(比如Apache Commons FileUpload)来简化开发,避免自己造轮子踩更多坑。
内容的提问来源于stack exchange,提问作者Arik Rundquist

