Rack 入门实战:从零创建 Rack 应用、处理路由与请求体,并理解中间件机制
后端【免费下载链接】rackA modular Ruby web server interface.项目地址https://gitcode.com/gh_mirrors/ra/rack点击查看免费下载本文基于 rack 仓库当前版本 3.2.0的官方入门指南 GETTING-STARTED.md系统讲解如何创建一个最简 Rack 应用并用 Web 服务器运行、如何读取env实现路由与请求体解析、如何通过 streaming body 实现流式响应以及中间件的工作原理。读完之后你将掌握 Rack 协议中「env进、[status, headers, body]出」这一核心契约的完整使用方式并能结合 lib/rack/builder.rb、lib/rack/request.rb 等源码理解其底层实现。一、前置条件Rack 与 Web 服务器Rack 本身不是一个 Web 服务器而是定义了 Web 服务器与 Ruby 应用之间的标准接口。真正接收 HTTP 请求的是符合 Rack 规范的 Web 服务器官方指南的示例统一使用 Puma 作为演示服务器README 中还列出了 Falcon、Iodine、Thin、Unicorn、Pitchfork、Lamby 等一系列受支持服务器见 README.md。安装方式为安装 rack gem 与 puma gem$ gem install rack $ gem install puma当前仓库的 lib/rack/version.rb 中声明VERSION 3.2.0即本文所有示例对应的实现基线是 Rack 3.2。二、Rack 应用的最小定义一个响应call的对象根据 SPEC.rdoc 的定义一个 Rack 应用是一个 Ruby 对象它对call做出响应call恰好接受一个参数——environment请求环境即env并返回一个恰好含三个元素的非冻结Arraystatus、headers、body。2.1 用 lambda 定义rack_app lambda do |env| [200, { content-type text/plain }, [Hello World]] end run rack_app返回数组三个元素的含义是HTTP 响应码示例中为200一个 Hash包含要发送的任意 HTTP 响应头一个可枚举对象yield 出 String表示响应体。2.2 用类定义也可以用类来定义 Rack 应用通过self.call类方法接入class App def self.call(env) new(env).route end def initialize(env) env env end def route [200, { content-type text/plain }, [Hello World]] end end run App2.3 响应各部分的规范约束从 SPEC.rdoc 的 The Response 一节可以看到应用返回的三元组必须满足严格的契约status必须是一个不小于100的 Integerheaders必须是一个未冻结的 Hash键为 String不能含status键键不得含大写字母且状态码为1xx/204/304时不得携带content-type或content-lengthbody通常是一个 yield String 的枚举体响应each也可以是一个接受stream参数的可调对象响应call即下文要讲的 Streaming Body。若 Body 同时响应each和call按枚举体对待。Body 被消费后若响应close则必须调用一次。这个契约保证了「任何合法的 Rack 应用可以原样运行在任何合规的 Web 服务器上」。三、通过 config.ru 运行应用Web 服务器通过命令行启动 Rack 应用应用入口保存在config.ru文件中。完整的最小运行流程如下$ cat config.ru APP rack_app lambda do |env| [200, { content-type text/plain }, [Hello World]] end run rack_app APP $ gem install puma $ puma应用随后应在http://localhost:9292上可用$ curl localhost:9292 Hello World3.1 config.ru 是如何被解析的config.ru中使用的use/run并不是普通 Ruby 语句而是Rack::Builder提供的 DSL。从源码 lib/rack/builder.rb 可以看到其加载链路Rack::Builder.parse_file(path)判断文件后缀以.ru结尾则走load_file否则require该文件并按文件名的驼峰形式猜测应用常量例如app.rb对应App常量load_file读取文件内容剥离 UTF-8 BOM忽略__END__之后的内容然后交给new_from_stringnew_from_string创建Rack::Builder实例通过BUILDER_TOPLEVEL_BINDINGlib/rack/builder.rb在 Builder 实例的上下文中eval脚本最后调用to_app返回组装好的应用对象。因此run rack_app只是把最终应用登记到 Builder 上真正返回可调用对象的是to_app。测试 test/spec_builder.rb 用Rack::MockRequest对 Builder 生成的应用直接发起虚拟请求例如验证run支持 block 形式、同时传 app 和 block 会抛ArgumentError等行为test/rackup/config.ru 则是一个真实的config.ru示例展示了use EnvMiddlewarerun TestRequest.new的标准写法。四、路由与 HTTP 方法处理4.1 基于 PATH_INFO 的路由不同路径的分发可以查询env中的PATH_INFOapp lambda do |env| body case env[PATH_INFO] when /admin [Hello Admin] else [Hello World] end [200, { content-type text/plain }, body] end run app4.2 基于 REQUEST_METHOD 的方法感知HTTP 方法同样可以从env中读取app lambda do |env| [200, { content-type text/plain }, [HTTP #{env[REQUEST_METHOD]}]] end run app4.3 使用 Rack::Request 的便捷接口Rack 提供了Rack::Request它对 Rack 环境实现了便捷访问接口上面两个例子可以改写为app lambda do |env| request Rack::Request.new(env) body case request.path_info when /admin [Hello Admin] else [Hello World] end [200, { content-type text/plain }, body] end run appapp lambda do |env| request Rack::Request.new(env) [200, { content-type text/plain }, [HTTP #{request.request_method}]] end run app从源码 lib/rack/request.rb 可以看到path_info、request_method、body本质都是对env中对应键的薄封装path_info读取PATH_INFO并强制to_sbody读取RACK_INPUT即rack.input。Rack::Request还额外提供了 query string 解析、multipart 表单处理、IP 过滤与 forwarded 头解析等能力见 lib/rack/request.rb 开头的注释与 README 的 Convenience interfaces 一节。4.4 env 中必须存在哪些键从 SPEC.rdoc 的 The Request Environment 一节整理Web 服务器必须为每个请求填充以下 CGI 变量除可为空者外env 键含义REQUEST_METHODHTTP 方法如GET、POST永远非空SCRIPT_NAMEURL 路径中对应应用对象的部分可为空字符串PATH_INFO请求目标在应用内的虚拟位置可为空字符串QUERY_STRING?之后的部分可为空但键必须存在SERVER_NAME合法主机名RFC3986SERVER_PROTOCOLHTTP 版本须匹配HTTP\/\d(\.\d)?SERVER_PORT服务器端口仅非标准端口时CONTENT_TYPE/CONTENT_LENGTH请求体的 MIME 类型与长度可选HTTP_HOST可选的 HTTP authorityrack.url_schemehttp/https/ws/wss按最后一跳设置rack.input输入流若存在请求体rack.errors错误流此外任意 HTTP 头以HTTP_前缀进入 env例如HTTP_AUTHORIZATION而rack.前缀的键保留给 Rack 规范与内置类使用。五、读取请求体当 HTTP 请求携带 body 时Rack 通过env[rack.input]或Rack::Request#body提供一个IO风格的输入流对象。以下应用演示如何处理带 JSON body 的POST请求require json app lambda do |env| body if env[REQUEST_METHOD] POST raw_body env[rack.input].read city JSON.parse(raw_body)[city] [Hello #{city}] else [Hello World!] end [200, { content-type text/plain }, body] end run app启动应用后发起请求$ curl -X POST http://localhost:9292/ \ -H Content-Type: application/json; charsetutf-8 \ -d { city: London } Hello London关于输入流SPEC.rdoc 的 The Input Stream 一节给出了明确约束它是一个IO风格对象外部编码须为ASCII-8BIT二进制模式必须响应gets无参调用返回 String 或 EOF 时返回 nil、each仅 yield String和read签名为read([length, [buffer]])不带参数读到 EOF带length时 EOF 返回 nil、不带length时 EOF 返回。因此对请求体执行一次read拿到完整内容正是上例的做法需要注意输入流通常只能顺序读取一次若后续组件还需要重新消费 body需要借助Rack::RewindableInput这类缓冲组件。六、流式响应体Streaming Body除了「body 是字符串的可枚举对象」之外body 还可以是一个可调对象——它会被传入一个stream用于写入app lambda do |env| body proc do |stream| 5.times do stream.write #{Time.now}\n\n sleep 1 end ensure stream.close end [200, { content-type text/plain }, body] end run app运行该应用并发起请求$ curl localhost:9292你会看到时间戳以 1 秒为间隔打印 5 次随后连接关闭。从 SPEC.rdoc 的 Streaming Body 一节看stream参数必须响应read、write、、flush、close、close_read、close_write、closed?其语义应尽力与标准 RubyIO/Socket保持一致服务器可以直接传入真实 IO也可以在transfer-encoding或 HTTP/2 场景下传入实现这些方法的包装对象。Streaming body 让应用得以在持久连接上实现流式通信例如 HTTP Server-Sent Events 与 WebSocket。七、Rack 中间件7.1 中间件的基本形态Rack 应用可以被middleware包裹。中间件在请求到达主应用之前对其操作又可以在应用返回响应之后再次操作它典型用途包括日志、缓存、认证、性能度量等。中间件必须有一个接受 Rack 应用及任意配置参数的new方法并返回一个响应call的 Rack 应用。通常中间件是类其实例包裹对下层应用的访问class MyMiddleware def initialize(app) app app end def call(env) # Operations before the request hits the main application # ------------------------------------------------------- # Propgate the request down the middleware stack status, headers, body app.call(env) # --------------------------------------- # Operations after the request comes back # Propogate the response up the the middleware stack [status, headers, body] end end7.2 短路short-circuit中间件中间件可以完全跳过app.call、自行返回响应来短路整个栈——此时请求不会触及主应用和栈中其余中间件。认证类中间件常用这一手法class AuthenticateRequest def initialize(app) app app end def call(env) if authenticated?(env[HTTP_AUTHORIZATION]) app.call(env) else [401, { content-type text/plain }, [Authentication failed]] end end def authenticated?(token) # ... end end注意这里读取的env[HTTP_AUTHORIZATION]正是前文 HTTP_ 头进入 env 规则的实际应用。7.3 用 use 组装中间件栈中间件通过use添加到 Rack 应用class AuthenticateRequest # ... end app lambda do |env| [200, { content-type text/plain }, [Hello World]] end use AuthenticateRequest run app构建应用所用的这套 DSL 由Rack::Builder提供。从 lib/rack/builder.rb 的实现看use只是把proc { |app| middleware.new(app, ...) }追加进use数组真正组装发生在to_applib/rack/builder.rb先以run登记的应用或map生成的Rack::URLMap为内核再用use.reverse.inject按倒序依次用中间件包裹最终形成洋葱式的调用链——先use的中间件位于栈外层最先收到请求。Builder还支持map在特定路径下路由到不同应用、可嵌套使用use与warmup应用返回前执行预热逻辑等语句测试 test/spec_builder.rb 对map与路径级use的组合行为有完整断言。7.4 Rack 内置中间件清单README 列出了随 Rack 一起发布的中间件可直接用use引入见 README.md Available middleware shipped with Rack 一节Rack::CommonLogger生成 Apache 风格日志Rack::ConditionalGet响应未变化时返回 304 Not ModifiedRack::Config在处理请求前修改环境Rack::ContentLength按 body 大小设置content-length头Rack::ContentType为响应设置默认content-typeRack::Deflatergzip 压缩响应Rack::ETag为可缓冲的 body 设置etag头Rack::Events提供请求接收/响应发送时的钩子Rack::Head对 HEAD 请求返回空 bodyRack::Lint检查应用是否符合 Rack 规范Rack::Lock用互斥锁串行化请求Rack::MethodOverride按提交的参数改写请求方法Rack::Recursive包含应用内其他路径的数据、执行内部重定向Rack::Reloader文件变化时自动重载Rack::Runtime添加记录请求处理耗时的响应头Rack::Sendfile配合支持优化文件发送的 Web 服务器Rack::ShowException以可点击回溯的友好页面呈现未处理异常Rack::ShowStatus为空的客户端错误响应渲染错误页Rack::Static可配置的静态文件服务Rack::TempfileReaper清理请求期间创建的临时文件。这些组件在 lib/rack.rb 中全部以autoload登记require rack后即可按需加载。SPEC.rdoc 特别建议开发中间件时务必用Rack::Lint实现见 lib/rack/lint.rb进行测试以便尽早发现对规范的违反。八、小结Rack 用「请求是env、响应是[status, headers, body]」这一极简设计标准化了 Web 服务器与应用之间的接口任何符合规范的 Ruby Web 应用都可以原样运行在任何符合规范的 Web 服务器上。由于 Rack 定位在低层Ruby Web 应用通常构建在它之上的框架Rails、Sinatra、Roda、Hanami 等见 README.md Supported web frameworks 一节中开发而这些框架几乎都暴露了对底层 Rack API 的访问。本文所用的Rack::Request、Rack::Builder、Rack::Lint、Rack::MockRequest/Rack::MockResponse等组件既是编写应用的日常工具也是理解整个 Rack 生态的入口。赞分享后端【免费下载链接】rackA modular Ruby web server interface.项目地址https://gitcode.com/gh_mirrors/ra/rack点击查看免费下载相关推荐Rack::Attack 实战指南用 Rack 中间件实现请求放行、拦截与限流Rack::Attack 实战指南用 Rack 中间件实现请求放行、拦截与限流 Rack::Attack 是一个基于 Rack 的中间件用于保护 Rails应用安全后端VCR Rack 中间件实战为每个请求自动录制与回放 HTTP 交互VCR Rack 中间件实战为每个请求自动录制与回放 HTTP 交互 导读 本文深入解析 VCR 提供的 VCR::Middleware::Rack Rack测试开发工具终极Rack::Attack安全防护指南如何彻底拦截恶意请求保护Rails应用终极Rack::Attack安全防护指南如何彻底拦截恶意请求保护Rails应用 Rack::Attack是一款强大的Rack中间件专为保护Rails和Rac应用安全后端上一篇ruflo 中的 TDD London School 测试 AgentSwarm 协同下的 Mock 驱动测试实战指南下一篇Authelia 存储后端配置指南PostgreSQL 全面解析与生产实践创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考