【问题标题】:Why Tomcat returns different headers for HEAD and GET requests to my RESTful API?为什么 Tomcat 为我的 RESTful API 返回 HEAD 和 GET 请求的不同标头?
【发布时间】:2016-03-21 07:14:03
【问题描述】:

我最初的目的是验证 HTTP 分块传输。但是无意中发现了这个不一致。

API 旨在将文件返回给客户端。我对它使用HEAD 和GET 方法。 返回不同的标头。

对于GET,我得到了这些标题:(这是我所期望的。)

对于HEAD,我得到了这些标题:

根据this thread、HEAD 和GET 应该返回相同的标头,但不一定。

我的问题是:

如果使用Transfer-Encoding: chunked因为文件是动态提供给客户端的,而 Tomcat 服务器无法事先知道它的大小,Tomcat 怎么可能知道Content-Length使用HEAD方法? Tomcat 是否只是 dry-run 处理程序并计算所有文件字节数?为什么它不简单地返回相同的 Transfer-Encoding: chunked 标头?

下面是我用 Spring Web MVC 实现的 RESTful API:

@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 以它认为合适的方式添加 Content-Length 和 Transfer-Encoding 标头。

那么,Tomcat 决定发送哪些标头的规则是什么?

添加 2 个

可能与Tomcat的工作方式有关。我希望有人可以在这里有所启发。否则,我将调试到Tomcat 8的源代码并分享结果。但这可能需要一段时间。

相关:

【问题讨论】:

    标签: http tomcat8 http-method chunked


    【解决方案1】:

    Tomcat 是否只是空运行处理程序并计算所有文件字节数?

    是的,javax.servlet.http.HttpServlet.doHead() 的默认实现就是这样做的。

    您可以查看 HttpServlet.java 中的辅助类 NoBodyResponse、NoBodyOutputStream

    DefaultServlet 类(用于提供静态文件的 Tomcat servlet)更为明智。它能够发送正确的 Content-Length 值,以及为文件子集(Range 标头)提供 GET 请求。您可以将您的请求转发到该 servlet,使用

      ServletContext.getNamedDispatcher("default").forward(request, response);
    

    【讨论】:

      【解决方案2】:

      虽然看起来很奇怪,但根据服务器必须返回的数据类型,仅发送大小以响应 HEAD 请求并分块以响应 GET 请求可能是有意义的.

      虽然您的 API 似乎提供了一个静态文件,但您也谈到了动态创建的文件或数据,所以我将在这里泛泛而谈(也适用于一般的网络服务器)。

      首先让我们看看GET和HEAD的不同用法:

      • 使用GET,客户端请求整个文件或数据(或数据范围),并希望它尽可能快。因此,服务器没有特别的理由首先发送数据的大小,尤其是当它可以在分块模式下更快/更快地开始发送时。所以这里首选最快的方式(无论如何客户端都会有下载后的大小)。

      • 另一方面,使用HEAD,客户通常需要一些特定的信息。这可能只是检查存在或“最后更改”,但如果客户端想要数据的某个部分(使用范围请求,包括检查该请求是否支持范围请求),也可以使用它),或者只是出于某种原因需要预先知道数据的大小。

      让我们看看一些可能的情况:

      静态文件:

      HEAD:没有理由不在响应标头中包含大小,因为该信息可用。

      GET: 大多数情况下,大小会包含在标头中,并且一次性发送的数据,除非有特定的性能原因需要分块发送。另一方面,您似乎希望为您的文件进行分块传输,所以这在这里可能是有意义的。

      实时日志文件:

      好的,有点奇怪,但有可能:下载的文件在下载时大小可能会发生变化。

      HEAD:同样,客户端可能想要大小,服务器可以轻松地在标头中提供特定时间的文件大小。

      GET:因为下载时可以添加日志,所以前面的大小是未知的。唯一的选择是发送分块。

      具有固定大小记录的表:

      假设服务器需要发回一个包含来自多个源/数据库的固定长度记录的表:

      HEAD: 大小可能是客户想要的。服务器可以快速查询每个数据库中的 count,并将计算出的大小发送回客户端。

      GET: 与其先在每个数据库中查询 count,不如让服务器开始以块的形式从每个数据库发送结果记录。

      动态生成的 zip 文件:

      也许不常见,但一个有趣的例子。

      假设您想根据一些参数向用户提供动态生成的 zip 文件。

      让我们先看一下 zip 文件的结构:

      有两个部分:首先,每个文件都有一个块:一个小标题,然后是该文件的压缩数据。然后是 zip 文件中所有文件的列表(包括大小/位置)。

      因此可以在磁盘上预先生成每个文件的准备块(以及存储在某些数据结构中的名称/大小。

      HEAD: 客户可能想知道这里的尺寸。服务器可以很容易地计算出所有需要的块的大小+第二部分的大小以及里面的文件列表。

      如果客户端想要提取单个文件,它可以直接请求文件的最后一部分(带有范围请求)以获取列表,然后通过第二个请求请求该单个文件。尽管获取最后 n 个字节不一定需要该大小,但如果您想将不同部分存储在与完整 zip 文件大小相同的稀疏文件中,它可能会很方便。

      GET: 无需先进行计算(包括生成第二部分以了解其大小)。以块的形式开始发送每个块会更好更快。

      完全动态生成的文件:

      在这种情况下,将大小返回到HEAD 请求当然不是很有效,因为需要生成整个文件才能知道它的大小。

      【讨论】:

        猜你喜欢
        • 2017-04-15
        • 1970-01-01
        • 2020-03-17
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-11-15
        • 1970-01-01
        • 2023-01-13
        相关资源
        最近更新 更多