NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

浅谈 WEBrick 的多线程模型

  • 2019-12-05
  • 本文字数:6447 字

    阅读完需:约 21 分钟

浅谈 WEBrick 的多线程模型


这篇文章会介绍在开发环境中最常用的应用容器 WEBrick 的实现原理,除了通过源代码分析之外,我们也会介绍它的 IO 模型以及一些特点。


在 GitHub 上,WEBrick 从 2003 年的六月份就开始开发了,有着十几年历史的 WEBrick 的实现非常简单,总共只有 4000 多行的代码:


Ruby


$ loc_counter .40 files processedTotal     6918 linesEmpty     990 linesComments  1927 linesCode      4001 lines
复制代码

WEBrick 的实现

由于 WEBrick 是 Rack 中内置的处理器,所以与 Unicorn 和 Puma 这种第三方开发的 webserver 不同,WEBrick 的处理器是在 Rack 中实现的,而 WEBrick 的运行也都是从这个处理器的开始的。


Ruby


module Rack  module Handler    class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet      def self.run(app, options={})        environment  = ENV['RACK_ENV'] || 'development'        default_host = environment == 'development' ? 'localhost' : nil
options[:BindAddress] = options.delete(:Host) || default_host options[:Port] ||= 8080 @server = ::WEBrick::HTTPServer.new(options) @server.mount "/", Rack::Handler::WEBrick, app yield @server if block_given? @server.start end end endend
复制代码


我们在上一篇文章 谈谈 Rack 协议与实现 中介绍 Rack 的实现原理时,最终调用了上述方法,从这里开始大部分的实现都与 WEBrick 有关了。


在这里,你可以看到方法会先处理传入的参数比如:地址、端口号等等,在这之后会使用 WEBrick 提供的 HTTPServer 来处理 HTTP 请求,调用 mount 在根路由上挂载应用和处理器 Rack::Handler::WEBrick 接受请求,最后执行 #start 方法启动服务器。

初始化服务器

HTTPServer 的初始化分为两个阶段,一部分是 HTTPServer 的初始化,另一部分调用父类的 initialize 方法,在自己构造器中,会配置当前服务器能够处理的 HTTP 版本并初始化新的 MountTable 实例:


Ruby


From: lib/webrick/httpserver.rb @ line 46:Owner: #<Class:WEBrick::HTTPServer>
def initialize(config={}, default=Config::HTTP) super(config, default) @http_version = HTTPVersion::convert(@config[:HTTPVersion])
@mount_tab = MountTable.new if @config[:DocumentRoot] mount("/", HTTPServlet::FileHandler, @config[:DocumentRoot], @config[:DocumentRootOptions]) end
unless @config[:AccessLog] @config[:AccessLog] = [ [ $stderr, AccessLog::COMMON_LOG_FORMAT ], [ $stderr, AccessLog::REFERER_LOG_FORMAT ] ] end
@virtual_hosts = Array.newend
复制代码


在父类 GenericServer 中初始化了用于监听端口号的 Socket 连接:


Ruby


From: lib/webrick/server.rb @ line 185:Owner: #<Class:WEBrick::GenericServer>
def initialize(config={}, default=Config::General) @config = default.dup.update(config) @status = :Stop
@listeners = [] listen(@config[:BindAddress], @config[:Port]) if @config[:Port] == 0 @config[:Port] = @listeners[0].addr[1] endend
复制代码


每一个服务器都会在初始化的时候创建一系列的 listener 用于监听地址和端口号组成的元组,其内部调用了 Utils 模块中定义的方法:


Ruby


From: lib/webrick/server.rb @ line 127:Owner: #<Class:WEBrick::GenericServer>
def listen(address, port) @listeners += Utils::create_listeners(address, port)end
From: lib/webrick/utils.rb @ line 61:Owner: #<Class:WEBrick::Utils>
def create_listeners(address, port) sockets = Socket.tcp_server_sockets(address, port) sockets = sockets.map {|s| s.autoclose = false ts = TCPServer.for_fd(s.fileno) s.close ts } return socketsendmodule_function :create_listeners
复制代码


.create_listeners 方法中调用了 .tcp_server_sockets 方法由于初始化一组 Socket 对象,最后得到一个数组的 TCPServer 实例。

挂载应用

在使用 WEBrick 启动服务的时候,第二步就是将处理器和 Rack 应用挂载到根路由下:


Ruby


@server.mount "/", Rack::Handler::WEBrick, app
复制代码


#mount 方法其实是一个比较简单的方法,因为我们在构造器中已经初始化了 MountTable 对象,所以这一步只是将传入的多个参数放到这个表中:


Ruby


From: lib/webrick/httpserver.rb @ line 155:Owner: WEBrick::HTTPServer
def mount(dir, servlet, *options) @mount_tab[dir] = [ servlet, options ]end
复制代码


