├── http-3 ├── client.rb └── server.rb ├── .gitignore ├── .editorconfig ├── gems.rb ├── public ├── index.html └── secret.html ├── best ├── shared.rb ├── worker.rb └── fan-out.rb ├── async ├── client.rb └── server.rb ├── http-0.9 ├── client.rb └── server.rb ├── readme.md ├── files.rb ├── http-1.0 ├── client.rb └── server.rb ├── http-1.1 ├── client.rb └── server.rb ├── license.md └── http-2 ├── client.rb └── server.rb /http-3/client.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /http-3/server.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | gems.locked -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 2 6 | -------------------------------------------------------------------------------- /gems.rb: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "async" 4 | gem "async-http" 5 | gem "console" 6 | gem "falcon" 7 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |You have found the secret page!
10 | 11 | -------------------------------------------------------------------------------- /best/shared.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'async' 4 | require 'async/http/internet/instance' 5 | 6 | Async do |task| 7 | internet = Async::HTTP::Internet.instance 8 | 9 | ARGV.each do |url| 10 | response = internet.get(url) 11 | puts response.read 12 | ensure 13 | response.close 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /async/client.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'async' 4 | require 'async/http/client' 5 | require 'async/http/endpoint' 6 | 7 | Async do |task| 8 | endpoint = Async::HTTP::Endpoint.parse('http://127.0.0.1:9294') 9 | client = Async::HTTP::Client.new(endpoint) 10 | 11 | ARGV.each do |path| 12 | response = client.get(path) 13 | puts response.read 14 | ensure 15 | response.close 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /http-0.9/client.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'async' 4 | require 'async/io' 5 | require 'async/io/stream' 6 | 7 | Sync do 8 | endpoint = Async::IO::Endpoint.tcp('localhost', 8009) 9 | 10 | ARGV.each do |path| 11 | endpoint.connect do |connection| 12 | stream = Async::IO::Stream.new(connection) 13 | stream.write("GET #{path}\r\n") 14 | stream.flush 15 | puts stream.read 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /best/worker.rb: -------------------------------------------------------------------------------- 1 | class FanOutWorker < ApplicationWorker 2 | def perform(urls) 3 | Sync do 4 | internet = Async::HTTP::Internet.instance 5 | 6 | tasks = urls.map do |url| 7 | task.async do 8 | response = internet.get(url) 9 | [url, response.read] 10 | ensure 11 | response.close 12 | end 13 | end 14 | 15 | responses = tasks.map(&:wait) 16 | # ... do something with responses ... 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /best/fan-out.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'async' 4 | require 'async/http/internet/instance' 5 | 6 | Async do |task| 7 | internet = Async::HTTP::Internet.instance 8 | 9 | tasks = ARGV.map do |url| 10 | task.async do 11 | response = internet.get(url) 12 | [url, response.read] 13 | ensure 14 | response.close 15 | end 16 | end 17 | 18 | responses = tasks.map(&:wait) 19 | 20 | responses.each do |url, body| 21 | puts "#{url}: #{body}" 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # RubyKaigi 2023: Unleashing the Power of Asynchronous HTTP with Ruby 2 | 3 | This repository includes working examples from the presentation. 4 | 5 | ## Background Materials 6 | 7 | - [RubyKaigi 2022: Real World Applications with the Ruby Fiber Scheduler](https://github.com/ioquatix/rubykaigi-2022/). 8 | - [My previous talks on this topic](https://www.youtube.com/watch?v=qKQcUDEo-ZI&list=PLG-PicXncPwLlJDxW6n99GMsHf6Ol9TKV). 9 | - [The first website: info.cern.ch](http://info.cern.ch/). 10 | -------------------------------------------------------------------------------- /files.rb: -------------------------------------------------------------------------------- 1 | 2 | # This class is used to get files from the public folder. 3 | class Files 4 | def initialize(root = File.expand_path('public', __dir__)) 5 | @root = root 6 | end 7 | 8 | def get(path) 9 | path = File.expand_path(@root + path) 10 | 11 | if path.start_with?(@root) 12 | if File.directory?(path) 13 | path = File.join(path, 'index.html') 14 | end 15 | 16 | if File.file?(path) 17 | return File.open(path, 'rb') 18 | end 19 | end 20 | 21 | return nil 22 | end 23 | end 24 | 25 | FILES = Files.new 26 | -------------------------------------------------------------------------------- /async/server.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'async' 4 | require 'async/http/server' 5 | require 'async/http/endpoint' 6 | require 'async/http/protocol/response' 7 | require_relative '../files' 8 | 9 | Sync do |task| 10 | app = lambda do |request| 11 | if file = FILES.get(request.path) 12 | Protocol::HTTP::Response[200, {}, [file.read]] 13 | else 14 | Protocol::HTTP::Response[404, {}, []] 15 | end 16 | end 17 | 18 | endpoint = Async::HTTP::Endpoint.parse('http://127.0.0.1:9294') 19 | server = Async::HTTP::Server.new(app, endpoint) 20 | 21 | server.run 22 | end 23 | -------------------------------------------------------------------------------- /http-0.9/server.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'async' 4 | require 'async/io' 5 | require 'async/io/stream' 6 | require_relative '../files' 7 | 8 | Sync do 9 | endpoint = Async::IO::Endpoint.tcp('localhost', 8009) 10 | 11 | endpoint.accept do |connection| 12 | stream = Async::IO::Stream.new(connection) 13 | method, path = stream.read_until("\r\n").split(/\s+/, 2) 14 | Console.logger.info(self, "Received #{method} #{path}") 15 | 16 | if file = FILES.get(path) 17 | Console.logger.info(self, "Serving #{path}") 18 | connection.write(file.read) 19 | file.close 20 | else 21 | Console.logger.warn(self, "Could not find #{path}") 22 | end 23 | 24 | rescue => error 25 | Console.logger.error(self, error) 26 | ensure 27 | connection.close 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /http-1.0/client.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'async' 4 | require 'async/io' 5 | require 'async/io/stream' 6 | 7 | Sync do 8 | endpoint = Async::IO::Endpoint.tcp('localhost', 8010) 9 | 10 | ARGV.each do |path| 11 | endpoint.connect do |connection| 12 | stream = Async::IO::Stream.new(connection) 13 | stream.write("GET #{path} HTTP/1.0\r\n") 14 | stream.write("Accept: text/html\r\n") 15 | stream.write("\r\n") 16 | stream.flush 17 | 18 | response_line = stream.read_until("\r\n") 19 | version, status, reason = response_line.split(' ', 3) 20 | Console.logger.info(self, "Received #{version} #{status} #{reason}") 21 | 22 | while line = stream.read_until("\r\n") 23 | break if line.empty? 24 | name, value = line.split(/:\s*/, 2) 25 | Console.logger.info(self, "Header #{name}: #{value}") 26 | end 27 | 28 | puts stream.read 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /http-1.1/client.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'async' 4 | require 'async/io' 5 | require 'async/io/stream' 6 | 7 | Sync do 8 | endpoint = Async::IO::Endpoint.tcp('localhost', 8011) 9 | 10 | endpoint.connect do |connection| 11 | stream = Async::IO::Stream.new(connection) 12 | ARGV.each do |path| 13 | stream.write("GET #{path} HTTP/1.1\r\n") 14 | stream.write("Accept: text/html\r\n") 15 | stream.write("\r\n") 16 | stream.flush 17 | 18 | response_line = stream.read_until("\r\n") 19 | version, status, reason = response_line.split(' ', 3) 20 | Console.logger.info(self, "Received #{version} #{status} #{reason}") 21 | 22 | length = nil 23 | 24 | while line = stream.read_until("\r\n") 25 | break if line.empty? 26 | 27 | name, value = line.split(/:\s+/, 2) 28 | if name.downcase == "content-length" 29 | length = Integer(value) 30 | end 31 | Console.logger.info(self, "Header #{name}: #{value}") 32 | end 33 | 34 | if length 35 | if length > 0 36 | puts stream.read(length) 37 | end 38 | else 39 | puts stream.read 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright, 2023, by Samuel Williams. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /http-1.0/server.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'async' 4 | require 'async/io' 5 | require 'async/io/stream' 6 | require_relative '../files' 7 | 8 | Sync do 9 | endpoint = Async::IO::Endpoint.tcp('localhost', 8010) 10 | 11 | endpoint.accept do |connection| 12 | stream = Async::IO::Stream.new(connection) 13 | method, path, version = stream.read_until("\r\n").split(/\s+/, 3) 14 | Console.logger.info(self, "Received #{method} #{path} #{version}") 15 | 16 | while line = stream.read_until("\r\n") 17 | break if line.empty? 18 | name, value = line.split(/:\s*/, 2) 19 | Console.logger.info(self, "Header #{name}: #{value}") 20 | end 21 | 22 | if file = FILES.get(path) 23 | Console.logger.info(self, "Serving #{path}") 24 | stream.write("HTTP/1.0 200 OK\r\n") 25 | stream.write("Content-Type: text/html\r\n") 26 | stream.write("\r\n") 27 | stream.write(file.read) 28 | file.close 29 | else 30 | Console.logger.warn(self, "Could not find #{path}") 31 | stream.write("HTTP/1.0 404 Not Found\r\n") 32 | stream.write("\r\n") 33 | end 34 | 35 | stream.flush 36 | rescue => error 37 | Console.logger.error(self, error) 38 | ensure 39 | connection.close 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /http-2/client.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'async' 4 | require 'async/io/stream' 5 | require 'async/http/endpoint' 6 | require 'protocol/http2/client' 7 | 8 | CLIENT_SETTINGS = { 9 | ::Protocol::HTTP2::Settings::ENABLE_PUSH => 0, 10 | ::Protocol::HTTP2::Settings::MAXIMUM_FRAME_SIZE => 0x100000, 11 | ::Protocol::HTTP2::Settings::INITIAL_WINDOW_SIZE => 0x800000, 12 | } 13 | 14 | Sync do 15 | endpoint = Async::HTTP::Endpoint.parse("http://localhost:8020") 16 | connection = endpoint.connect 17 | framer = Protocol::HTTP2::Framer.new(connection) 18 | client = Protocol::HTTP2::Client.new(framer) 19 | 20 | client.send_connection_preface(CLIENT_SETTINGS) 21 | 22 | ARGV.each do |path| 23 | stream = client.create_stream 24 | 25 | headers = [ 26 | [":scheme", endpoint.scheme], 27 | [":method", "GET"], 28 | [":authority", "localhost"], 29 | [":path", path], 30 | ["accept", "*/*"], 31 | ] 32 | 33 | stream.send_headers(nil, headers, Protocol::HTTP2::END_STREAM) 34 | 35 | def stream.process_headers(frame) 36 | headers = super 37 | Console.logger.info(self, "Received #{headers}") 38 | end 39 | 40 | def stream.process_data(frame) 41 | if data = super 42 | $stdout.write(data) 43 | end 44 | end 45 | 46 | until stream.closed? 47 | client.read_frame 48 | end 49 | end 50 | 51 | client.send_goaway 52 | end 53 | -------------------------------------------------------------------------------- /http-1.1/server.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'async' 4 | require 'async/io' 5 | require 'async/io/stream' 6 | require_relative '../files' 7 | 8 | Sync do 9 | endpoint = Async::IO::Endpoint.tcp('localhost', 8011) 10 | 11 | endpoint.accept do |connection| 12 | stream = Async::IO::Stream.new(connection) 13 | while request_line = stream.read_until("\r\n") 14 | method, path, version = request_line.split(/\s+/, 3) 15 | Console.logger.info(self, "Received #{method} #{path} #{version}") 16 | 17 | while line = stream.read_until("\r\n") 18 | break if line.empty? 19 | name, value = line.split(/:\s*/, 2) 20 | Console.logger.info(self, "Header #{name}: #{value}") 21 | end 22 | 23 | if file = FILES.get(path) 24 | Console.logger.info(self, "Serving #{path}") 25 | stream.write("HTTP/1.1 200 OK\r\n") 26 | stream.write("Content-Type: text/html\r\n") 27 | 28 | body = file.read 29 | 30 | stream.write("Content-Length: #{body.bytesize}\r\n") 31 | stream.write("\r\n") 32 | stream.write(body) 33 | else 34 | Console.logger.warn(self, "Could not find #{path}") 35 | stream.write("HTTP/1.1 404 Not Found\r\n") 36 | stream.write("Content-Length: 0\r\n") 37 | stream.write("\r\n") 38 | end 39 | 40 | stream.flush 41 | end 42 | rescue => error 43 | Console.logger.error(self, error) 44 | break 45 | ensure 46 | connection.close 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /http-2/server.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative '../files' 4 | 5 | require 'async' 6 | require 'async/io/stream' 7 | require 'async/http/endpoint' 8 | require 'protocol/http2/client' 9 | 10 | SERVER_SETTINGS = { 11 | ::Protocol::HTTP2::Settings::MAXIMUM_CONCURRENT_STREAMS => 128, 12 | ::Protocol::HTTP2::Settings::MAXIMUM_FRAME_SIZE => 0x100000, 13 | ::Protocol::HTTP2::Settings::INITIAL_WINDOW_SIZE => 0x800000, 14 | ::Protocol::HTTP2::Settings::ENABLE_CONNECT_PROTOCOL => 1, 15 | } 16 | 17 | Sync do 18 | endpoint = Async::HTTP::Endpoint.parse("http://localhost:8020") 19 | endpoint.accept do |connection| 20 | framer = Protocol::HTTP2::Framer.new(connection) 21 | server = Protocol::HTTP2::Server.new(framer) 22 | 23 | server.read_connection_preface(SERVER_SETTINGS) 24 | 25 | def server.accept_stream(stream_id) 26 | super.tap do |stream| 27 | def stream.process_headers(frame) 28 | headers = super.to_h 29 | Console.logger.info(self, "Received #{headers}") 30 | 31 | path = headers[':path'] 32 | 33 | if file = FILES.get(path) 34 | Console.logger.info(self, "Serving #{path}") 35 | self.send_headers(nil, [ 36 | [":status", "200"], 37 | ["content-type", "text/html"], 38 | ]) 39 | 40 | self.send_data(file.read, Protocol::HTTP2::END_STREAM) 41 | file.close 42 | else 43 | Console.logger.warn(self, "Could not find #{path}") 44 | self.send_headers(nil, [ 45 | [":status", "404"], 46 | ], Protocol::HTTP2::END_STREAM) 47 | end 48 | end 49 | end 50 | end 51 | 52 | while frame = server.read_frame 53 | break if server.closed? 54 | end 55 | end 56 | end 57 | --------------------------------------------------------------------------------