-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathruby-rack.html
More file actions
501 lines (378 loc) · 22 KB
/
ruby-rack.html
File metadata and controls
501 lines (378 loc) · 22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
<!DOCTYPE html>
<html>
<head>
<!-- Document Settings -->
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!-- Page Meta -->
<title>Ruby Middleware</title>
<meta name="description" content="学习的一些记录" />
<!-- Mobile Meta -->
<meta name="HandheldFriendly" content="True" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Brand icon -->
<link rel="shortcut icon" href="/assets/images/favicon.ico" >
<!-- Styles'n'Scripts -->
<link rel="stylesheet" type="text/css" href="/assets/css/screen.css" />
<link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Merriweather:300,700,700italic,300italic|Open+Sans:700,400" />
<link rel="stylesheet" type="text/css" href="/assets/css/syntax.css" />
<!-- highlight.js -->
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.3.0/styles/default.min.css">
<style>.hljs { background: none; }</style>
<!-- Ghost outputs important style and meta data with this tag -->
<link rel="canonical" href="http://localhost:4000//ruby-rack" />
<meta name="referrer" content="origin" />
<link rel="next" href="/page2/" />
<meta property="og:site_name" content="Thinking" />
<meta property="og:type" content="website" />
<meta property="og:title" content="Ruby Middleware" />
<meta property="og:description" content="学习的一些记录" />
<meta property="og:url" content="http://localhost:4000//ruby-rack" />
<meta property="og:image" content="/assets/images/cover3.jpg" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Ruby Middleware" />
<meta name="twitter:description" content="学习的一些记录" />
<meta name="twitter:url" content="http://localhost:4000//ruby-rack" />
<meta name="twitter:image:src" content="/assets/images/cover3.jpg" />
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "Website",
"publisher": "Thinking",
"name": "Ruby Middleware",
"url": "http://localhost:4000//ruby-rack",
"image": "/assets/images/cover3.jpg",
"description": "学习的一些记录"
}
</script>
<meta name="generator" content="Jekyll 3.0.0" />
<link rel="alternate" type="application/rss+xml" title="Thinking" href="/feed.xml" />
</head>
<body class="home-template nav-closed">
<!-- The blog navigation links -->
<div class="nav">
<h3 class="nav-title">Menu</h3>
<a href="#" class="nav-close">
<span class="hidden">Close</span>
</a>
<ul>
<li class="nav-home " role="presentation"><a href="/">Home</a></li>
<li class="nav-about " role="presentation"><a href="/about">About</a></li>
<li class="nav-fables " role="presentation"><a href="/tag/fables">Fables</a></li>
<li class="nav-speeches " role="presentation"><a href="/tag/speeches">Speeches</a></li>
<li class="nav-fiction " role="presentation"><a href="/tag/fiction">Fiction</a></li>
<li class="nav-author " role="presentation"><a href="/author/casper">Casper</a></li>
<li class="nav-author " role="presentation"><a href="/author/edgar">Edgar</a></li>
<li class="nav-author " role="presentation"><a href="/author/abraham">Abraham</a></li>
<li class="nav-author " role="presentation"><a href="/author/martin">Martin</a></li>
<li class="nav-author " role="presentation"><a href="/author/lewis">Lewis</a></li>
</ul>
<a class="subscribe-button icon-feed" href="/feed.xml">Subscribe</a>
</div>
<span class="nav-cover"></span>
<div class="site-wrapper">
<!-- All the main content gets inserted here, index.hbs, post.hbs, etc -->
<!-- default -->
<!-- The comment above "< default" means - insert everything in this file into -->
<!-- the [body] of the default.hbs template, which contains our header/footer. -->
<!-- Everything inside the #post tags pulls data from the post -->
<!-- #post -->
<header class="main-header post-head " style="background-image: url(/assets/images/cover3.jpg) ">
<nav class="main-nav overlay clearfix">
<a class="blog-logo" href="/"><img src="/assets/images/ghost.png" alt="Blog Logo" /></a>
<a class="menu-button icon-menu" href="#"><span class="word">Menu</span></a>
</nav>
</header>
<main class="content" role="main">
<article class="post tag-test tag-content">
<header class="post-header">
<h1 class="post-title">Ruby Middleware</h1>
<section class="post-meta">
<!-- <a href='/'></a> -->
<time class="post-date" datetime="2019-10-11">11 Oct 2019</time>
<!-- [[tags prefix=" on "]] -->
on
<a href='/tag/ruby'>Ruby</a>,
<a href='/tag/puma'>Puma</a>
</section>
</header>
<section class="post-content">
<p><a href="https://rack.github.io/">Rack官网</a>对于Rack的介绍比较简单,只是介绍了Rack的作用和基本的使用。虽然我们不用了解middleware的调用原理也可以开发出能使用的middleware,但是总有点不知所以然的感觉,所以抽空总结了下Rack中middleware的调用原理。</p>
<h3 id="装饰者模式">装饰者模式</h3>
<ul>
<li>
<p>首先理解下装饰者模式,装饰者模式中有装饰者和被装饰者,就像套娃,在外面套上一层,外面那层就是装饰者,里面那层就是被装饰者。可能这个比喻有点不太合理,因为装饰者模式其实就是在之前的功能上添加装饰多点功能上去,而套娃是全部盖住了。但是按照代码的表面上看调用方式确实是盖住了,所以暂时用这个比喻了。这两个概念要区分好,要不容易绕晕。在装饰者初始化时,被装饰者一般作为参数传递给装饰者,作为装饰者的成员。装饰者和被装饰者一般会有相同的行为,在装饰者的行为发生时会通过他的成员去调用被装饰者的行为,从而达到被装饰的目的。其实装饰者模式就是利用类有相似的行为这种方式,用装饰者去替代一下被装饰者,但是又不影响被装饰者的行为调用,同时在装饰者的行为发生时加些额外的功能。可以当作是被装饰者在外面加上了一层外壳,然后外壳发生变化的时候,会顺带着调用被装饰者的行为,Rack就是用到了这种模式。开始作为run方法调用的middleware就是最初的被装饰者,装饰者也有可能会成为被装饰者。而use方法调用的middleware就是接下来的装饰者了(也有可能作为被装饰者用)。对于上面要有相同的行为这点,其实感觉也不是很必要,统一定义成那个行为只是为了方便定义统一的接口模式,方便开发。理解这种模式对下面的调用思路比较有帮助。</p>
</li>
<li>
<p>实现类似代码如下:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>class A
def call
puts "be decorator"
end
end
class B
attr_reader :a
def initialize
@a = A.new
end
def call
puts "do something before"
@a.call
puts "do something after"
end
end
B.new.call
</code></pre></div> </div>
<p>A为被装饰者,B为装饰着,在<code class="highlighter-rouge">@a.call</code>之前和之后的部分为装饰的内容。</p>
</li>
</ul>
<h3 id="rack-middleware的使用">Rack middleware的使用</h3>
<ol>
<li>配置config.ru文件,定义好要用到的middleware和要run的middleware。下面是一个简单的调用。
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code> # ./config.ru
app = Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['get rack\'d']] }
run app
</code></pre></div> </div>
<p>接着执行<code class="highlighter-rouge">rackup</code>命令应用就可以跑起来了。</p>
</li>
<li>
<p>同时还可以把middleware定义成一个类,但是要在初始化实例的时候初始化@app和定义一个call方法,并且在call方法中需要调用<code class="highlighter-rouge">@app.call(env)</code>,如下:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code> # rack_demo.rb
require 'rack'
class Timing
def initialize(app)
@app = app
end
def call(env)
ts = Time.now
status, headers, body = @app.call(env)
elapsed_time = Time.now - ts
puts "Timing: #{env['REQUEST_METHOD']} #{env['REQUEST_URI']} #{elapsed_time.round(3)}"
return [status, headers, body]
end
end
app = proc do |env|
['200', {'Content-Type' => 'text/html'}, ['Hello, Rack!']]
end
Rack::Handler::WEBrick.run(Timing.new(app), :Port => 9292, :Host => '0.0.0.0')
</code></pre></div> </div>
<p>执行<code class="highlighter-rouge">ruby rack_demo.rb</code>就可以跑起一个服务了。</p>
</li>
</ol>
<p>middleware的具体使用和返回格式要求这里不详细介绍,可以参考<a href="https://ruby-china.org/topics/31592">Ruby Rack 及其应用</a>。同时上面的两种使用方式的作用原理是一样的。下面再详细分析。</p>
<h3 id="rack-middleware实现的原理">Rack Middleware实现的原理</h3>
<p>定义好config.ru配置文件后在当前目录执行<code class="highlighter-rouge">rackup</code>命令,会去到ruby对应的bin目录执行文件。一般在ruby安装好后都会有这个可执行文件的,在我本地的位置是 <code class="highlighter-rouge">~/.rvm/gems/ruby-2.5.3/bin/rackup</code></p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code># ~/.rvm/gems/ruby-2.5.3/bin/rackup
require 'rubygems'
version = ">= 0.a"
...
if Gem.respond_to?(:activate_bin_path)
load Gem.activate_bin_path('rack', 'rackup', version)
else
gem "rack", version
load Gem.bin_path("rack", "rackup", version)
end
</code></pre></div></div>
<p>上面源码会执行后面的rack gem下面的rackup二进制文件,其中的源码为:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env ruby</span>
<span class="c1"># ~/.rvm/gems/ruby-2.5.3/gems/rack-2.0.6/bin/rackup</span>
<span class="nb">require</span> <span class="s2">"rack"</span>
<span class="no">Rack</span><span class="o">::</span><span class="no">Server</span><span class="p">.</span><span class="nf">start</span>
</code></pre></div></div>
<p>主要是为了启动Rack Server,那启动的过程又做了什么东西呢?
调用栈从 <code class="highlighter-rouge">def self.start</code> => <code class="highlighter-rouge">initialize</code> => <code class="highlighter-rouge">parse_options</code> 这一系列的调用只是为了初始化一个Server,然后加上一些默认的Options配置,初始化后主要是加了如下的默认配置:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
:environment => "development",
:pid => nil,
:Port => 9292,
:Host => "localhost",
:AccessLog => [],
:config => "config.ru"
}
</code></pre></div></div>
<p>其中config中的值config.ru就是默认的配置文件,然后就是实例执行run方法了。run方法中调用了wrapped_app方法,这个方法主要是把那些我们自己定义的middleware和Rack自己提供的middleware合并成一个app对象。沿着方法调用栈继续查看,其中<code class="highlighter-rouge">build_app</code>方法比较重要。源码如下:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def build_app(app)
middleware[options[:environment]].reverse_each do |middleware|
middleware = middleware.call(self) if middleware.respond_to?(:call)
next unless middleware
klass, *args = middleware
app = klass.new(app, *args) # 这一步把参数app当作被装饰者了
end
app
end
</code></pre></div></div>
<p><code class="highlighter-rouge">middleware[options[:environment]]</code>求得的值是Rack中默认的middleware。遍历的块中每个middleware都会去创建一个实例,以app变量作为参数传入,这就是为什么每个middleware在initialize的时候都需要传入一个app变量,并初始化赋值给@app实例变量的原因。初始化的过程也就是被装饰者要被装饰的过程。上面的迭代遍历过程最后方法返回的app会变成如下的链式反应:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#<Rack::ContentLength:0x00007ff72d568200
@app=
#<Rack::Chunked:0x00007ff72d568ef8
@app=
#<Rack::CommonLogger:0x00007ff72d569ce0
@app=
#<Rack::ShowExceptions:0x00007ff72e940c28
@app=
#<Rack::Lint:0x00007ff72e941ee8
@app=
#<Rack::TempfileReaper:0x00007ff72e943018
@app=
#<StatusLogger:0x00007ff72d4a0570
@app=
#<StatusLoggear:0x00007ff72d4a1088
@app=
#<Proc:0x00007ff72d4a2820@/Users/Cain/code/ruby/rack/config.ru:30>>>>
</code></pre></div></div>
<p>这样通过调用<code class="highlighter-rouge">app.call</code>可以调用到所有middleware的call方法。
举个例子:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#config.ru
class FirstMidd
def initialize(app)
@app = app
end
def call(env)
puts "1"
status, head, body = @app.call(env)
puts "7"
[status, head, body]
end
end
class SecondMidd
def initialize(app)
@app = app
end
def call(env)
puts "2"
status, head, body = @app.call(env)
puts "6"
[status, head, body]
end
end
class ThirdMidd
def initialize(app)
@app = app
end
def call(env)
puts "3"
status, head, body = @app.call(env)
puts "5"
[status, head, body]
end
end
class Top
def call(env)
puts "4"
[200, {'Content-Type' => 'text/plain'}, ["hello, this is a test."]]
end
end
use FirstMidd
use SecondMidd
use ThirdMidd
run Top.new
</code></pre></div></div>
<p>发送一个请求<code class="highlighter-rouge">curl http://localhost:9292</code>后,服务器会会按照 <code class="highlighter-rouge">=> 1, 2, 3, 4, 5, 6, 7</code> 的顺序输出。Top作为最开始的被装饰者app,会先用ThirdMidd去装饰,返回app1,然后app1又作为被装饰者被SecondMidd装饰,直到FirstMidd,返回app3对象,这时候调用app2.call的时候,就会先调用FirstMidd的call方法,然后调用app3的call方法,然后调用直到app的call方法。
详细的调用过程如下:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code> # ~/.rvm/gems/ruby-2.5.3/gems/rack-2.0.6/lib/rack/server.rb
def build_app_and_options_from_config
...
app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
@options.merge!(options) { |key, old, new| old }
app
end
# ~/.rvm/gems/ruby-2.5.3/gems/rack-2.0.6/lib/rack/builder.rb
def self.parse_file(config, opts = Server::Options.new)
options = {}
if config =~ /\.ru$/
...
app = new_from_string cfgfile, config
else
...
end
return app, options
end
def self.new_from_string(builder_script, file="(rackup)")
eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
TOPLEVEL_BINDING, file, 0
end
</code></pre></div></div>
<p>最后会调用new_from_string这个方法。这个方法就是通过eval执行之前config.ru中的内容。其中<code class="highlighter-rouge">Builder#to_app</code>方法执行 <code class="highlighter-rouge">app = @use.reverse.inject(app) { |a,e| e[a] }</code> 把 <code class="highlighter-rouge">config.ru</code> 中run方法和use方法调用的那些middleware实例逐个迭代作为参数嵌入到app变量中,app最终的效果如上面<code class="highlighter-rouge">build_app</code>方法中最后的app变量的形式一样。最终这个app值作为build_app方法的实参调用,最终合并成最终链条。</p>
<p>rack对middleware的处理使用了装饰者模式,不需要包含被装饰者的类就是初始的被装饰者,如上面例子的Top类和赋值proc给app变量的都是初始的被装饰者。逐级装饰嵌套后,最后会调用最外层的装饰者,调用call方法,这时会发生一系列的连锁反应,一直调用其它装饰者(也是被装饰者)的call方法,直到最初的那个被装饰者。这时候就会有内容返回了。</p>
<h3 id="rack和应用服务器的对接">Rack和应用服务器的对接</h3>
<p>上面的过程只是启动一个服务的时候准备的一些middlewa的过程。那应用服务器和middleware的连接桥梁是怎么实现的呢?其实Rack只是按照一定的规则去找出应用服务器,然后通过执行其中定义好的<code class="highlighter-rouge">run</code>方法,把上面链接好的迭代app传入服务器中去执行,从而达到中间桥梁的作用,主要的代码在<code class="highlighter-rouge">Rack::Server#start</code>的时候run了应用服务器。如下:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code># ~/.rvm/gems/ruby-2.5.3/gems/rack-2.0.6/lib/rack/server.rb
def start
...
server.run wrapped_app, options, &blk
end
</code></pre></div></div>
<p>其中server的调用过程如下:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code># ~/.rvm/gems/ruby-2.5.3/gems/rack-2.0.6/lib/rack/server.rb
def server
@_server ||= Rack::Handler.get(options[:server])
unless @_server
@_server = Rack::Handler.default
# We already speak FastCGI
@ignore_options = [:File, :Port] if @_server.to_s == 'Rack::Handler::FastCGI'
end
@_server
end
</code></pre></div></div>
<p>如果没有配置对应的:server选项,调用<code class="highlighter-rouge">Rack::Handler.get</code>会返回nil,然后调用<code class="highlighter-rouge">Rack::Handler.default</code>去查找对应的server。会通过<code class="highlighter-rouge">pick ['puma', 'thin', 'webrick']</code>按照默认服务器名字的顺序去查找对应的服务器handler。
如果require了puma,而puma因为定义了一个覆盖Handler中default的方法,如下:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code># /Users/Cain/.rvm/gems/ruby-2.5.3/gems/puma-3.12.0/lib/puma/rack_default.rb
require 'rack/handler/puma'
module Rack::Handler
def self.default(options = {})
Rack::Handler::Puma
end
end
</code></pre></div></div>
<p>这时 <code class="highlighter-rouge">Rack::Handler.default</code> 返回的就是 <code class="highlighter-rouge">Rack::Handler::Puma</code> 这个类。这时调用<code class="highlighter-rouge">server.run wrapped_app, options, &blk</code>就相当于调用了<code class="highlighter-rouge">Rack::Handler::Puma.run</code>,从而在puma中接到可以处理的请求后再执行<code class="highlighter-rouge">app.call(env)</code>,就会出现一系列的链式调用。从而保证middleware都可以被调用到call方法,然后再次返回处理逻辑。</p>
<p>总结:Rack还做了很多的其它处理工作,把app和app server给串联起来只是其中的一部分。总的来说就是当一个请求过来时,可以通过一个个的middleware链式的调用,完成一些列的功能调用。从而细分了各个功能点,方便了开发人员更好的去扩展。</p>
<h3 id="ref">Ref:</h3>
<ul>
<li><a href="https://ruby-china.org/topics/21517">为什么我们需要 Rack ?</a></li>
<li><a href="https://ruby-china.org/topics/31592">Ruby Rack 及其应用 (上)</a></li>
</ul>
</section>
</article>
</main>
<aside class="read-next">
<!-- [[! next_post ]] -->
<!-- [[! /next_post ]] -->
<!-- [[! prev_post ]] -->
<a class="read-next-story prev " style="background-image: url(/assets/images/cover3.jpg)" href="/algorithm-comic">
<section class="post">
<h2>Algorithm</h2>
<p></p>
</section>
</a>
<!-- [[! /prev_post ]] -->
</aside>
<!-- /post -->
<!-- The tiny footer at the very bottom -->
<footer class="site-footer clearfix">
<section class="copyright"><a href="/">Thinking</a> © 2020</section>
<section class="poweredby">Proudly published with <a href="https://jekyllrb.com/">Jekyll</a> using <a href="https://github.com/jekyller/jasper">Jasper</a></section>
</footer>
</div>
<!-- highlight.js -->
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.3.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
<!-- jQuery needs to come before `` so that jQuery can be used in code injection -->
<script type="text/javascript" src="//code.jquery.com/jquery-1.12.0.min.js"></script>
<!-- Ghost outputs important scripts and data with this tag -->
<!-- -->
<!-- Add Google Analytics -->
<!-- Google Analytics Tracking code -->
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-69281367-1', 'auto');
ga('send', 'pageview');
</script>
<!-- Fitvids makes video embeds responsive and awesome -->
<script type="text/javascript" src="/assets/js/jquery.fitvids.js"></script>
<!-- The main JavaScript file for Casper -->
<script type="text/javascript" src="/assets/js/index.js"></script>
</body>
</html>