接收 HTTP Body
优质
小牛编辑
128浏览
2023-12-01
解析请求中的Body需要注意Transfer-Encoding头。
当Transfer-Encoding的值为chunked时,不应该使用Content-Length去读取Body。
chunked的Body格式为:
数据以一系列分块的形式进行发送。
首部在这种情况下不被发送。。在每一个分块的开头需要添加当前分块的长度,以十六进制的形式表示,后面紧跟着 '
\r\n
' ,之后是分块本身,后面也是'
\r\n
' 。终止块是一个常规的分块,不同之处在于其长度为0。终止块后面是一个挂载(trailer),由一系列(或者为空)的实体消息首部构成
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Transfer-Encoding
所以chunked的Body使用 0\r\n\r\n 作为Body的结束标志。
由此可以构造延迟读取Body内容的流:
public class HttpBodyInputStream extends InputStream {
//chunked body使用 0\r\n\r\n结尾
private static final char[] TAILER_CHARS = {'0', '\r', '\n', '\r', '\n'};
private final InputStream inputStream;
private long contentLength;
private boolean isChuncked;
private long readed = 0;
private boolean isFinished = false;
private int idx = 0;
public HttpBodyInputStream(InputStream inputStream, boolean ifChuncked) {
this.inputStream = inputStream;
this.isChuncked = ifChuncked;
}
@Override
public int read() throws IOException {
if (isChuncked) {
return readChunked();
} else {
return readByte();
}
}
/**
* 读取chunked body
*
* @return
* @throws IOException
*/
private int readChunked() throws IOException {
if (isFinished) {
return -1;
}
int i = this.inputStream.read();
if (i == -1) {
return i;
} else {
if (idx == TAILER_CHARS.length - 1) {
isFinished = true;
} else if (TAILER_CHARS[idx++] != (char) i) {
idx = 0;
}
}
return i;
}
/**
* 读取非chunked body
*
* @return
* @throws IOException
*/
private int readByte() throws IOException {
readed++;
if (readed > contentLength) {
return -1;
} else {
return this.inputStream.read();
}
}
public long getContentLength() {
return contentLength;
}
public void setContentLength(long contentLength) {
this.contentLength = contentLength;
}
}
此处BodyParser并不处理压缩和编码,只是直接返回raw body,使用处再根据实际情况进行处理。
单元测试:
public class TestHttpBodyInputStream {
private static final Logger LOGGER =
LoggerFactory.getLogger(TestHttpBodyInputStream.class);
/*
7
Mozilla
9
Developer
7
Network
0
*/
private static final String TEST_OTHER_MESSAGE = "testtest";
private static final String CHUNKED_BODY = "7\r\n" +
"Mozilla\r\n" +
"9\r\n" +
"Developer\r\n" +
"7\r\n" +
"Network\r\n" +
"0\r\n" +
"\r\n";
private static final String BODY = CHUNKED_BODY + TEST_OTHER_MESSAGE;
private static final String NORMAL_BODY = "Mozilla Developer Network";
@Test
public void testReadChunkedBody() throws IOException {
InputStream in = new ByteArrayInputStream(BODY.getBytes());
HttpBodyInputStream httpBodyInputStream = new HttpBodyInputStream(in, true);
ByteOutputStream out = new ByteOutputStream();
IOUtils.copy(httpBodyInputStream, out);
LOGGER.info(out.toString());
assertEquals(CHUNKED_BODY, out.toString());
}
@Test
public void testReadBody() throws IOException {
InputStream in = new ByteArrayInputStream(NORMAL_BODY.getBytes());
HttpBodyInputStream httpBodyInputStream = new HttpBodyInputStream(in, false);
httpBodyInputStream.setContentLength(25);
ByteOutputStream out = new ByteOutputStream();
IOUtils.copy(httpBodyInputStream, out);
LOGGER.info(out.toString());
assertEquals(NORMAL_BODY, out.toString());
}
}
完整代码:https://github.com/pkpk1234/BeggarServletContainer/tree/step10