如何在上传时使用多路复用http2功能



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

Java有一个HTTP客户端,它本机支持HTTP/2协议,所以考虑到这一点,我试图编写代码以便于理解。

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

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

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));
}
});

上传多个文件时,使用多路复用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  

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

多路复用对许多小请求非常有效,这就是浏览器的使用情况:许多小的GET请求(没有请求内容)可以在HTTP/2中多路复用,比相应的HTTP/1.1请求更早到达服务器,因此会更早得到服务并更早返回浏览器。

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

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

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

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

最新更新