Http 为什么Tomcat为HEAD返回不同的头,并向我的RESTful API获取请求?
我最初的目的是验证HTTP分块传输。但无意中发现了这种矛盾 API设计用于将文件返回到客户端。我使用Http 为什么Tomcat为HEAD返回不同的头,并向我的RESTful API获取请求?,http,tomcat8,http-method,chunked,Http,Tomcat8,Http Method,Chunked,我最初的目的是验证HTTP分块传输。但无意中发现了这种矛盾 API设计用于将文件返回到客户端。我使用HEAD和GET方法来对付它返回不同的标题。 对于GET,我得到以下标题:(这是我所期望的。) 对于标题,我得到以下标题: 根据,HEAD和GET应该返回相同的头,但不一定返回相同的头 我的问题是: 如果使用Transfer Encoding:chunked是因为文件被动态地馈送到客户端,而Tomcat服务器事先无法知道其大小,那么当使用HEAD方法时,Tomcat如何知道内容长度?Tomca
HEAD
和GET
方法来对付它返回不同的标题。
对于GET
,我得到以下标题:(这是我所期望的。)
对于标题
,我得到以下标题:
根据,HEAD
和GET
应该返回相同的头,但不一定返回相同的头
我的问题是:
如果使用Transfer Encoding:chunked
是因为文件被动态地馈送到客户端,而Tomcat服务器事先无法知道其大小,那么当使用HEAD
方法时,Tomcat如何知道内容长度?Tomcat是否只运行处理程序并计算所有文件字节?为什么它不简单地返回相同的传输编码:chunked
头
下面是我用SpringWebMVC实现的RESTfulAPI:
@RestController
public class ChunkedTransferAPI {
@Autowired
ServletContext servletContext;
@RequestMapping(value = "bootfile.efi", method = { RequestMethod.GET, RequestMethod.HEAD })
public void doHttpBoot(HttpServletResponse response) {
String filename = "/bootfile.efi";
try {
ServletOutputStream output = response.getOutputStream();
InputStream input = servletContext.getResourceAsStream(filename);
BufferedInputStream bufferedInput = new BufferedInputStream(input);
int datum = bufferedInput.read();
while (datum != -1) {
output.write(datum);
datum = bufferedInput.read();
}
output.flush();
output.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
添加1
在我的代码中,我没有显式地添加任何标题,那么必须是Tomcat添加它认为合适的内容长度
和传输编码
标题
那么,Tomcat决定发送哪些头的规则是什么?
添加2
也许这与Tomcat的工作方式有关。我希望有人能给我们一些启示。否则,我将调试到Tomcat8的源代码中并共享结果。但这可能需要一段时间
相关:
Tomcat是否只是干运行处理程序并计算所有文件字节
是的,javax.servlet.http.HttpServlet.doHead()的默认实现就是这样做的
您可以查看HttpServlet.java中的帮助器类NoBodyResponse、NoBodyOutputStream
DefaultServlet类(用于服务静态文件的TomcatServlet)更明智。它能够发送正确的内容长度值,并为文件子集(范围
头)的GET请求提供服务。您可以使用
ServletContext.getNamedDispatcher("default").forward(request, response);
尽管这看起来很奇怪,但根据服务器必须返回的数据类型,仅在响应HEAD
请求时发送大小并在响应GET
请求时分块可能是有意义的
虽然您的API似乎提供了一个静态文件,但您也讨论了动态创建的文件或数据,所以我将在这里进行概述(一般也针对Web服务器)
首先让我们看看GET
和HEAD
的不同用法:
- 使用
GET
时,客户机请求整个文件或数据(或一系列数据),并希望尽快获得。因此,服务器没有特定的理由首先发送数据的大小,特别是当它可以在分块模式下更快/更快地发送数据时。因此,这里最好采用最快的方式(无论如何,下载后客户端都会有相应的大小)
- 另一方面,客户通常需要一些特定的信息。这可能只是对存在性或“上次更改”的检查,但如果客户端需要数据的某一部分(使用范围请求,包括检查该请求是否支持范围请求),或者出于某种原因只需要预先知道数据的大小,也可以使用它
让我们看看一些可能的情况:
静态文件:
@RestController
public class ChunkedTransferAPI {
@Autowired
ServletContext servletContext;
@RequestMapping(value = "bootfile.efi", method = { RequestMethod.GET, RequestMethod.HEAD })
public void doHttpBoot(HttpServletResponse response) {
String filename = "/bootfile.efi";
try {
ServletOutputStream output = response.getOutputStream();
InputStream input = servletContext.getResourceAsStream(filename);
BufferedInputStream bufferedInput = new BufferedInputStream(input);
int datum = bufferedInput.read();
while (datum != -1) {
output.write(datum);
datum = bufferedInput.read();
}
output.flush();
output.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
HEAD
:没有理由不在响应头中包含大小,因为该信息可用
GET
:大多数情况下,大小将包含在头中,数据将一次性发送,除非有特定的性能原因需要成批发送。另一方面,您似乎希望为您的文件进行分块传输,所以这在这里是有意义的
实时日志文件:
@RestController
public class ChunkedTransferAPI {
@Autowired
ServletContext servletContext;
@RequestMapping(value = "bootfile.efi", method = { RequestMethod.GET, RequestMethod.HEAD })
public void doHttpBoot(HttpServletResponse response) {
String filename = "/bootfile.efi";
try {
ServletOutputStream output = response.getOutputStream();
InputStream input = servletContext.getResourceAsStream(filename);
BufferedInputStream bufferedInput = new BufferedInputStream(input);
int datum = bufferedInput.read();
while (datum != -1) {
output.write(datum);
datum = bufferedInput.read();
}
output.flush();
output.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
好吧,有点奇怪,但也有可能:下载一个文件时,文件大小可能会发生变化
HEAD
:同样,客户机可能需要文件大小,服务器可以在文件头的特定时间轻松提供文件大小
GET
:由于可以在下载时添加日志行,因此事先不知道日志行的大小。唯一的选择是发送分块
具有固定大小记录的表:
@RestController
public class ChunkedTransferAPI {
@Autowired
ServletContext servletContext;
@RequestMapping(value = "bootfile.efi", method = { RequestMethod.GET, RequestMethod.HEAD })
public void doHttpBoot(HttpServletResponse response) {
String filename = "/bootfile.efi";
try {
ServletOutputStream output = response.getOutputStream();
InputStream input = servletContext.getResourceAsStream(filename);
BufferedInputStream bufferedInput = new BufferedInputStream(input);
int datum = bufferedInput.read();
while (datum != -1) {
output.write(datum);
datum = bufferedInput.read();
}
output.flush();
output.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
假设服务器需要发回一个包含来自多个源/数据库的固定长度记录的表:
HEAD
:客户可能需要尺寸。服务器可以快速查询每个数据库中的计数,并将计算出的大小发送回客户端
GET
:与其先查询每个数据库中的计数,服务器最好开始以块的形式从每个数据库发送结果记录
动态生成的zip文件:
@RestController
public class ChunkedTransferAPI {
@Autowired
ServletContext servletContext;
@RequestMapping(value = "bootfile.efi", method = { RequestMethod.GET, RequestMethod.HEAD })
public void doHttpBoot(HttpServletResponse response) {
String filename = "/bootfile.efi";
try {
ServletOutputStream output = response.getOutputStream();
InputStream input = servletContext.getResourceAsStream(filename);
BufferedInputStream bufferedInput = new BufferedInputStream(input);
int datum = bufferedInput.read();
while (datum != -1) {
output.write(datum);
datum = bufferedInput.read();
}
output.flush();
output.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
也许不常见,但这是一个有趣的例子
假设您希望根据一些参数向用户提供动态生成的zip文件
让我们首先看看zip文件的结构:
有两个部分:首先,每个文件都有一个块:一个小标题,后面是该文件的压缩数据。然后是zip文件中所有文件的列表(包括大小/位置)
因此,可以在磁盘上预先生成每个文件的准备块(以及存储在某些数据结构中的名称/大小)
HEAD
:客户端可能想知道此处的大小。服务器可以轻松计算所有所需块的大小+第二部分的大小以及其中的文件列表
<