$30 off During Our Annual Pro Sale. View Details »

The Little Server That Could

stellacotton
November 11, 2016

The Little Server That Could

Have you ever wondered what dark magic happens when you start up your Ruby server? Let’s explore the mysteries of the web universe by writing a tiny web server in Ruby! Writing a web server lets you dig deeper into the Ruby Standard Library and the Rack interface. You’ll get friendlier with I/O, signal trapping, file handles, and threading. You’ll also explore dangers first hand that can be lurking inside your production code- like blocking web requests and shared state with concurrency.

stellacotton

November 11, 2016
Tweet

More Decks by stellacotton

Other Decks in Technology

Transcript

  1. T h e L i t t l e S

    e r v e r T h a t C o u l d T h e L i t t l e S e r v e r T h a t C o u l d
  2. Stella Cotton | @practice_cactus Production Servers: • Unicorn • Puma

    • Phusion Passenger • Thin Dev Server: • WEBrick

  3. Stella Cotton | @practice_cactus 
 1. Interacts with the OS

    to talk to the outside world
 2. Conforms to a specific API
  4. Stella Cotton | @practice_cactus 1. Initial line 2. Zero or

    more header lines 3. Blank line 4. Message body (optional) https://www.jmarshall.com/easy/http/ GET /path/to/index.html HTTP/1.0 Host: www.example.com:80 Hello, this is a message body
  5. Stella Cotton | @practice_cactus 1. Visiting web page
 2. Telnet


    3. CURL
 4. HTTP Client Libraries
 (e.g. Net::HTTP) Clients:
  6. Stella Cotton | @practice_cactus created by unlimicon from the Noun

    Project Client Server Ack Data Pkt 1 Ack Data Pkt 2
  7. Stella Cotton | @practice_cactus created by unlimicon from the Noun

    Project Client Server Data Pkt 10 Data Pkt 4 Data Pkt 4
  8. Stella Cotton | @practice_cactus require 'socket'
 
 def server
 create_server_socket


    end
 
 def create_server_socket
 Socket.new(:INET, :STREAM, 0) #create socket
 end
  9. Stella Cotton | @practice_cactus require 'socket'
 
 def server
 create_server_socket


    end
 
 def create_server_socket
 socket = Socket.new(:INET, :STREAM, 0) #create socket
 sockaddr = Addrinfo.tcp("127.0.0.1", 2222) #create an address
 end
  10. Stella Cotton | @practice_cactus require 'socket'
 
 def server
 create_server_socket


    end
 
 def create_server_socket
 socket = Socket.new(:INET, :STREAM, 0) #create socket
 sockaddr = Addrinfo.tcp("127.0.0.1", 2222) #create an address
 socket.bind(sockaddr) #bind socket to that address
 end
  11. Stella Cotton | @practice_cactus require 'socket'
 
 def server
 create_server_socket


    end
 
 def create_server_socket
 socket = Socket.new(:INET, :STREAM, 0) #create socket
 sockaddr = Addrinfo.tcp("127.0.0.1", 2222) #create an address
 socket.bind(sockaddr) #bind socket to that address
 socket.listen(5) #listen on the socket
 socket #return the socket
 end
  12. Stella Cotton | @practice_cactus require 'socket'
 
 def server
 socket

    = create_server_socket
 loop_and_listen_for_client_requests(socket)
 end def loop_and_listen_for_client_requests(server)
 loop do client_socket, client_addrinfo = server.accept end
 end
  13. Stella Cotton | @practice_cactus require 'socket'
 
 def server
 socket

    = create_server_socket
 loop_and_listen_for_client_requests(socket)
 end def loop_and_listen_for_client_requests(server)
 loop do client_socket, client_addrinfo = server.accept # returns new sockets end
 end
  14. Stella Cotton | @practice_cactus require 'socket'
 
 def server
 socket

    = create_server_socket
 loop_and_listen_for_client_requests(socket)
 end def loop_and_listen_for_client_requests(server)
 loop do client_socket, client_addrinfo = server.accept request = client_socket.recv(1056) end
 end
  15. Stella Cotton | @practice_cactus require 'socket'
 
 def server
 socket

    = create_server_socket
 loop_and_listen_for_client_requests(socket)
 end
 def loop_and_listen_for_client_requests(server)
 loop do
 client_socket, client_addrinfo = server.accept request = client_socket.recv(1056)
 response = run_application_code
 end
 end

  16. Stella Cotton | @practice_cactus require 'socket'
 
 def server
 socket

    = create_server_socket
 loop_and_listen_for_client_requests(socket)
 end def loop_and_listen_for_client_requests(server)
 loop do
 client_socket, client_addrinfo = server.accept request = client_socket.recv(1056)
 response = run_application_code
 end
 end
 def run_application_code
 app_response = "Hello World!\n"
 server_response = "HTTP/1.1 200 OK\r\n" +
 "Content-Type: text/plain\r\n" +
 "Content-Length: #{app_response.bytesize}\r\n” +
 "Connection: close\r\n" +
 "\r\n" +
 app_response
 end
  17. Stella Cotton | @practice_cactus require 'socket'
 
 def server
 socket

    = create_server_socket
 loop_and_listen_for_client_requests(socket)
 end def loop_and_listen_for_client_requests(server)
 loop do
 client_socket, client_addrinfo = server.accept request = client_socket.recv(1056)
 response = run_application_code client_socket.print response
 end
 end
 def run_application_code
 app_response = "Hello World!\n"
 server_response = "HTTP/1.1 200 OK\r\n" +
 "Content-Type: text/plain\r\n" +
 "Content-Length: #{app_response.bytesize}\r\n” +
 "Connection: close\r\n" +
 "\r\n" +
 app_response
 end
  18. Stella Cotton | @practice_cactus require 'socket'
 
 def server
 socket

    = create_server_socket
 loop_and_listen_for_client_requests(socket)
 end def loop_and_listen_for_client_requests(server)
 loop do
 client_socket, client_addrinfo = server.accept request = client_socket.recv(1056)
 response = run_application_code client_socket.print response client_socket.close
 end
 end
 def run_application_code
 app_response = "Hello World!\n"
 server_response = "HTTP/1.1 200 OK\r\n" +
 "Content-Type: text/plain\r\n" +
 "Content-Length: #{app_response.bytesize}\r\n” +
 "Connection: close\r\n" +
 "\r\n" +
 app_response
 end
  19. Stella Cotton | @practice_cactus require 'socket'
 
 def server
 socket

    = create_server_socket
 loop_and_listen_for_client_requests(socket)
 end def loop_and_listen_for_client_requests(server)
 loop do
 client_socket, client_addrinfo = server.accept request = client_socket.recv(1056)
 response = run_application_code client_socket.print response client_socket.close
 end
 end
 def run_application_code
 app_response = "Hello World!\n"
 server_response = "HTTP/1.1 200 OK\r\n" +
 "Content-Type: text/plain\r\n" +
 "Content-Length: #{app_response.bytesize}\r\n” +
 "Connection: close\r\n" +
 "\r\n" +
 app_response
 end
  20. Stella Cotton | @practice_cactus require 'socket'
 
 def server
 socket

    = create_server_socket
 loop_and_listen_for_client_requests(socket)
 end def loop_and_listen_for_client_requests(server)
 loop do
 client_socket, client_addrinfo = server.accept request = client_socket.recv(1056)
 response = run_application_code client_socket.print response client_socket.close
 end
 end
 def run_application_code
 app_response = "Hello World!\n"
 server_response = "HTTP/1.1 200 OK\r\n" +
 "Content-Type: text/plain\r\n" +
 "Content-Length: #{app_response.bytesize}\r\n” +
 "Connection: close\r\n" +
 "\r\n" +
 app_response
 end we never 
 actually use this
 request
  21. Stella Cotton | @practice_cactus 
 Parser extracts… Header fields and

    values Content-Length Request method Response status code Transfer-Encoding HTTP version Request URL Message body
  22. Stella Cotton | @practice_cactus require 'socket'
 
 def server
 socket

    = create_server_socket
 loop_and_listen_for_client_requests(socket)
 end def loop_and_listen_for_client_requests(server)
 loop do
 client_socket, client_addrinfo = server.accept request = client_socket.recv(1056)
 response = run_application_code client_socket.print response client_socket.close
 end
 end
 def run_application_code
 app_response = "Hello World!\n"
 server_response = "HTTP/1.1 200 OK\r\n" +
 "Content-Type: text/plain\r\n" +
 "Content-Length: #{app_response.bytesize}\r\n” +
 "Connection: close\r\n" +
 "\r\n" +
 app_response
 end let’s modify this!
  23. Stella Cotton | @practice_cactus 1. Ruby Object
 2. Responds to

    “.call”
 3. Takes 1 argument (env hash)
 4. Returns a response with 
 status, header, body Rack Interface:
  24. Stella Cotton | @practice_cactus class RackApp
 def call(environment)
 [
 '200',


    {'Content-Type' => 'text/html'},
 ["Hello world for reals"]
 ]
 end
 end
 
 Rack::Handler.register('server', 'Rack::Handler::Server')
 Rack::Handler::Server.run(RackApp.new)

  25. Stella Cotton | @practice_cactus require 'socket'
 
 def server
 socket

    = create_server_socket
 loop_and_listen_for_client_requests(socket)
 end def loop_and_listen_for_client_requests(server)
 loop do
 client_socket, client_addrinfo = server.accept request = client_socket.recv(1056)
 response = run_application_code client_socket.print response client_socket.close
 end
 end
 def run_application_code
 app_response = "Hello World!\n"
 server_response = "HTTP/1.1 200 OK\r\n" +
 "Content-Type: text/plain\r\n" +
 "Content-Length: #{app_response.bytesize}\r\n” +
 "Connection: close\r\n" +
 "\r\n" +
 app_response
 end let’s modify this!
  26. Stella Cotton | @practice_cactus require 'socket'
 
 def server
 socket

    = create_server_socket
 loop_and_listen_for_client_requests(socket)
 end def loop_and_listen_for_client_requests(server)
 loop do
 client_socket, client_addrinfo = server.accept request = client_socket.recv(1056)
 response = run_application_code client_socket.print response client_socket.close
 end
 end def run_application_code
 status, headers, body = app.call({})
 
 server_response = "HTTP/1.1 #{status}\r\n" +
 "#{headers}" +
 "\r\n" +
 "#{body}"
 end
 Rack interface!
  27. Stella Cotton | @practice_cactus class RackApp
 def call(environment)
 [
 '200',


    {'Content-Type' => 'text/html'},
 ["Hello world for reals"]
 ]
 end
 end
 
 Rack::Handler.register('server', 'Rack::Handler::Server')
 Rack::Handler::Server.run(RackApp.new)

  28. Stella Cotton | @practice_cactus class RackApp
 def call(environment)
 maru_gif =

    get_maru_gif_from_giphy_api
 [
 '200',
 {'Content-Type' => 'text/html'},
 [ maru_gif ]
 ]
 end
 end
 
 Rack::Handler.register('server', 'Rack::Handler::Server')
 Rack::Handler::Server.run(RackApp.new) External API call
  29. Stella Cotton | @practice_cactus Client 1 waits 5 seconds Server

    Two Clients Slowly 
 downloading 
 cat gifs
  30. Stella Cotton | @practice_cactus Server Two Clients Client 2 waits

    almost 10 seconds Slowly 
 downloading 
 cat gifs
  31. Stella Cotton | @practice_cactus require 'socket'
 
 def server
 socket

    = create_server_socket
 loop_and_listen_for_client_requests(socket)
 end def loop_and_listen_for_client_requests(server)
 loop do
 client_socket, client_addrinfo = server.accept request = client_socket.recv(1056)
 response = run_application_code client_socket.print response client_socket.close
 end
 end
 code that 
 handles request
  32. Stella Cotton | @practice_cactus require 'socket'
 
 def server
 socket

    = create_server_socket
 loop_and_listen_for_client_requests(socket)
 end def loop_and_listen_for_client_requests(server)
 loop do
 client_socket, client_addrinfo = server.accept handle_request(client_socket) client_socket.close
 end
 end
 def handle_request
 request = client_socket.recv(1056)
 response = run_application_code
 client_socket.print response
 end code that 
 handles request
  33. Stella Cotton | @practice_cactus require 'socket'
 
 def server
 socket

    = create_server_socket
 loop_and_listen_for_client_requests(socket)
 end def loop_and_listen_for_client_requests(server)
 loop do
 client_socket, client_addrinfo = server.accept fork do handle_request(client_socket) end client_socket.close
 end
 end
 def handle_request
 request = client_socket.recv(1056)
 response = run_application_code
 client_socket.print response
 end code that 
 handles request
  34. Stella Cotton | @practice_cactus Still slowly 
 downloading 
 cat

    gifs Server Two Clients Client 2 only waits 5 seconds
  35. Stella Cotton | @practice_cactus require 'socket'
 
 def server
 socket

    = create_server_socket
 loop_and_listen_for_client_requests(socket)
 end def loop_and_listen_for_client_requests(server)
 loop do
 client_socket, client_addrinfo = server.accept fork do handle_request(client_socket) end client_socket.close
 end
 end
 def handle_request
 request = client_socket.recv(1056)
 response = run_application_code
 client_socket.print response
 end be sure to close this!
  36. Stella Cotton | @practice_cactus 
 Killing Zombies: sudo lsof -i

    -n -P | grep 2222 ps aux | grep [process id] kill [process id]

  37. Stella Cotton | @practice_cactus Parent Process fork do Child Process

    fork do Child Process fork do Child Process
  38. Stella Cotton | @practice_cactus Parent Process fork do Child Process

    fork do Child Process fork do Child Process
  39. Stella Cotton | @practice_cactus Pre- Ruby 2.0
 :( *technically a

    patched version of Ruby 1.8 existed, but wasn’t in the core
  40. Stella Cotton | @practice_cactus Parent Process fork do Child Process

    fork do Child Process fork do Child Process
  41. Stella Cotton | @practice_cactus 
 
 Ruby 2.0
 :) http://patshaughnessy.net/2012/3/23/


    why-you-should-be-excited-about-garbage-collection-in-ruby-2-0
  42. Stella Cotton | @practice_cactus require 'socket'
 
 def server
 socket

    = create_server_socket
 loop_and_listen_for_client_requests(socket)
 end def loop_and_listen_for_client_requests(server)
 loop do
 client_socket, client_addrinfo = server.accept fork do handle_request(client_socket) end client_socket.close
 end
 end
 def handle_request
 request = client_socket.recv(1056)
 response = run_application_code
 client_socket.print response
 end
  43. Stella Cotton | @practice_cactus Parent Process Thread.new Child Thread Child

    Thread Child Thread Thread.new Thread.new Shared Memory
  44. Stella Cotton | @practice_cactus Benefits of Threading:
 - Uses less

    memory
 -Threads die with process
 - Much faster to communicate
  45. Stella Cotton | @practice_cactus def purchase_cat_toys
 if @available_cat_toys > 0


    buy_cat_toy
 else
 cat_toy_unavailable
 end
 end

  46. Stella Cotton | @practice_cactus def purchase_cat_toys
 if @available_cat_toys > 0

    
 #context switch 
 #buy a cat toy that #doesn’t exist :( buy_cat_toy
 else
 cat_toy_unavailable
 end
 end def purchase_cat_toys 
 if @available_cat_toys > 0
 #buy last cat toy
 buy_cat_toy 
 else
 cat_toy_unavailable
 end
 end #context switch Thread 1 Thread 2
  47. Stella Cotton | @practice_cactus Downsides of Threading:
 - Your code

    has to be thread safe
 - Your gems have to be thread safe
  48. Stella Cotton | @practice_cactus require 'socket'
 
 def server
 trap_interrupt


    socket = create_server_socket
 loop_and_listen_for_client_requests(socket)
 socket.close
 end
 
 def trap_interrupt
 Signal.trap("INT") do
 puts "\nTerminating..."
 exit 130
 end
 end