async-http 0.0.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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