用 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!
用 Python 來執行 Ruby 的我,與撿到的 Puma 和 Rack
By Meng-Ying Tsai
用 Python 來執行 Ruby 的我,與撿到的 Puma 和 Rack
- 722