MountTable 是一个包含从路由到 Rack 处理器一个 App 的映射表:



当执行了 MountTable#compile 方法时,上述的对象就会将表中的所有键按照加入的顺序逆序拼接成一个如下的正则表达式用来匹配传入的路由:


Ruby


^(/|/admin|/user)(?=/|$)
复制代码


上述正则表达式在使用时如果匹配到了指定的路由就会返回 $&$' 两个部分,前者表示整个匹配的文本,后者表示匹配文本后面的字符串。

启动服务器

Rack::Handler::WEBrick 中的 .run 方法先初始化了服务器,将处理器和应用挂载到了根路由上,在最后执行 #start 方法启动服务器:


Ruby


From: lib/webrick/server.rb @ line 152:Owner: WEBrick::GenericServer
def start(&block) raise ServerError, "already started." if @status != :Stop
@status = :Running begin while @status == :Running begin if svrs = IO.select([*@listeners], nil, nil, 2.0) svrs[0].each{ |svr| sock = accept_client(svr) start_thread(sock, &block) } end rescue Errno::EBADF, Errno::ENOTSOCK, IOError, StandardError => ex rescue Exception => ex raise end end ensure cleanup_listener @status = :Stop endend
复制代码


由于原方法的实现比较复杂不容易阅读,在这里对方法进行了简化,省略了向 logger 中输出内容、处理服务的关闭以及执行回调等功能。


我们可以理解为上述方法通过 .select 方法对一组 Socket 进行监听,当有消息需要处理时就依次执行 #accept_client#start_thread 两个方法处理来自客户端的请求:


Ruby


From: lib/webrick/server.rb @ line 254:Owner: WEBrick::GenericServer
def accept_client(svr) sock = nil begin sock = svr.accept sock.sync = true Utils::set_non_blocking(sock) rescue Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPROTO, Errno::EINVAL rescue StandardError => ex msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}" @logger.error msg end return sockend
复制代码


WEBrick 在 #accept_client 方法中执行了 #accept 方法来得到一个 TCP 客户端 Socket,同时会通过 set_non_blocking 将该 Socket 变成非阻塞的,最后在方法末尾返回创建的 Socket。


#start_thread 方法中会开启一个新的线程,并在新的线程中执行 #run 方法来处理请求:


Ruby


From: lib/webrick/server.rb @ line 278:Owner: WEBrick::GenericServer
def start_thread(sock, &block) Thread.start { begin Thread.current[:WEBrickSocket] = sock run(sock) rescue Errno::ENOTCONN, ServerError, Exception ensure Thread.current[:WEBrickSocket] = nil sock.close end }end
复制代码

处理请求

所有的请求都不会由 GenericServer 这个通用的服务器来处理,它只处理通用的逻辑,对于 HTTP 请求的处理都是在 HTTPServer#run 中完成的:


Ruby


From: lib/webrick/httpserver.rb @ line 69:Owner: WEBrick::HTTPServer
def run(sock) while true res = HTTPResponse.new(@config) req = HTTPRequest.new(@config) server = self begin timeout = @config[:RequestTimeout] while timeout > 0 break if sock.to_io.wait_readable(0.5) break if @status != :Running timeout -= 0.5 end raise HTTPStatus::EOFError if timeout <= 0 || @status != :Running raise HTTPStatus::EOFError if sock.eof? req.parse(sock) res.request_method = req.request_method res.request_uri = req.request_uri res.request_http_version = req.http_version self.service(req, res) rescue HTTPStatus::EOFError, HTTPStatus::RequestTimeout, HTTPStatus::Error => ex res.set_error(ex) rescue HTTPStatus::Status => ex res.status = ex.code rescue StandardError => ex res.set_error(ex, true) ensure res.send_response(sock) if req.request_line end break if @http_version < "1.1" endend
复制代码


对 HTTP 协议了解的读者应该能从上面的代码中看到很多与 HTTP 协议相关的东西,比如 HTTP 的版本号、方法、URL 等等,上述方法总共做了三件事情,等待监听的 Socket 变得可读,执行 #parse 方法解析 Socket 上的数据,通过 #service 方法完成处理请求的响应,首先是对 Socket 上的数据进行解析:


Ruby


