Rack trong Rails là gì?

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