async-http 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 83321e752ca55286b8558d3135b5af2f367d8191
4
- data.tar.gz: aef2d3a44c36bd25e9bb8f389138b78f2b056ed6
3
+ metadata.gz: e71a596dc1dee6c4eab53691aa115ca3131f7af6
4
+ data.tar.gz: 46ed7eb01476eb5e2296a81f4e3dccb9c6c42eba
5
5
  SHA512:
6
- metadata.gz: 7777ced365d9b9d2fe40eea4944bec10c2b2cdc119d86f8e59aac047eb30b30a07ca1a1ae31e67e4f4e8984c08c55b1637954c4e5be6c2fb365af8b3676d786a
7
- data.tar.gz: a70e8f5f7198c6928227f2f3234fd3a59e30ba90510fee9d09dc64dbe919f6207fa37323eeca936d5cf737a9b4e75f619340ddc2e8f75e315b48e7b574532ed4
6
+ metadata.gz: 4c978a492a58171e6fb5009af835aabf11ee44d9b1ca27dc4a003782210b51ced1d83fe9f1aef6fb916660fe92a63c009eb70db6a0a6af0381b93116447d7f5c
7
+ data.tar.gz: aa00d677dfb29f8ff334ee6d664e42c37cc8b31b75483bda223842f9245f94e91a2a1a56c54c30292fdc71bae4184250dbe605fb44532ce6f1a0f35fe0933521
@@ -0,0 +1,3 @@
1
+ [submodule "ext/http-parser"]
2
+ path = ext/http-parser
3
+ url = https://github.com/nodejs/http-parser
data/.rspec CHANGED
@@ -1,4 +1,3 @@
1
- --color
2
1
  --format documentation
3
2
  --warnings
4
3
  --require spec_helper
@@ -2,18 +2,17 @@ language: ruby
2
2
  sudo: false
3
3
  dist: trusty
4
4
  cache: bundler
5
- addons:
6
- apt:
7
- packages:
8
- - bind9
9
5
  rvm:
6
+ - 2.0
10
7
  - 2.1
11
8
  - 2.2
12
9
  - 2.3
13
10
  - 2.4
14
- - ruby-head
15
11
  - jruby-head
12
+ - ruby-head
13
+ - rbx-3
16
14
  matrix:
17
15
  allow_failures:
18
16
  - rvm: ruby-head
19
- - rvm: jruby-head
17
+ - rvm: jruby-head
18
+ - rvm: rbx-3
data/Gemfile CHANGED
@@ -1,4 +1,9 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in async-http.gemspec
3
+ # Specify your gem's dependencies in async-io.gemspec
4
4
  gemspec
5
+
6
+ group :test do
7
+ gem 'simplecov'
8
+ gem 'coveralls', require: false
9
+ end
data/README.md CHANGED
@@ -1,11 +1,16 @@
1
1
  # Async::HTTP
2
2
 
3
- Asynchornous HTTP Client/Server. Currently unimplemented.
3
+ A multi-process, multi-fiber HTTP client/server built on top of [async] and [async-io]. Each request is run within a light weight fiber and can block on up-stream requests without stalling the entire server process.
4
+
5
+ *This code is a proof-of-concept and not intended (yet) for production use.* The entire stack requires more scrutiny both in terms of performance and security.
4
6
 