From: lib/webrick/httprequest.rb @ line 192:Owner: WEBrick::HTTPRequest
def parse(socket=nil) @socket = socket begin @peeraddr = socket.respond_to?(:peeraddr) ? socket.peeraddr : [] @addr = socket.respond_to?(:addr) ? socket.addr : [] rescue Errno::ENOTCONN raise HTTPStatus::EOFError end
read_request_line(socket) if @http_version.major > 0 # ... end return if @request_method == "CONNECT" return if @unparsed_uri == "*"
begin setup_forwarded_info @request_uri = parse_uri(@unparsed_uri) @path = HTTPUtils::unescape(@request_uri.path) @path = HTTPUtils::normalize_path(@path) @host = @request_uri.host @port = @request_uri.port @query_string = @request_uri.query @script_name = "" @path_info = @path.dup rescue raise HTTPStatus::BadRequest, "bad URI `#{@unparsed_uri}'." end
if /close/io =~ self["connection"] # deal with keep alive endend
复制代码


由于 HTTP 协议本身就比较复杂,请求中包含的信息也非常多,所以在这里用于解析 HTTP 请求的代码也很多,想要了解 WEBrick 是如何解析 HTTP 请求的可以看 httprequest.rb 文件中的代码,在处理了 HTTP 请求之后,就开始执行 #service 响应该 HTTP 请求了:


Ruby


From: lib/webrick/httpserver.rb @ line 125:Owner: WEBrick::HTTPServer
def service(req, res) servlet, options, script_name, path_info = search_servlet(req.path) raise HTTPStatus::NotFound, "`#{req.path}' not found." unless servlet req.script_name = script_name req.path_info = path_info si = servlet.get_instance(self, *options) si.service(req, res)end
复制代码


在这里我们会从上面提到的 MountTable 中找出在之前注册的处理器 handler 和 Rack 应用:


Ruby


From: lib/webrick/httpserver.rb @ line 182:Owner: WEBrick::HTTPServer
def search_servlet(path) script_name, path_info = @mount_tab.scan(path) servlet, options = @mount_tab[script_name] if servlet [ servlet, options, script_name, path_info ] endend
复制代码


得到了处理器 handler 之后,通过 .get_instance 方法创建一个新的实例,这个方法在大多数情况下等同于初始化方法 .new,随后调用了该处理器 Rack::WEBrick::Handler#service 方法,该方法是在 rack 工程中定义的:


Ruby


