如何使用 HTTP::Server 同时流式传输多个文件?
How can I stream multiple files at the same time using HTTP::Server?
我正在开发一个提供大文件的 HTTP 服务。我注意到并行下载是不可能的。该过程一次只提供一个文件,所有其他下载都在等待上一个下载完成。如何同时流式传输多个文件?
require "http/server"
server = HTTP::Server.new(3000) do |context|
context.response.content_type = "application/data"
f = File.open "bigfile.bin", "r"
IO.copy f, context.response.output
end
puts "Listening on http://127.0.0.1:3000"
server.listen
一次请求一个文件:
$ ab -n 10 -c 1 127.0.0.1:3000/
[...]
Percentage of the requests served within a certain time (ms)
50% 9
66% 9
75% 9
80% 9
90% 9
95% 9
98% 9
99% 9
100% 9 (longest request)
一次请求 10 个文件:
$ ab -n 10 -c 10 127.0.0.1:3000/
[...]
Percentage of the requests served within a certain time (ms)
50% 52
66% 57
75% 64
80% 69
90% 73
95% 73
98% 73
99% 73
100% 73 (longest request)
这里的问题是File#read
和context.response.output
都不会阻塞。 Crystal 的并发模型基于协作调度的纤程,其中切换纤程仅在 IO 阻塞时发生。使用非阻塞 IO 从磁盘读取是不可能的,这意味着唯一可能阻塞的部分是写入 context.response.output
。然而,在同一台机器上,磁盘 IO 比网络 IO 慢很多,这意味着写入永远不会阻塞,因为 ab 的读取速度比磁盘提供数据的速度快得多,即使是从磁盘缓存中也是如此。这个例子实际上是打破 crystal 并发性的完美风暴。
在现实世界中,服务的客户端更有可能驻留在计算机的网络上,这使得响应写入偶尔会阻塞。此外,如果您正在从其他网络服务或 pipe/socket 读取数据,您也会被阻止。另一种解决方案是使用线程池来实现非阻塞文件 IO,这就是 libuv 所做的。作为旁注,Crystal 移至 libevent,因为 libuv 不允许多线程事件循环(即让任何线程恢复任何纤程)。
调用Fiber.yield
将执行传递给任何挂起的纤程是正确的解决方案。下面是一个在读取文件时如何阻塞(和产生)的例子:
def copy_in_chunks(input, output, chunk_size = 4096)
size = 1
while size > 0
size = IO.copy(input, output, chunk_size)
Fiber.yield
end
end
File.open("bigfile.bin", "r") do |file|
copy_in_chunks(file, context.response)
end
这是此处讨论的转录:https://github.com/crystal-lang/crystal/issues/4628
向 GitHub 用户 @cschlack、@RX14 和 @ysbaddaden 的支持
我正在开发一个提供大文件的 HTTP 服务。我注意到并行下载是不可能的。该过程一次只提供一个文件,所有其他下载都在等待上一个下载完成。如何同时流式传输多个文件?
require "http/server"
server = HTTP::Server.new(3000) do |context|
context.response.content_type = "application/data"
f = File.open "bigfile.bin", "r"
IO.copy f, context.response.output
end
puts "Listening on http://127.0.0.1:3000"
server.listen
一次请求一个文件:
$ ab -n 10 -c 1 127.0.0.1:3000/
[...]
Percentage of the requests served within a certain time (ms)
50% 9
66% 9
75% 9
80% 9
90% 9
95% 9
98% 9
99% 9
100% 9 (longest request)
一次请求 10 个文件:
$ ab -n 10 -c 10 127.0.0.1:3000/
[...]
Percentage of the requests served within a certain time (ms)
50% 52
66% 57
75% 64
80% 69
90% 73
95% 73
98% 73
99% 73
100% 73 (longest request)
这里的问题是File#read
和context.response.output
都不会阻塞。 Crystal 的并发模型基于协作调度的纤程,其中切换纤程仅在 IO 阻塞时发生。使用非阻塞 IO 从磁盘读取是不可能的,这意味着唯一可能阻塞的部分是写入 context.response.output
。然而,在同一台机器上,磁盘 IO 比网络 IO 慢很多,这意味着写入永远不会阻塞,因为 ab 的读取速度比磁盘提供数据的速度快得多,即使是从磁盘缓存中也是如此。这个例子实际上是打破 crystal 并发性的完美风暴。
在现实世界中,服务的客户端更有可能驻留在计算机的网络上,这使得响应写入偶尔会阻塞。此外,如果您正在从其他网络服务或 pipe/socket 读取数据,您也会被阻止。另一种解决方案是使用线程池来实现非阻塞文件 IO,这就是 libuv 所做的。作为旁注,Crystal 移至 libevent,因为 libuv 不允许多线程事件循环(即让任何线程恢复任何纤程)。
调用Fiber.yield
将执行传递给任何挂起的纤程是正确的解决方案。下面是一个在读取文件时如何阻塞(和产生)的例子:
def copy_in_chunks(input, output, chunk_size = 4096)
size = 1
while size > 0
size = IO.copy(input, output, chunk_size)
Fiber.yield
end
end
File.open("bigfile.bin", "r") do |file|
copy_in_chunks(file, context.response)
end
这是此处讨论的转录:https://github.com/crystal-lang/crystal/issues/4628
向 GitHub 用户 @cschlack、@RX14 和 @ysbaddaden 的支持