Rack trong Rails là gì?
Bài đăng này đã không được cập nhật trong 3 năm
I. Lời nói đầu:
Trong hệ thống Ruby web, Rack là một thành phần không thể thiếu.
Khi code Rails chắc hẳn bạn đã từng nghe tới Rack - aka Web server interface.
Tò mò search thử trên trang chủ của nó thì đập thẳng vào mặt cái mô tả:
Rack provides a minimal interface between webservers that support Ruby and Ruby frameworks.
To use Rack, provide an "app": an object that responds to the call method, taking the environment hash as a parameter, and returning an Array with three elements: The HTTP response code, a hash of headers, the response body, which must respond to each.
WTH, nghe vi diệu quá
Hy vọng bài viết dưới đây sẽ giúp các bạn hiểu Rack là gì? Tại sao ta lại cần tới nó?
GLHF
II.Rack
Trước khi đi vào phần chính, ta sẽ nói qua 1 chút về flow của 1 request client đi tới web application ra sao nếu không có Rack.
1. App servers
Khi bạn bắt đầu code Rails, app server của nó mặc định sẽ là WEBrick. (Đối với Rails >= 5 thì là Puma)
Công việc chính của một App server như WEBrick là một phiên dịch viên giữa client requests và web application.
Cụ thể là WEBrick sẽ chờ những HTTP requests từ client gửi tới, xử lý và ném về cho web application.
Sau khi chuyển request xong, WEBrick lại ngồi chờ cho đến khi web app trả lại response, xử lý và bắn về cho client.
2. HTTP request
Khi client tạo request gọi tới server, cái HTTP requests đấy cũng chỉ là 1 đoạn text loằng ngoằng:
Plain text in, plain text out.
Như đã nói ở trên, app server sẽ tiếp nhận và xử lý cái đống HTTP requests dạng plain text kia, trước khi chuyển tới web application.
Dưới đây là 1 đoạn code cụ thể của WEBrick
def parse_header(raw)
header = Hash.new([].freeze)
field = nil
raw.each_line{|line|
case line
when /^([A-Za-z0-9!\#$%&'*+\-.^_`|~]+):\s*(.*?)\s*\z/om
field, value = $1, $2
field.downcase!
header[field] = [] unless header.has_key?(field)
header[field] << value
when /^\s+(.*?)\s*\z/om
value = $1
unless field
raise HTTPStatus::BadRequest, "bad header '#{line}'."
end
header[field][-1] << " " << value
else
raise HTTPStatus::BadRequest, "bad header '#{line}'."
end
}
header.each{|key, values|
values.each{|value|
value.strip!
value.gsub!(/\s+/, " ")
}
}
header
end
Chạy thử trên console ta sẽ nhận được:
Bằng việc sử dụng nhiều function kiểu dạng như trên, WEBrick có thể chuyển tới web application của ta 1 đoạn Hash
sạch đẹp từ HTTP requests.
Để web app đọc được request, chúng ta đơn giản chỉ cần gọi kiểu như:
content_type = request["content-type"]
Thay vì 1 đoạn code lằng nhằng:
_header, content_type = /^([A-Za-z0-9!\#$%&'*+\-.^_`|~]+):\s*(.*?)\s*\z/om
.match("content-type: application/json")
.captures
Plain text in. Hash out.
3. HTTP response
Bây giờ hãy chuyển trọng tâm tới những gì web app sẽ xử lý với request này.
Hãy tưởng tượng web app được implement method call
class App
def call(request)
end
end
Mỗi lần WEBrick nhận requests, nó sẽ parse thành 1 Hash và chuyển tới method call
như một arguments:
app = App.new
# incoming request
# request = { REQUEST_METHOD: "GET", PATH_INFO: "/features" }
app.call(request)
Giá trị mà method call
sẽ trả về, sẽ được chuyển ngược lại cho web server WEBrick. Từ dữ liệu response đó, WEBrick dịch nó thành plain text và trả về cho client. Giống như web server đã parse request thành một Ruby Hash, chúng ta cần định nghĩa format response của method call
.
Response trả về bao gồm 3 phần chính:
- Status
- Header
- Body
WEBrick cần nhận được cả 3 để xây dựng một response hợp lệ.
class App
def call(request)
"status: 200\n content_type: 'application/json'\n body: ''"
end
end
Trả về String
nhìn hơi sida, thử với Hash
thì sao:
class App
def call(request)
{ status: 200, headers: { content_type: "application/json" }, body: "" }
end
end
Ngon rồi.
Coi như thỏa thuận giữa web application và web server đã xong. Nếu cung cấp một Hash
với các keys chính xác, WEBrick sẽ đảm bảo nó sẽ parse đúng thành một HTTP response hợp lệ.
Hash in. Plain text out.
Mọi chuyện diễn ra ngon lành, nhưng bất ngờ ta nhận ra sự hạn chế về performance của WEBrick - chỉ dùng single thread, chúng ta quyết định chuyển sang một web server rất phổ biến khác - Unicorn. Lúc này sẽ phát sinh ra vấn đề.
Unicorn yêu cầu method call
trả về một Array thay vì cái Hash
ở trên, nếu không, web servers sẽ không parse được để trả về cho Client. Phải viết lại code rồi...
class App
def call(request)
[200, { content_type: "application/json" }, ""]
end
end
Không tới 1 tuần sau, chúng ta gặp vấn đề về memory khi dùng Unicorn. Chúng ta chuyển sang dùng web server Puma.
Puma cũng expect trả về Array giống như Unicorn. Tuy nhiên, thay vì method call
, Puma expects app trả về method với tên khác - run
. Vậy ta lại phải viết code để sửa...
class App
def run(request)
[200, { content_type: "application/json" }, ""]
end
end
4. Rack
Vấn đề ở đây quá rõ ràng. Với mỗi ruby web server chỉ làm việc được với một ruby web app là không ổn chút nào - và đây chính xác là những gì Rack sinh ra để giải quyết.
Vậy phần mô tả về Rack giờ dễ hiểu rồi nhỉ :v
Provide an “app”: an object that responds to the call method, taking the environment hash as a parameter, and returning an Array with three elements: the HTTP response code, a Hash of headers, and the response body, which must respond to each
require 'rack'
class App
def call(request)
[200, { content_type: "application/json", body: "" }, [""]]
end
end
Rack::Handler::WEBrick.run App.new
Chạy thử server:
Chỉ đơn giản vậy thôi. Với vài dòng code Ruby đó, bạn đã trả lời được câu hỏi: Làm thế nào để những web application như Rails, Sinatra,... đều có thể hoạt động được với những web server khác nhau. Test thử khi ta swap qua dùng web server Puma.
require 'rack'
require 'rack/handler/puma'
class App
def call(request)
[200, { content_type: "application/json", body: "" }, [""]]
end
end
Rack::Handler::Puma.run App.new
Chạy server:
Flow của request nếu có thêm Rack giờ sẽ như sau:
Nguồn:
All rights reserved