From: rack/lib/handler/webrick.rb @ line 57:Owner: Rack::Handler::WEBrick
def service(req, res) res.rack = true env = req.meta_vars env.delete_if { |k, v| v.nil? }
env.update( # ... RACK_URL_SCHEME => ["yes", "on", "1"].include?(env[HTTPS]) ? "https" : "http", # ... )
status, headers, body = @app.call(env) begin res.status = status.to_i headers.each { |k, vs| # ... }
body.each { |part| res.body << part } ensure body.close if body.respond_to? :close endend
复制代码


由于上述方法也涉及了非常多 HTTP 协议的实现细节所以很多过程都被省略了,在上述方法中,我们先构建了应用的输入 env 哈希变量,然后通过执行 #call 方法将控制权交给 Rack 应用,最后获得一个由 statusheadersbody 组成的三元组;在接下来的代码中,分别对这三者进行处理,为这次请求『填充』一个完成的 HTTP 请求。


到这里,最后由 WEBrick::HTTPServer#run 方法中的 ensure block 来结束整个 HTTP 请求的处理:


Ruby


From: lib/webrick/httpserver.rb @ line 69:Owner: WEBrick::HTTPServer
def run(sock) while true res = HTTPResponse.new(@config) req = HTTPRequest.new(@config) server = self begin # ... ensure res.send_response(sock) if req.request_line end break if @http_version < "1.1" endend
复制代码


#send_reponse 方法中,分别执行了 #send_header#send_body 方法向当前的 Socket 中发送 HTTP 响应中的数据:


Ruby


From: lib/webrick/httpresponse @ line 205:Owner: WEBrick::HTTPResponse
def send_response(socket) begin setup_header() send_header(socket) send_body(socket) rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ENOTCONN => ex @logger.debug(ex) @keep_alive = false rescue Exception => ex @logger.error(ex) @keep_alive = false endend
复制代码


所有向 Socket 中写入数据的工作最终都会由 #_write_data 这个方法来处理,将数据全部写入 Socket 中:


Ruby


From: lib/webrick/httpresponse @ line 464:Owner: WEBrick::HTTPResponse
def _write_data(socket, data) socket << dataend
复制代码


从解析 HTTP 请求、调用 Rack 应用、创建 Response 到最后向 Socket 中写回数据,WEBrick 处理 HTTP 请求的部分就结束了。

I/O 模型

通过对 WEBrick 源代码的阅读,我们其实已经能够了解整个 webserver 的工作原理,当我们启动一个 WEBrick 服务时只会启动一个进程,该进程会在指定的 ip 和端口上使用 .select 监听来自用户的所有 HTTP 请求:



.select 接收到来自用户的请求时,会为每一个请求创建一个新的 Thread 并在新的线程中对 HTTP 请求进行处理。


由于 WEBrick 在运行时只会启动一个进程,并没有其他的守护进程,所以它不够健壮,不能在发生问题时重启持续对外界提供服务,再加上 WEBrick 确实历史比较久远,代码的风格也不是特别的优雅,还有普遍知道的内存泄漏以及 HTTP 解析的问题,所以在生产环境中很少被使用。


虽然 WEBrick 有一些性能问题,但是作为 Ruby 自带的默认 webserver,在开发阶段使用 WEBrick 提供服务还是没有什么问题的。

总结

WEBrick 是 Ruby 社区中老牌的 webserver,虽然至今也仍然被广泛了解和使用,但是在生产环境中开发者往往会使用更加稳定的 Unicorn 和 Puma 代替它,我们选择在这个系列的文章中介绍它很大原因就是 WEBrick 的源代码与实现足够简单,我们很快就能了解一个 webserver 到底具备那些功能,在接下来的文章中我们就可以分析更加复杂的 webserver、了解更复杂的 IO 模型与实现了。

相关文章


本文转载自 Draveness 技术博客。


原文链接:https://draveness.me/rack-webrick


2019-12-05 18:14541

评论

发布
暂无评论
发现更多内容

Cinema 4D 2024 图文安装教程 c4d2024中文版附激活补丁

南屿

3d建模 Cinema 4D 2024 C4D R24插件 Cinema 4D安装教程

毫巅之微---不同写法的性能差异 番外篇

fliter

点燃你的Python技能:剖析闭包与装饰器的魔力

测试人

软件测试

不会用Photoshop修图?别急,Pixelmator Pro比肩ps的mac修图软件 轻松实现专业级图像处理!

南屿

Pixelmator Pro破解 Pixelmator Pro下载 修图软件 Mac图像编辑器

Python多任务协程:编写高性能应用的秘密武器

霍格沃兹测试开发学社

Go语言对象池实践

FunTester

用Python实现高效数据记录!Web自动化技术助你告别重复劳动!

霍格沃兹测试开发学社

Paste for Mac剪切板工具 强大的功能可提高您的工作效率

南屿

Paste for Mac 剪切板管理软件 paste mac破解版

测试开发高薪私教线下班手把手带你提升职业技能

霍格沃兹测试开发学社

甲骨文云-云迁移新篇章:轻松、快捷的数据库搬家之路

Geek_2d6073

1Password 7 :为用户提供安全高效的密码管理

南屿

1Password 7 Mac密码管理器

Python多任务协程:编写高性能应用的秘密武器!

测试人

软件测试 自动化测试 测试开发

如何快速上手Visio?从入门到精通 | Visio替代软件,建议收藏!

彭宏豪95

效率工具 在线白板 架构图 绘图软件 Visio

青否数字人的源码之家!

青否数字人

数字人

Docker 魔法解密:探索 UnionFS 与 OverlayFS

EquatorCoco

Docker 运维 容器化

短程无线自组网协议之:发展现状与趋势?

Geek_ab1536

SDWAN为什么性价比更高,价格更低廉?

Geek一起出海

SD-WAN

云手机与实体手机的对比

Ogcloud

云手机 海外云手机 跨境电商云手机 tiktok云手机 云手机海外版

网络要素服务(WFS)详解

快乐非自愿限量之名

开发 网络 服务 wfs

Acxyn 和 Footprint Analytics 联手探索 Web3 游戏知识产权评估

Footprint Analytics

区块链 区块链游戏

运用ETLCloud快速实现数据清洗、转换

RestCloud

ETL 数据清洗 数据集成工具

[分词]基于Lucene8版本的逆向最大匹配分词器(依赖本地词典)

alexgaoyh

Java 中文分词 lucene 逆向最大匹配

前端重磅福利!前端百宝箱

前端连环话

前端开发平台

OpenKruise :Kubernetes背后的托底

不在线第一只蜗牛

Kubernetes 容器 云原生

软件测试/测试开发|给你剖析闭包与装饰器的魔力

霍格沃兹测试开发学社

花 15 分钟把 Express.js 搞明白,全栈没有那么难

杨成功

JavaScript node.js 前端 全栈

幻兽帕鲁 Palworld 私有服务器一键部署教程

米开朗基杨

游戏 steam Sealos Palworld 幻兽帕鲁

测试开发高薪私教线下班手把手带你提升职业技能

测试人

软件测试

中国信通院正式启动视联网产业与技术研究工作

信通院IOMM数字化转型团队

中国信通院 视频行业 视联网

香港网站服务器的优势:为什么它们如此受欢迎?

一只扑棱蛾子

香港服务器

浅谈 WEBrick 的多线程模型_语言 & 开发_Draveness_InfoQ精选文章