如何使用HTTP :: Server同时流式传输多个文件?

时间:2017-06-28 12:43:01

标签: crystal-lang

我正在开发一个提供大文件的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)

1 个答案:

答案 0 :(得分:6)

这里的问题是File#readcontext.response.output都不会阻止。 Crystal的并发模型基于协同调度的光纤,其中交换光纤仅在IO块时发生。使用非阻塞IO从磁盘读取是不可能的,这意味着可能阻止的唯一部分是写入context.response.output。但是,磁盘IO比同一台机器上的网络IO慢很多,这意味着写入永远不会阻塞,因为ab的读取速度比磁盘可以提供数据的速度快得多,即使是从磁盘缓存也是如此。这个例子实际上是打破水晶并发性的完美风暴。

在现实世界中,服务的客户端更可能从机器驻留在网络上,使得响应写入偶尔会被阻止。此外,如果您正在从另一个网络服务或管道/套接字读取,您也会阻止。另一种解决方案是使用线程池来实现非阻塞文件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