Upgrade to Pro — share decks privately, control downloads, hide ads and more …

From Rails to the Web Server to The Browser

From Rails to the Web Server to The Browser

Most of us know how to build beautiful web applications with Rails. With the help of templating tools like ERB and HAML our web apps create HTML documents, but, do you know exactly how those HTML documents end up in a browser?

During this talk I will show you the bits that make it all happen. We will dissect the relevant code within Rails, Rack and the thin web server to discover exactly how the web server starts and listens to a TCP port, communicates with Rails and returns the HTML document that your browser parses.

Why? Because we're curious about it, that's why.

0f9c9bbecc4067b9bce445cb11ed5d53?s=128

David Padilla

October 09, 2013
Tweet

Transcript

  1. From Rails to the web server to the browser David

    Padilla @dabit
  2. David Padilla @dabit

  3. www.crowdint.com

  4. Image from: http://www.vectorworldmap.com/vectormaps/vector-world-map-v2.2.jpg

  5. Image from: http://www.vectorworldmap.com/vectormaps/vector-world-map-v2.2.jpg

  6. None
  7. Image from: http://www.vectorworldmap.com/vectormaps/vector-world-map-v2.2.jpg

  8. From Rails to the web server to the browser

  9. Life of a Request

  10. www.app.com/hello HTTP Request

  11. HTTP Request

  12. HTTP Request

  13. <html> <head> <title>Railsconf</title> </head> <body>LOL</body> </html>

  14. <html> <head> <title>Railsconf</title> </head> <body>LOL</body> </html>

  15. None
  16. Rack

  17. Rack

  18. Rack

  19. class App def call(env) # ... return [http_code, headers, body]

    end end
  20. [http_code, headers, body] Integer Hash Responds to :each

  21. Somewhere on the web server...

  22. # ... # ... # ... @app.call(env) # ... #

    ... # ...
  23. Somewhere on the Application...

  24. class App def call(env) # ... return [http_code, headers, body]

    end end
  25. config.ru

  26. require 'rack/app' run Rack::App.new config.ru

  27. None
  28. None
  29. rackup

  30. None
  31. None
  32. None
  33. None
  34. None
  35. All Rails apps are Rack apps

  36. None
  37. None
  38. class HelloController def show render text: "Hello World" end end

    app/controllers/hello_controller.rb
  39. None
  40. App::Application.routes.draw do root to: "hello#show" end config/routes.rb

  41. None
  42. None
  43. class TestServer < Test::Unit::TestCase def test_the_request uri = URI('http://127.0.0.1:9292') assert

    "Hello World", Net::HTTP.get(uri) end end test/test.rb
  44. None
  45. None
  46. None
  47. None
  48. None
  49. None
  50. None
  51. @app.call({ "REQUEST_METHOD" => "GET", "PATH_INFO" => "/", "rack.input" => ""

    })
  52. [status, headers, body] Rack

  53. None
  54. None
  55. None
  56. None
  57. None
  58. None
  59. None
  60. None
  61. None
  62. None
  63. None
  64. None
  65. None
  66. StringIO.new(“Hello World”)

  67. None
  68. class App def call(env) content = "Hello World" status_code =

    200 headers = { "Content-Length" => content.length.to_s } [status_code, headers, [content]] end end rack/app.rb
  69. None
  70. Webservers

  71. # ... # ... # ... @app.call(env) # ... #

    ... # ...
  72. Thin

  73. thin start

  74. module Thin # The uterly famous Thin HTTP server. #

    It listen for incoming request through # a given +backend+ # and forward all request to +app+. # # == TCP server # Create a new TCP server on bound # to <tt>host:port</tt> by specifiying +host+ # and +port+ as the first 2 arguments. # # Thin::Server.start('0.0.0.0', 3000, app) lib/thin/server.rb
  75. Thin::Server.start('0.0.0.0', 3000, app) thin start

  76. # Start the server and listen for connections. def start

    raise ArgumentError, 'app required' unless @app log ">> Thin web server (v#{VERSION::STRING} codename #{VERSION::C debug ">> Debugging ON" trace ">> Tracing ON" log ">> Maximum connections set to #{@backend.maximum_connections}" log ">> Listening on #{@backend}, CTRL+C to stop" @backend.start end lib/thin/server.rb
  77. # Start the server and listen for connections. def start

    raise ArgumentError, 'app required' unless @app log ">> Thin web server (v#{VERSION::STRING} codename #{VERSION::C debug ">> Debugging ON" trace ">> Tracing ON" log ">> Maximum connections set to #{@backend.maximum_connections}" log ">> Listening on #{@backend}, CTRL+C to stop" @backend.start end lib/thin/server.rb
  78. @backend.start lib/thin/server.rb

  79. def select_backend(host, port, options) case when options.has_key?(:backend) raise ArgumentError, ":backend

    must be options[:backend].new(host, port, optio when options.has_key?(:swiftiply) Backends::SwiftiplyClient.new(host, por when host.include?('/') Backends::UnixServer.new(host) else Backends::TcpServer.new(host, port) end end lib/thin/server.rb
  80. def select_backend(host, port, options) case when options.has_key?(:backend) raise ArgumentError, ":backend

    must be options[:backend].new(host, port, optio when options.has_key?(:swiftiply) Backends::SwiftiplyClient.new(host, por when host.include?('/') Backends::UnixServer.new(host) else Backends::TcpServer.new(host, port) end end lib/thin/server.rb
  81. def select_backend(host, port, options) case when options.has_key?(:backend) raise ArgumentError, ":backend

    must be options[:backend].new(host, port, optio when options.has_key?(:swiftiply) Backends::SwiftiplyClient.new(host, por when host.include?('/') Backends::UnixServer.new(host) else Backends::TcpServer.new(host, port) end end lib/thin/server.rb
  82. lib/thin/backends/tcp_server.rb

  83. module Thin module Backends # Backend to act as a

    TCP socket server. class TcpServer < Base # Address and port on which the server is listening for connections. attr_accessor :host, :port def initialize(host, port) @host = host @port = port super() end # Connect the server def connect @signature = EventMachine.start_server(@host, @port, Connection, &method(:initializ end # Stops the server def disconnect EventMachine.stop_server(@signature) end def to_s "#{@host}:#{@port}" end end end end lib/thin/backends/tcp_server.rb
  84. # Connect the server def connect @signature = EventMachine.start_server(@ end

    lib/thin/backends/tcp_server.rb
  85. EventMachine

  86. module Connection def post_init # A client connected end def

    receive_data data # Data received end def unbind # Client disconnected end end EventMachine.start_server("0.0.0.0", 8081, Connection) EventMachine
  87. # Connect the server def connect @signature = EventMachine.start_server(@ end

    lib/thin/backends/tcp_server.rb
  88. ne.start_server(@host, @port, Connection, &metho lib/thin/backends/tcp_server.rb

  89. ne.start_server(@host, @port, Connection, &metho lib/thin/backends/tcp_server.rb

  90. lib/thin/connection.rb

  91. lib/thin/connection.rb

  92. # Called when data is received from the client. def

    receive_data(data) @idle = false trace { data } process if @request.parse(data) rescue InvalidRequest => e log "!! Invalid request" log_error e post_process Response::BAD_REQUEST end lib/thin/connection.rb
  93. # Called when data is received from the client. def

    receive_data(data) @idle = false trace { data } process if @request.parse(data) rescue InvalidRequest => e log "!! Invalid request" log_error e post_process Response::BAD_REQUEST end lib/thin/connection.rb
  94. Thin::Request

  95. lib/thin/request.rb

  96. # Parse a chunk of data into the request environment

    # Raises a +InvalidRequest+ if invalid. # Returns +true+ if the parsing is complete. def parse(data) if @parser.finished? # Header finished, can only be some more body @body << data else # Parse more header using the super parser @data << data raise InvalidRequest, 'Header longer than allowed' if @data.size > MAX_HEADER @nparsed = @parser.execute(@env, @data, @nparsed) # Transfert to a tempfile if body is very big move_body_to_tempfile if @parser.finished? && content_length > MAX_BODY end if finished? # Check if header and body are complete @data = nil @body.rewind true # Request is fully parsed else false # Not finished, need more data end end lib/thin/request.rb
  97. Mongrel Parser

  98. /** * Copyright (c) 2005 Zed A. Shaw * You

    can redistribute it and/or modify * it under the same terms as Ruby. */ #ifndef http11_parser_h #define http11_parser_h #include <sys/types.h> #if defined(_WIN32) #include <stddef.h> #endif
  99. GET /index.html HTTP/1.1 Host:localhost HTTP Request

  100. None
  101. GET /index.html HTTP/1.1\r\n Host:localhost\r\n\r\n HTTP Request

  102. None
  103. None
  104. None
  105. None
  106. # Called when data is received from the client def

    receive_data(data) @idle = false trace { data } process if @request.parse(data) rescue InvalidRequest => e log "!! Invalid request" log_error e post_process Response::BAD_REQUEST end lib/thin/connection.rb
  107. # Called when data is received from the client def

    receive_data(data) @idle = false trace { data } process if @request.parse(data) rescue InvalidRequest => e log "!! Invalid request" log_error e post_process Response::BAD_REQUEST end lib/thin/connection.rb
  108. # Called when all data was received and # the

    request is ready to be processed. def process if threaded? @request.threaded = true EventMachine.defer(method(:pre_process), me else @request.threaded = false post_process(pre_process) end end lib/thin/connection.rb
  109. # Called when all data was received and # the

    request is ready to be processed. def process if threaded? @request.threaded = true EventMachine.defer(method(:pre_process), else @request.threaded = false post_process(pre_process) end end lib/thin/connection.rb
  110. def pre_process # Add client info to the request env

    @request.remote_address = remote_address # Connection may be closed unless the App# # It should be noted that connection objec # callback is no longer referenced, so be @request.async_callback = method(:post_pro if @backend.ssl? @request.env["rack.url_scheme"] = "https if cert = get_peer_cert @request.env['rack.peer_cert'] = cert end end
  111. end # When we're under a non-async framework l #

    off async responses using the callback i # in removing this. response = AsyncResponse catch(:async) do # Process the request calling the Rack a response = @app.call(@request.env) end response rescue Exception handle_error # Pass through error response can_persist? && @request.persistent? ? Res end
  112. end # When we're under a non-async framework l #

    off async responses using the callback i # in removing this. response = AsyncResponse catch(:async) do # Process the request calling the Rack a response = @app.call(@request.env) end response rescue Exception handle_error # Pass through error response can_persist? && @request.persistent? ? Res end lib/thin/connection.rb
  113. # Called when all data was received and # the

    request is ready to be processed. def process if threaded? @request.threaded = true EventMachine.defer(method(:pre_process), else @request.threaded = false post_process(pre_process) end end lib/thin/connection.rb
  114. # Called when all data was received and # the

    request is ready to be processed. def process if threaded? @request.threaded = true EventMachine.defer(method(:pre_process), else @request.threaded = false post_process(pre_process) end end
  115. HTTP/1.1 200 OK Content-Length: 11 Hello World HTTP Response

  116. Unicorn

  117. EventMachine No

  118. @app.call(env) YES

  119. lib/unicorn/http_server.rb def process_client(client)

  120. Uses the mongrel parser Kinda

  121. PUMA

  122. What we’ve learned so far

  123. EventMachine.start_server("0.0.0.0", 8081, Connection)

  124. GET /index.html HTTP/1.1 Host:localhost

  125. GET /index.html HTTP/1.1 Host:localhost { "REQUEST_METHOD" => "GET", "PATH_INFO" =>

    "/", "rack.input" => "" }
  126. @app.call({ "REQUEST_METHOD" => "GET", "PATH_INFO" => "/", "rack.input" => ""

    })
  127. [http_code, headers, body]

  128. [http_code, headers, body] HTTP/1.1 200 OK Content-Length: 11 Hello World

  129. HTTP/1.1 200 OK Content-Length: 11 Hello World

  130. Our own Webserver

  131. None
  132. module Server def receive_data(data) puts data end end rack/server.rb

  133. None
  134. None
  135. module Server def receive_data(data) request = Thin::Request.new request.parse(data) puts request.env

    end end rack/server.rb
  136. None
  137. None
  138. def receive_data(data) request = Thin::Request.new request.parse(data) app = App.new response

    = app.call(request.env) end rack/server.rb
  139. None
  140. HTTP/1.1 200 OK Content-Length: 11 Hello World HTTP Response

  141. None
  142. None
  143. None
  144. None
  145. None
  146. None
  147. None
  148. Do not try this at home

  149. Do not try this in production

  150. Be curious

  151. dabit/rails-server-browser

  152. El fin

  153. Thank you! David Padilla @dabit david@crowdint.com