用 Python 來執行 Ruby 的我,

與撿到的 Puma 和 Rack

蔡孟穎 (Meng-Ying Tsai)

  • 又名文月、八盤
  • 現職為普通的後端工程師
  • ❤: 喝淺焙咖啡、唱日卡、嚐甜食

一年 365 天歡迎餵食,請多指教 ヽ(●´∀`●)ノ

動手寫個簡單的 CGI script

CGI 的問題與各種衍生後輩

回來看 Rack 是什麼,它怎麼運作的

server serve 動態頁面的老祖先 CGI

簡單的 Rack Server 示範

用 Python 來執行 Ruby 的我,

與撿到的 Puma 和 Rack

 用 Python Server 跑 CGI Script!

Python 在這裡其實不是很重要 :P

Rack 是什麼?Rack 如何連接 Rack App 和 Web Server

在談談怎麼用 python 跑 ruby 以前...

我們先來回到過去...我小時候

那個年代的網站?

www.lis.ntu.edu.tw 2001/2/5 (WayBackMachine)

  • gif 網頁素材
  • 江湖在走,跑馬燈要有
  • 靜態頁面、影像

想跟使用者互動?

CGI (Common Gateway Interface)

How to serve dynamic content? 

Browser

Web Server

Program

business logic

connect to DB

Browser

Web Server

CGI Program

query_string,

request_method...

Content-Type: text/plain

Hello World!

How CGI Works?

其實我說的 Python 跑 Ruby 

就是用 python 跑 web server,執行 ruby CGI script 啦(標題詐騙

BTW 先說要用 python 跑 ruby 其實可以直接這樣:

import os

os.system("ruby hello.rb")

python http.server 也是用類似這樣的方式

來執行 CGI script

def run_cgi(self):
  # ...
  if self.have_fork:
    # Unix -- fork as we should
    # ...
    if pid != 0:
      # Parent
      # ...
    # Child
    try:
      # ...
      os.execve(scriptfile, args, env)
  else:
    # Non-Unix -- use subprocess
    # ...

DEMO TIME owo/

從 DEMO 中可以發現

  • server 用 ENV 的方式把 query_string 等資訊傳給 CGI 程式
  • CGI 程式 STDOUT http response , server 回傳

Do not use CGI?!

1. 效能瓶頸

Request

Request

Request

Web server

CGI Shell

CGI Shell

CGI Shell

2. DOS

3.too low level

自己從頭寫整個 CGI Script

找有完整功能、架構的 web framework

4. 重造輪子

  • 改善效能
  • 加速開發時程

PHP-CGI

ASP

WSGI

JSP

FastCGI

ASP(Active Static Pages)

  • scripting on the server: <% ... %> 的內容會被 ASP 直譯器執行
  • 內建 Objects (Application, Session...)

FastCGI(Fast Common Gateway Interface

CGI 一個 process 處理一個 request 太慢,

FastCGI 有 pool 的機制,

一個 long-lived process serve 多個 request

WSGI(Python Web Server Gateway Interface) 

讓 server 跟 framework 的大家都符合一個規範

我以為可以拿來用的 server

我使用的 framework

WSGI(Python Web Server Gateway Interface) 

可以執行 WSGI app 的 server

一團符合 WSGI 的 python script

🤝

讓 server 跟 framework 的大家都符合一個規範

回到我撿到的 Puma & Rack

web server for Ruby & Rack

還有其他可以跑 Rack App 的 Server 只是先以他為例 

Ruby web frameworks

a ruby web server interface

Puma

Rails / Sinatra / ...

Rack?

回去看看 WSGI 的模式,我們可以理解成這樣...

可以執行 Rack app 的 server

一個 based on Rack

的 web app

🤝

但 Rack 是個 gem?

more than a interface, Rack 還提供了一些:

  • middleware eg. Rack::Config
  • helpers eg. Rack::Request

Browser

Puma

How Rack Works?

Rails / Sinatra/ ...

Puma

Rails / Sinatra/ ...

 App.call(env)

[200,

{'Content-Type' => 'text/plain'},

['hello world!']]

 

Rack

Rack

Rails / Sinatra/ ...

A Rack application is a Ruby object (not a class) that responds to call. It takes exactly one argument, the environment and returns an Array of exactly three values: The status, the headers, and the body.

 

# lib/sinatra/base.rb:928
def call(env)
  dup.call!(env)
end

def call!(env)
  # ...
  @request  = Request.new(env)  # class Request < Rack::Request
  @response = Response.new
  
  # ...

  @response.finish
end

# lib/sinatra/base.rb:172
def finish
  # ...    
  [status, headers, result]
end

必須要有 call 方法

固定回傳格式

以 Sinatra 為例

The environment must be an unfrozen instance of Hash that includes CGI-like headers. The application is free to modify the environment. The environment is required to include these variables (adopted from PEP333), except when they'd be empty, but see below.

# in an Rack App
request = Rack::Request.new(env)
if request.request_method == 'GET'
  # ...
end
# rack /lib/rack/request.rb:57
def get_header(name)
  @env[name]
end
    
# rack lib/rack/request.rb:151
def request_method;  get_header(REQUEST_METHOD)  end

可以使用 Rack gem 提供了 Rack::Request: 

Rack

Puma

# in a config.ru

run MyApp
$ rackup

在 Rack App 專案的 config.ru:

可以用 rackup 把 app 跑起來

Handlers usually are activated by calling MyHandler.run(myapp)

A second optional hash can be passed to include server-specific configuration.

lib/rack/handler.rb:9

要把 Puma 跑起來:

  • Puma CLI
$ puma
  • rackup 時會用 Rack::Handler::Puma 跑起來
# /lib/puma/rack_default.rb

module Rack::Handler
  def self.default(options = {})
    Rack::Handler::Puma
  end
end

puma 在哪裡 app.call(@env)?

CLI

Handler

OR

Launcher

Single < Runner

Binder

  • entrypoint
  • config

CLI

Handler

OR

Launcher

Cluster < Runner

Worker

Worker

Worker

Binder

  • entrypoint
  • config

Server

Thread_pool

Server

Server

loop ,放 client 到 thread pool 

handle_request

@app.call(@env)

process_client

 Puma 會呼叫 app 的 call method

# lib/puma/request.rb:76

status, headers, res_body = @thread_pool.with_force_shutdown do
  @app.call(env)
end

簡單的 Rack Server DEMO!

首先會需要一個 Rack App

class MyApp
  def self.call(env)
    [200, { 'Content-Type' => 'text/plain' }, 'hello']
  end
end

還會需要一個 Handler

class SimpleServerHandler
  def self.run(app, **options)
    # 先不處理 environment, pid, AccessLog, config, etc.
    @server = SimpleServer.new(app, server_name: options[:Host], port: options[:Port])
    @server.start
  end
end

module Rack::Handler
  def self.default(options = {})
    SimpleServerHandler
  end
end
  • 開 TCPServer 偵聽
  • 拿取資料,處理 Http Request
  • 處理 Rack App 需要的 env
  • app.call(@env) 拿回 [status, headers, body]
  • 處理 Http Response,傳送資料
  • 關閉連線

總結

CGI

非常簡單的 interface

Rack

效能問題

不要重複造輪子

 

...

Thanks for listening!