0

上传多个文件时,使用多路复用 http2 功能应该会显着提高性能。

Java 有一个本地支持 HTTP/2 协议的 httpclient,所以考虑到我尝试编写代码以供自己理解。

这项任务似乎并不像我最初想的那样容易,或者另一方面,我似乎无法找到能够在上传中使用多路复用的服务器(如果存在)。

这是我写的代码,有人有想法吗?

HttpClient httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build();
String url = "https://your-own-http2-server.com/incoming-files/%s";
Path basePath = Path.of("/path/to/directory/where/is/a/bunch/of/jpgs");

Function<Path, CompletableFuture<HttpResponse<String>>> handleFile = file -> {
    String currentUrl = String.format(url, file.getFileName().toString());
    try {
        HttpRequest request = HttpRequest.newBuilder()
                                         .uri(URI.create(currentUrl))
                                         .header("Content-Type", "image/jpeg")
                                         .PUT(HttpRequest.BodyPublishers.ofFile(file))
                                         .build();
        return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString());
    } catch (IOException e) {
        e.printStackTrace();
        throw new RuntimeException(e);
    }
};

List<Path> files = Files.list(basePath).collect(toList());

files.parallelStream().map(handleFile).forEach(c -> {
         try {
             final HttpResponse<String> response = c.get();
             System.out.println(response.statusCode());
         } catch (Exception e) {
             e.printStackTrace();
             throw new RuntimeException((e));
         }
     });
4

2 回答 2

8

上传多个文件时,使用多路复用 http2 功能应该会显着提高性能。

这是一个通常是错误的假设。

让我们放弃您有多个 HTTP/1.1 连接的情况,以便您可以并行上传。

然后我们有 1 个 TCP 连接,我们想将上传与 HTTP/1.1 和 HTTP/2 进行比较。

在 HTTP/1.1 中,请求会一个接一个地被序列化,所以多次上传的结束时间取决于连接的带宽(忽略 TCP 慢启动)。

在 HTTP/2 中,请求将通过多路复用来交错。但是,需要发送的数据是相同的,因此多次上传的结束时间再次取决于连接的带宽。

在 HTTP/1.1 中,您将拥有upload1.start...upload1.end|upload2.start...upload2.end|upload3.start...upload3.end等。

在 HTTP/2 中,您将拥有upload1.start|upload2.start|upload3.start.....upload3.end..upload1.end..upload2.end

结束时间将是相同的。

HTTP/2 的问题在于您通常不受连接带宽的限制,而是受到 HTTP/2 流量控制窗口的限制,该窗口通常要小得多

HTTP/2 规范默认 HTTP/2 流控制窗口为 65535 字节,这意味着客户端必须每隔 65535 字节停止发送数据,直到服务器确认这些字节。这可能需要一个往返,因此即使对于大文件上传的往返时间很小(例如 50 毫秒),您也可能会多次支付此往返费用,从而为您的上传增加秒数(例如,对于 6 MiB 的上传,您可能需要支付 100次,即 5 秒)。

因此,为服务器配置一个大的 HTTP/2 流控制窗口非常重要,尤其是当您的服务器用于文件上传时。服务器上较大的 HTTP/2 流控制窗口意味着服务器必须准备好缓冲大量字节,这意味着主要处理文件上传的 HTTP/2 服务器将需要比 HTTP/1.1 服务器更多的内存。

使用较大的 HTTP/2 流控制窗口,服务器可能很智能,并在客户端仍在上传时向客户端发送确认。

当客户端上传时,它会缩小其“发送”窗口。通过接收来自服务器的确认,客户端放大“发送”窗口。

典型的不良交互是,指示客户端“发送”窗口值,从 1 MiB 开始:

[client send window]

1048576 
        client sends 262144 bytes
786432  
        client sends 262144 bytes
524288  
        client sends 262144 bytes
262144  
        client sends 262144 bytes
0       
        client cannot send
.
. (stalled)
.
        client receives acknowledgment from server (524288 bytes)
524288  
        client sends 262144 bytes
262144  
        client sends 262144 bytes
0       
        client cannot send
.
. (stalled)
.

一个好的互动应该是:

[client send window]

1048576 
        client sends 262144 bytes
786432  
        client sends 262144 bytes
524288  
        client sends 262144 bytes
262144  
        client receives acknowledgment from server (524288 bytes)
786432  
        client sends 262144 bytes
524288  
        client sends 262144 bytes
262144  
        client receives acknowledgment from server (524288 bytes)
786432  

正如您在良好的交互中看到的那样,在客户端耗尽“发送”窗口之前,服务器正在确认客户端,因此客户端可以保持全速发送。

多路复用对于许多小请求确实有效,这就是浏览器用例:许多可以在 HTTP/2 中多路复用的小型 GET 请求(没有请求内容),比对应的 HTTP/1.1 请求更早到达服务器,并且因此将更早地提供服务并更早地返回浏览器。

对于大型请求,就像文件上传的情况一样,HTTP/2 可以与 HTTP/1.1 一样高效,但如果服务器的默认配置使其性能远低于 HTTP/1.1 - HTTP/2,我不会感到惊讶将需要对服务器配置进行一些调整。

HTTP/2 流控制窗口也可能妨碍下载,因此通过 HTTP/2 从服务器下载大型内容可能非常慢(原因与上述相同)。

浏览器通过告诉服务器有一个非常大的服务器“发送”窗口来避免这个问题 - Firefox 72 将它设置为每个连接 12 MiB,并且非常聪明地确认服务器,这样它就不会停止下载。

于 2020-02-06T18:06:16.930 回答
1

通过原始正文数据提供的java.net.http.HttpClient句柄字节BodyPublisher,没有任何解释。为了说明我的观点,无论您使用HttpRequest.BodyPublishers::ofFile(Path)还是HttpRequest.BodyPublishers::ofByteArray(byte[])在语义上无关:改变的只是如何获得将传输到远程方的字节。在文件上传的情况下 - 服务器可能期望请求正文将以某种方式格式化。它还可能期望一些特定的标头与请求一起传输(例如 Content-Type 等)。HttpClient 不会为您神奇地做到这一点。目前,API 没有提供开箱即用的功能。您需要在调用者级别实现它。(记录了一个 RFE 用于调查对 multipart/form-data 的支持,但尚未在 API 中实现https://bugs.openjdk.java.net/browse/JDK-8235761)。

于 2020-02-06T16:08:05.053 回答