使用 Ruby EventMachine 構建高併發非阻塞 HTTP 文件伺服器,利用 em-http-server 與 EventMachine::FileStreamer 實現大檔案異步流式傳輸,避免記憶體溢出,支援 HTTP 範圍請求(斷點續傳)。
# Gemfile
gem 'eventmachine', '~> 1.2'
gem 'em-http-server', '~> 0.2'
require 'rubygems'
require 'eventmachine'
require 'em-http-server'
class FileDownloadHandler < EM::HttpServer::Server
attr_accessor :file_path, :file_name, :file_size
def process_http_request
# 僅處理 GET 請求且路徑匹配目標檔案
if @http_request_method =~ /GET/i && @http_request_uri.end_with?("/" + file_name)
serve_file
else
render_usage_page
end
end
private
def serve_file
# 發送 HTTP 標頭
send_data "HTTP/1.1 200 OK\r\n"
send_data "Server: Ruby EventMachine FileServer\r\n"
send_data "Connection: keep-alive\r\n"
send_data "Keep-Alive: timeout=15\r\n"
send_data "Content-Type: application/octet-stream\r\n"
send_data "Content-Disposition: attachment; filename=\"#{file_name}\"\r\n"
send_data "Content-Length: #{file_size}\r\n"
send_data "Accept-Ranges: bytes\r\n" # 支援斷點續傳關鍵標頭
send_data "\r\n"
# 異步流式傳輸檔案內容(核心優勢:不載入整個檔案到記憶體)
streamer = EventMachine::FileStreamer.new(self, file_path)
streamer.callback {
# 傳輸成功完成
close_connection_after_writing
}
streamer.errback { |err|
# 傳輸錯誤處理
puts "檔案傳輸錯誤: #{err}"
close_connection
}
end
def render_usage_page
response = EM::DelegatedHttpResponse.new(self)
response.status = 200
response.content_type 'text/html; charset=utf-8'
response.content = "<h1>檔案下載伺服器</h1><p>用法: wget https://host:port/#{file_name}</p>"
response.send_response
end
end
# 啟動伺服器
EM.run do
file_path = '/tmp/large-file.tar.gz' # 替換為實際大檔案路徑
file_name = File.basename(file_path)
file_size = File.size(file_path)
puts "啟動檔案伺服器於 0.0.0.0:8080"
puts "下載連結: https://localhost:8080/#{file_name} (#{file_size} bytes)"
EM.start_server("0.0.0.0", 8080, FileDownloadHandler) do |conn|
conn.file_path = file_path
conn.file_name = file_name
conn.file_size = file_size
end
end
上述基礎版本未完整實作 Range 處理,生產級需補充:
def serve_file
# 解析 Range 標頭
range_header = @http_request_headers['range']
start_pos = 0
end_pos = file_size - 1
status_code = 200
if range_header && range_header =~ /bytes=(\d+)-(\d*)/
start_pos = $1.to_i
end_pos = $2.empty? ? file_size - 1 : $2.to_i
status_code = 206 # Partial Content
end
content_length = end_pos - start_pos + 1
send_data "HTTP/1.1 #{status_code} #{status_code == 206 ? 'Partial Content' : 'OK'}\r\n"
send_data "Content-Range: bytes #{start_pos}-#{end_pos}/#{file_size}\r\n" if status_code == 206
send_data "Content-Length: #{content_length}\r\n"
send_data "Accept-Ranges: bytes\r\n"
# ... 其他標頭
# 使用自定義 Streamer 支援 offset/limit(或改用 EM::FileStreamer 配合 seek)
# 此處略過完整實作,建議參考 em-http-server 原始碼擴展
end
EM.set_max_timers、EM.set_max_epoll_descriptors 調整系統限制。em-synchrony 或 puma 叢集模式利用多核心。EM.set_send_buffer_size、EM.set_receive_buffer_size。# 1. 建立測試大檔案 (1GB)
dd if=/dev/zero of=/tmp/large-file.tar.gz bs=1M count=1024
# 2. 啟動伺服器
ruby file_server.rb
# 3. 測試下載
wget https://localhost:8080/large-file.tar.gz
# 4. 測試斷點續傳
wget -c https://localhost:8080/large-file.tar.gz
# 5. 壓力測試
ab -n 1000 -c 50 https://localhost:8080/large-file.tar.gz
若團隊主力為 Java/Scala/Kotlin,建議考慮以下替代方案:
| 框架 | 特點 |
|---|---|
| Netty | 高效能 NIO,手動控制細節,學習曲線陡峭 |
| Vert.x | 多語言、事件驅動、內建檔案非同步傳輸 |
| Spring WebFlux | Reactive Streams 標準,整合 Spring 生態 |
| Ratpack | 輕量、Groovy DSL 友善、非阻塞 |