Rack使用与实现

304 阅读5分钟

一、前言

1. 什么是Rack
Rack是ruby应用服务器和Rack应用程序之间的接口

2. Rack的特点

a.提供一个广泛支持的标准接口
    Rack提供了一个标准接口,便于应用程序和应用服务器进行交互,一个Rack应用可以被任何和Rack兼容的应用服务器调用,最明显的一个例子,我们开发的rails应用或者Sinatra应用可以直接使用Webrick或者thin等服务器启动,不用做什么修改,非常方便。
b.模块化、高可重用性
    Rack利用中间件实现最大程度模块化,从而提高Web应用程序部件的可重用性,从而提高开发效率
    Rack中间件对Ruby Web框架也有很深远的影响:
    -   不同Web框架之间可重用Rack中间件,这意味着你编写的中间件可以在所有主流框架中 使用;
    -   可以通过不同中间件的组合组装出同一Web框架的不同变种以适应不同的需求;
    -   可以组合不同Web框架为同一个更大的系统服务。
c.简单
    Rack标准简单,这就很容易让用户实现一个Web服务器或者Web框架

Ruby 生态中的web server都有哪些?
Puma、Unicorn

Ruby 生态中的web 应用程序框架 都有哪些?
Ruby on Rails、 Sinatra image.png

3. 这么多web server & web 应用程序,Rack是如何实现兼容并简单切换的呢?

  • Rack 在 webserver 和web 应用程序 之间提供了一套最小的 API 接口。
  • 如果 webserver 和 web 应用程序 都遵循 Rack 提供的这套规则,那么他们就可以随意的搭配使用。
  • 那咱们今天的重点就是搞清楚rack 如何把web server 和 web 应用程序关联起来的。

二、初识Rack

1、协议 在 Rack 的协议中,将 Rack Server 描述成

  • 需要实现 run 方法,当前方法接受app、options、block 参数
  • 需要使用 register 方法进行服务器注册,方便 rack 找到 Rack Server。

在 Rack 的协议中,将 Rack 应用描述成

  • 一个可以响应 call 方法的 Ruby 对象
  • 这个方法接受来自外界的一个参数,也就是环境(env)
  • 返回一个只包含三个值的数组,按照顺序分别是状态码、Headers 以及响应的正文。

2、简单demo

# config.ru
require 'rack'
class MyApp 
  def call(env)
    [200, { 'content-type'=> 'text/plain'}, ['Hello World!']]
  end
end
# my_app = MyApp.new
run MyApp.new

上面这段代码就是我们实现的一个最简单的Rack应用,我们实现了一个类MyApp,他只有一 个call方法,此方法接受一个参数,返回一个数组,此数据包含status、header、body,在这 里呢,我使用WEBrick来启动这个应用,同样我也可以使用thin来启动,那么最后一句话就变成:

# 运行启动命令
rackup
# rackup -p 3000

# 进行请求
curl http://127.0.0.1:3000

# 结果输出
Hello World!

image.png

三、Rack实现原理

1、rackup做了什么?
通过运行rackup 命令启动服务,其实和运行rails s一样,都是gem提供的命令。

which rackup
... 
version = ">= 0.a"
p "path: #{Gem.activate_bin_path("rack", "rackup", version)}"
load Gem.activate_bin_path('rack', 'rackup', version)

代码简述

  • Gem.activate_bin_path 返回 rack 这个gem 中 rackup 这个可执行文件的路径。
  • load 是kernel的方法,加载可执行文件,并且执行其中的代码。

gem 中rackup 文件的内容

#!/usr/bin/env ruby
# frozen_string_literal: true

require "rack"
Rack::Server.start

最终执行了 Rack::Server.start

2、Rack::Server.start的执行

image.png

  • 找到Rack::Server,比如可能是puma
  • 封装rack 应用,也就是把rack 应用 和 多个middleware 封装在一起。
  • 这样rack 就把Rack::Server 和 rack 应用关联起来啦。

3、核心源码
3.1 以下是 Rack 核心源码的简化示例,展示了 Rack 模块、Request 类和 Response 类的基本实现:

# rack.rb
module Rack
  VERSION = "2.3.0"

  class Request
    attr_reader :env

    def initialize(env)
      @env = env
    end

    def method
      @env['REQUEST_METHOD']
    end

    def path_info
      @env['PATH_INFO']
    end

    # 其他方法...
  end

  class Response
    attr_accessor :status, :headers, :body

    def initialize
      @status = 200
      @headers = {}
      @body = []
    end

    def write(content)
      @body << content
    end

    def finish
      [@status, @headers, @body]
    end
  end

  def self.call(env)
    request = Request.new(env)
    response = Response.new

    # 处理请求逻辑...

    response.finish
  end
end

在 rack.rb 文件中,定义了 Rack 模块和一些全局常量,以及 Rack::Request 类和 Rack::Response 类。

Rack::Request 类封装了 HTTP 请求的相关信息,例如请求方法和路径等。它提供了一些方法来获取请求的各种属性。

Rack::Response 类封装了 HTTP 响应的相关信息,包括状态码、头部和正文内容。它提供了一些方法来设置响应的各种属性。

Rack 模块中的 self.call(env) 方法是 Rack 应用程序的入口点。当收到 HTTP 请求时,Rack 会调用该方法来处理请求。在示例中,创建了一个 Request 对象和一个 Response 对象,并在处理请求逻辑后返回最终的响应数据。

请注意,上述示例是简化的,并未包含 Rack 的完整实现。实际的 Rack 源码包含了更多的类、模块和方法,用于处理中间件、路由、异常处理等功能。

3.2 以下是 Rack 源码中用于遍历多个服务器和应用程序的核心部分:

# rack/builder.rb

module Rack
  class Builder
    def initialize(default_app = nil, &block)
      @use = []
      @run = default_app

      instance_eval(&block) if block_given?
    end

    def use(middleware, *args, &block)
      @use << lambda { |app| middleware.new(app, *args, &block) }
    end

    def run(app)
      @run = app
    end

    def to_app
      app = @run

      @use.reverse_each do |middleware|
        app = middleware.call(app)
      end

      app
    end
  end
end

上述代码展示了 Rack::Builder 类的关键部分。Rack::Builder 类用于构建 Rack 应用程序,它允许用户按顺序添加中间件,并指定最终的应用程序。

在 initialize 方法中,可以传入一个默认的应用程序(default_app)和一个块(block)。块中可以使用 use 方法来添加中间件,使用 run 方法来指定最终的应用程序。

use 方法接受一个中间件类(middleware)和可选的参数和块。它将中间件封装为一个 lambda,并将其添加到 @use 数组中。

run 方法用于指定最终的应用程序。

to_app 方法用于生成最终的 Rack 应用程序。它首先将 @run 变量赋值给 app,然后使用 reverse_each 方法遍历 @use 数组中的中间件。对于每个中间件,它将应用该中间件到 app 上,得到新的 app。最终,返回经过所有中间件处理后的应用程序。