5
7
  [![Build Status](https://secure.travis-ci.org/socketry/async-http.svg)](http://travis-ci.org/socketry/async-http)
6
8
  [![Code Climate](https://codeclimate.com/github/socketry/async-http.svg)](https://codeclimate.com/github/socketry/async-http)
7
9
  [![Coverage Status](https://coveralls.io/repos/socketry/async-http/badge.svg)](https://coveralls.io/r/socketry/async-http)
8
10
 
11
+ [async]: https://github.com/socketry/async
12
+ [async-io]: https://github.com/socketry/async-io
13
+
9
14
  ## Installation
10
15
 
11
16
  Add this line to your application's Gemfile:
@@ -24,7 +29,112 @@ Or install it yourself as:
24
29
 
25
30
  ## Usage
26
31
 
27
- TODO: Write usage instructions here
32
+ Here is a basic example of a client/server running in the same reactor:
33
+
34
+ ```ruby
35
+ require 'async/http/server'
36
+ require 'async/http/client'
37
+ require 'async/reactor'
38
+
39
+ server_addresses = [
40
+ Async::IO::Address.tcp('127.0.0.1', 9294, reuse_port: true)
41
+ ]
42
+
43
+ app = lambda do |env|
44
+ [200, {}, ["Hello World"]]
45
+ end
46
+
47
+ server = Async::HTTP::Server.new(server_addresses, app)
48
+ client = Async::HTTP::Client.new(server_addresses)
49
+
50
+ Async::Reactor.run do |task|
51
+ server_task = task.async do
52
+ server.run
53
+ end
54
+
55
+ response = client.get("/", {})
56
+ puts response.body
57
+
58
+ server_task.stop
59
+ end
60
+ ```
61
+
62
+ ## Performance
63
+
64
+ On a 4-core 8-thread i7, running `ab` which uses discrete (non-keep-alive) connections:
65
+
66
+ ```
67
+ $ ab -c 8 -t 10 http://127.0.0.1:9294/
68
+ This is ApacheBench, Version 2.3 <$Revision: 1757674 $>
69
+ Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
70
+ Licensed to The Apache Software Foundation, http://www.apache.org/
71
+
72
+ Benchmarking 127.0.0.1 (be patient)
73
+ Completed 5000 requests
74
+ Completed 10000 requests
75
+ Completed 15000 requests
76
+ Completed 20000 requests
77
+ Completed 25000 requests
78
+ Completed 30000 requests
79
+ Completed 35000 requests
80
+ Completed 40000 requests
81
+ Completed 45000 requests
82
+ Completed 50000 requests
83
+ Finished 50000 requests
84
+
85
+
86
+ Server Software:
87
+ Server Hostname: 127.0.0.1
88
+ Server Port: 9294
89
+
90
+ Document Path: /
91
+ Document Length: 13 bytes
92
+
93
+ Concurrency Level: 8
94
+ Time taken for tests: 1.869 seconds
95
+ Complete requests: 50000
96
+ Failed requests: 0
97
+ Total transferred: 2450000 bytes
98
+ HTML transferred: 650000 bytes
99
+ Requests per second: 26755.55 [#/sec] (mean)
100
+ Time per request: 0.299 [ms] (mean)
101
+ Time per request: 0.037 [ms] (mean, across all concurrent requests)
102
+ Transfer rate: 1280.29 [Kbytes/sec] received
103
+
104
+ Connection Times (ms)
105
+ min mean[+/-sd] median max
106
+ Connect: 0 0 0.0 0 0
107
+ Processing: 0 0 0.2 0 6
108
+ Waiting: 0 0 0.2 0 6
109
+ Total: 0 0 0.2 0 6
110
+
111
+ Percentage of the requests served within a certain time (ms)
112
+ 50% 0
113
+ 66% 0
114
+ 75% 0
115
+ 80% 0
116
+ 90% 0
117
+ 95% 1
118
+ 98% 1
119
+ 99% 1
120
+ 100% 6 (longest request)
121
+ ```
122
+
123
+ On a 4-core 8-thread i7, running `wrk`, which uses 8 keep-alive connections:
124
+
125
+ ```
126
+ $ wrk -c 8 -d 10 -t 8 http://127.0.0.1:9294/
127
+ Running 10s test @ http://127.0.0.1:9294/
128
+ 8 threads and 8 connections
129
+ Thread Stats Avg Stdev Max +/- Stdev
130
+ Latency 217.69us 0.99ms 23.21ms 97.39%
131
+ Req/Sec 12.18k 1.58k 17.67k 83.21%
132
+ 974480 requests in 10.10s, 60.41MB read
133
+ Requests/sec: 96485.00
134
+ Transfer/sec: 5.98MB
135
+ ```
136
+
137
+ According to these results, the cost of handling connections is quite high, while general throughput seems pretty decent.
28
138
 
29
139
  ## Contributing
30
140
 
data/Rakefile CHANGED
@@ -4,3 +4,68 @@ require "rspec/core/rake_task"
4
4
  RSpec::Core::RakeTask.new(:test)
5
5
 
6
6
  task :default => :test
7
+
8
+ task :server do
9
+ require 'async/reactor'
10
+ require 'async/http/server'
11
+
12
+ app = lambda do |env|
13
+ [200, {}, ["Hello World"]]
14
+ end
15
+
16
+ server = Async::HTTP::Server.new([
17
+ Async::IO::Address.tcp('127.0.0.1', 9294, reuse_port: true)
18
+ ], app)
19
+
20
+ Async::Reactor.run do
21
+ server.run
22
+ end
23
+ end
24
+
25
+ task :client do
26
+ require 'async/reactor'
27
+ require 'async/http/client'
28
+
29
+ client = Async::HTTP::Client.new([
30
+ Async::IO::Address.tcp('127.0.0.1', 9294, reuse_port: true)
31
+ ])
32
+
33
+ Async::Reactor.run do
34
+ response = client.get("/")
35
+
36
+ puts response.inspect
37
+ end
38
+ end
39
+
40
+ task :wrk do
41
+ require 'async/reactor'
42
+ require 'async/http/server'
43
+
44
+ app = lambda do |env|
45
+ [200, {}, ["Hello World"]]
46
+ end
47
+
48
+ server = Async::HTTP::Server.new([
49
+ Async::IO::Address.tcp('127.0.0.1', 9294, reuse_port: true)
50
+ ], app)
51
+
52
+ process_count = Etc.nprocessors
53
+
54
+ pids = process_count.times.collect do
55
+ fork do
56
+ Async::Reactor.run do
57
+ server.run
58
+ end
59
+ end
60
+ end
61
+
62
+ url = "http://127.0.0.1:9294/"
63
+
64
+ connections = process_count
65
+ system("wrk", "-c", connections.to_s, "-d", "2", "-t", connections.to_s, url)
66
+
67
+ pids.each do |pid|
68
+ Process.kill(:KILL, pid)
69
+ Process.wait pid
70
+ end
71
+ end
@@ -2,23 +2,25 @@
2
2
  require_relative 'lib/async/http/version'
3
3
 
4
4
  Gem::Specification.new do |spec|
5
- spec.name = "async-http"
6
- spec.version = Async::HTTP::VERSION
7
- spec.authors = ["Samuel Williams"]
8
- spec.email = ["samuel.williams@oriontransfer.co.nz"]
5
+ spec.name = "async-http"
6
+ spec.version = Async::HTTP::VERSION
7
+ spec.authors = ["Samuel Williams"]
8
+ spec.email = ["samuel.williams@oriontransfer.co.nz"]
9
9
 
10
- spec.summary = ""
11
- spec.homepage = "https://github.com/socketry/async-http"
10
+ spec.summary = ""
11
+ spec.homepage = "https://github.com/socketry/async-http"
12
12
 
13
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
14
- f.match(%r{^(test|spec|features)/})
15
- end
16
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
17
- spec.require_paths = ["lib"]
13
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
14
+ f.match(%r{^(test|spec|features)/})
15
+ end
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.require_paths = ["lib"]
18
18
 
19
- spec.add_dependency("async-io", "~> 0.1")
19
+ spec.add_dependency("async-io", "~> 0.4")
20
20
 
21
- spec.add_development_dependency "async-rspec", "~> 1.0"
21
+ spec.add_dependency('samovar', "~> 1.3")
22
+
23
+ spec.add_development_dependency "async-rspec", "~> 1.1"
22
24
 
23
25
  spec.add_development_dependency "bundler", "~> 1.3"
24
26
  spec.add_development_dependency "rspec", "~> 3.6"
@@ -19,9 +19,3 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  require "async/http/version"
22
-
23
- module Async
24
- module HTTP
25
- # Your code goes here...
26
- end
27
- end
@@ -0,0 +1,59 @@
1
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'async/io/address'
22
+
23
+ require_relative 'protocol'
24
+
25
+ module Async
26
+ module HTTP
27
+ class Client
28
+ def initialize(addresses, protocol_class = Protocol::HTTP11)
29
+ @addresses = addresses
30
+
31
+ @protocol_class = protocol_class
32
+ end
33
+
34
+ GET = 'GET'.freeze
35
+
36
+ def get(path, headers = {})
37
+ connect do |protocol|
38
+ protocol.send_request(GET, path, headers)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def connect
45
+ Async::IO::Address.each(@addresses) do |address|
46
+ # puts "Connecting to #{address} on process #{Process.pid}"
47
+
48
+ address.connect do |peer|
49
+ stream = Async::IO::Stream.new(peer)
50
+
51
+ # We only yield for first successful connection.
52
+
53
+ return yield @protocol_class.new(stream)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,21 @@
1
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'protocol/http1x'
@@ -0,0 +1,70 @@
1
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'http11'
22
+
23
+ require_relative 'request'
24
+ require_relative 'response'
25
+
26
+ module Async
27
+ module HTTP
28
+ module Protocol
29
+ # Implements basic HTTP/1.1 request/response.
30
+ class HTTP10 < HTTP11
31
+ VERSION = "HTTP/1.0".freeze
32
+
33
+ def version
34
+ VERSION
35
+ end
36
+
37
+ def keep_alive?(headers)
38
+ headers[HTTP_CONNECTION] == KEEP_ALIVE
39
+ end
40
+
41
+ # Server loop.
42
+ def receive_requests
43
+ while request = Request.new(*self.read_request)
44
+ status, headers, body = yield request
45
+
46
+ write_response(request.version, status, headers, body)
47
+
48
+ break unless keep_alive?(request.headers) && keep_alive?(headers)
49
+ end
50
+ end
51
+
52
+ def write_body(body, chunked = true)
53
+ buffer = String.new
54
+ body.each{|chunk| buffer << chunk}
55
+
56
+ @stream.write("Content-Length: #{buffer.bytesize}\r\n\r\n")
57
+ @stream.write(buffer)
58
+ end
59
+
60
+ def read_body(headers)
61
+ if content_length = headers[HTTP_CONTENT_LENGTH]
62
+ return @stream.read(Integer(content_length))
63
+ # elsif !keep_alive?(headers)
64
+ # return @stream.read
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,182 @@
1
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'async/io/protocol/line'
22
+
23
+ require_relative 'request'
24
+ require_relative 'response'
25
+
26
+ module Async
27
+ module HTTP
28
+ module Protocol
29
+ # Implements basic HTTP/1.1 request/response.
30
+ class HTTP11 < Async::IO::Protocol::Line
31
+ HTTP_CONTENT_LENGTH = 'HTTP_CONTENT_LENGTH'.freeze
32
+ HTTP_TRANSFER_ENCODING = 'HTTP_TRANSFER_ENCODING'.freeze
33
+
34
+ CRLF = "\r\n".freeze
35
+
36
+ def initialize(stream)
37
+ super(stream, CRLF)
38
+ end
39
+
40
+ HTTP_CONNECTION = 'HTTP_CONNECTION'.freeze
41
+ KEEP_ALIVE = 'keep-alive'.freeze
42
+ CLOSE = 'close'.freeze
43
+
44
+ VERSION = "HTTP/1.1".freeze
45
+
46
+ def version
47
+ VERSION
48
+ end
49
+
50
+ def keep_alive?(headers)
51
+ headers[HTTP_CONNECTION] != CLOSE
52
+ end
53
+
54
+ # Server loop.
55
+ def receive_requests
56
+ while request = Request.new(*read_request)
57
+ status, headers, body = yield request
58
+
59
+ write_response(request.version, status, headers, body)
60
+
61
+ break unless keep_alive?(request.headers) && keep_alive?(headers)
62
+ end
63
+
64
+ rescue EOFError, Errno::ECONNRESET
65
+ return nil
66
+ end
67
+
68
+ # Client request.
69
+ def send_request(method, path, headers, body = [])
70
+ write_request(method, path, version, headers, body)
71
+
72
+ return Response.new(*read_response)
73
+
74
+ rescue EOFError
75
+ return nil
76
+ end
77
+
78
+ def write_request(method, path, version, headers, body)
79
+ @stream.write("#{method} #{path} #{version}\r\n")
80
+ write_headers(headers)
81
+ write_body(body)
82
+
83
+ @stream.flush
84
+
85
+ return true
86
+ end
87
+
88
+ def read_response
89
+ version, status, reason = read_line.split(/\s+/, 3)
90
+ headers = read_headers
91
+ body = read_body(headers)
92
+
93
+ return version, Integer(status), reason, headers, body
94
+ end
95
+
96
+ def read_request
97
+ method, path, version = read_line.split(/\s+/, 3)
98
+ headers = read_headers
99
+ body = read_body(headers)
100
+
101
+ return method, path, version, headers, body
102
+ end
103
+
104
+ def write_response(version, status, headers, body)
105
+ @stream.write("#{version} #{status}\r\n")
106
+ write_headers(headers)
107
+ write_body(body)
108
+
109
+ @stream.flush
110
+
111
+ return true
112
+ end
113
+
114
+ protected
115
+
116
+ def write_headers(headers)
117
+ headers.each do |name, value|
118
+ @stream.write("#{name}: #{value}\r\n")
119
+ end
120
+ end
121
+
122
+ def read_headers(headers = {})
123
+ # Parsing headers:
124
+ each_line do |line|
125
+ if line =~ /^([a-zA-Z\-]+):\s*(.+?)\s*$/
126
+ headers["HTTP_#{$1.tr('-', '_').upcase}"] = $2
127
+ else
128
+ break
129
+ end
130
+ end
131
+
132
+ return headers
133
+ end
134
+
135
+ def write_body(body, chunked = true)
136
+ if chunked
137
+ @stream.write("Transfer-Encoding: chunked\r\n\r\n")
138
+
139
+ body.each do |chunk|
140
+ next if chunk.size == 0
141
+
142
+ @stream.write("#{chunk.bytesize.to_s(16).upcase}\r\n")
143
+ @stream.write(chunk)
144
+ @stream.write(CRLF)
145
+ end
146
+
147
+ @stream.write("0\r\n\r\n")
148
+ else
149
+ buffer = String.new
150
+ body.each{|chunk| buffer << chunk}
151
+
152
+ @stream.write("Content-Length: #{chunk.bytesize}\r\n\r\n")
153
+ @stream.write(chunk)
154
+ end
155
+ end
156
+
157
+ def read_body(headers)
158
+ if headers[HTTP_TRANSFER_ENCODING] == 'chunked'
159
+ buffer = Async::IO::BinaryString.new
160
+
161
+ while true
162
+ size = read_line.to_i(16)
163
+
164
+ if size == 0
165
+ read_line
166
+ break
167
+ end
168
+
169
+ buffer << @stream.read(size)
170
+
171
+ read_line # Consume the trailing CRLF
172
+ end
173
+
174
+ return buffer
175
+ elsif content_length = headers[HTTP_CONTENT_LENGTH]
176
+ return @stream.read(Integer(content_length))
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,65 @@
1
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'http10'
22
+ require_relative 'http11'
23
+
24
+ module Async
25
+ module HTTP
26
+ module Protocol
27
+ # A server that supports both HTTP1.0 and HTTP1.1 semantics by detecting the version of the request.
28
+ class HTTP1x < Async::IO::Protocol::Line
29
+ HANDLERS = {
30
+ "HTTP/1.0" => HTTP10,
31
+ "HTTP/1.1" => HTTP11,
32
+ }
33
+
34
+ def initialize(stream, handlers: HANDLERS)
35
+ super(stream, HTTP11::CRLF)
36
+
37
+ @handlers = handlers
38
+
39
+ @handler = nil
40
+ end
41
+
42
+ def create_handler(version)
43
+ if klass = @handlers[version]
44
+ klass.new(@stream)
45
+ else
46
+ raise RuntimeError, "Unsupported protocol version #{version}"
47
+ end
48
+ end
49
+
50
+ def receive_requests(&block)
51
+ method, path, version = self.peek_line.split(/\s+/, 3)
52
+
53
+ create_handler(version).receive_requests(&block)
54
+
55
+ rescue EOFError, Errno::ECONNRESET
56
+ return nil
57
+ end
58
+
59
+ def send_request(request, &block)
60
+ create_handler(request.version).send_request(request, &block)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,31 @@
1
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ module Async
22
+ module HTTP
23
+ module Protocol
24
+ class Request < Struct.new(:method, :path, :version, :headers, :body)
25
+ def read
26
+ @body
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,43 @@
1
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ module Async
22
+ module HTTP
23
+ module Protocol
24
+ class Response < Struct.new(:version, :status, :reason, :headers, :body)
25
+ def continue?
26
+ status == 100
27
+ end
28
+
29
+ def success?
30
+ status >= 200 && status < 300
31
+ end
32
+
33
+ def redirection?
34
+ status >= 300 && status < 400
35
+ end
36
+
37
+ def failure?
38
+ status >= 400 && status < 600
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -17,3 +17,48 @@
17
17
  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  # THE SOFTWARE.
20
+
21
+ require 'async/io/address'
22
+
23
+ require_relative 'protocol'
24
+
25
+ module Async
26
+ module HTTP
27
+ class Server
28
+ def initialize(addresses, protocol_class = Protocol::HTTP1x)
29
+ @addresses = addresses
30
+ @protocol_class = protocol_class
31
+ end
32
+
33
+ def handle_request(request, peer, address)
34
+ [200, {}, []]
35
+ end
36
+
37
+ def run
38
+ Async::IO::Address.each(@addresses) do |address|
39
+ # puts "Binding to #{address} on process #{Process.pid}"
40
+
41
+ address.accept do |peer|
42
+ stream = Async::IO::Stream.new(peer)
43
+
44
+ protocol = @protocol_class.new(stream)
45
+
46
+ # puts "Opening session on child pid #{Process.pid}"
47
+
48
+ hijack = catch(:hijack) do
49
+ protocol.receive_requests do |request|
50
+ handle_request(request, peer, address)
51
+ end
52
+ end
53
+
54
+ if hijack
55
+ hijack.call
56
+ end
57
+
58
+ # puts "Closing session"
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -20,6 +20,6 @@
20
20
 
21
21
  module Async
22
22
  module HTTP
23
- VERSION = "0.0.0"
23
+ VERSION = "0.2.0"
24
24
  end
25
25
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-http
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-05-25 00:00:00.000000000 Z
11
+ date: 2017-06-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async-io
@@ -16,28 +16,42 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0.1'
19
+ version: '0.4'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0.1'
26
+ version: '0.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: samovar
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.3'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: async-rspec
29
43
  requirement: !ruby/object:Gem::Requirement
30
44
  requirements:
31
45
  - - "~>"
32
46
  - !ruby/object:Gem::Version
33
- version: '1.0'
47
+ version: '1.1'
34
48
  type: :development
35
49
  prerelease: false
36
50
  version_requirements: !ruby/object:Gem::Requirement
37
51
  requirements:
38
52
  - - "~>"
39
53
  - !ruby/object:Gem::Version
40
- version: '1.0'
54
+ version: '1.1'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: bundler
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -88,15 +102,21 @@ extensions: []
88
102
  extra_rdoc_files: []
89
103
  files:
90
104
  - ".gitignore"
105
+ - ".gitmodules"
91
106
  - ".rspec"
92
107
  - ".travis.yml"
93
108
  - Gemfile
94
109
  - README.md
95
110
  - Rakefile
96
111
  - async-http.gemspec
97
- - bin/console
98
- - bin/setup
99
112
  - lib/async/http.rb
113
+ - lib/async/http/client.rb
114
+ - lib/async/http/protocol.rb
115
+ - lib/async/http/protocol/http10.rb
116
+ - lib/async/http/protocol/http11.rb
117
+ - lib/async/http/protocol/http1x.rb
118
+ - lib/async/http/protocol/request.rb
119
+ - lib/async/http/protocol/response.rb
100
120
  - lib/async/http/server.rb
101
121
  - lib/async/http/version.rb
102
122
  homepage: https://github.com/socketry/async-http
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "bundler/setup"
4
- require "async/http"
5
-
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
13
- require "irb"
14
- IRB.start(__FILE__)
data/bin/setup DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here