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 +4 -4
- data/.gitmodules +3 -0
- data/.rspec +0 -1
- data/.travis.yml +5 -6
- data/Gemfile +6 -1
- data/README.md +112 -2
- data/Rakefile +65 -0
- data/async-http.gemspec +15 -13
- data/lib/async/http.rb +0 -6
- data/lib/async/http/client.rb +59 -0
- data/lib/async/http/protocol.rb +21 -0
- data/lib/async/http/protocol/http10.rb +70 -0
- data/lib/async/http/protocol/http11.rb +182 -0
- data/lib/async/http/protocol/http1x.rb +65 -0
- data/lib/async/http/protocol/request.rb +31 -0
- data/lib/async/http/protocol/response.rb +43 -0
- data/lib/async/http/server.rb +45 -0
- data/lib/async/http/version.rb +1 -1
- metadata +28 -8
- data/bin/console +0 -14
- data/bin/setup +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e71a596dc1dee6c4eab53691aa115ca3131f7af6
|
4
|
+
data.tar.gz: 46ed7eb01476eb5e2296a81f4e3dccb9c6c42eba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4c978a492a58171e6fb5009af835aabf11ee44d9b1ca27dc4a003782210b51ced1d83fe9f1aef6fb916660fe92a63c009eb70db6a0a6af0381b93116447d7f5c
|
7
|
+
data.tar.gz: aa00d677dfb29f8ff334ee6d664e42c37cc8b31b75483bda223842f9245f94e91a2a1a56c54c30292fdc71bae4184250dbe605fb44532ce6f1a0f35fe0933521
|
data/.gitmodules
ADDED
data/.rspec
CHANGED
data/.travis.yml
CHANGED
@@ -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
data/README.md
CHANGED
@@ -1,11 +1,16 @@
|
|
1
1
|
# Async::HTTP
|
2
2
|
|
3
|
-
|
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
|
-
|
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
|
data/async-http.gemspec
CHANGED
@@ -2,23 +2,25 @@
|
|
2
2
|
require_relative 'lib/async/http/version'
|
3
3
|
|
4
4
|
Gem::Specification.new do |spec|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
11
|
-
|
10
|
+
spec.summary = ""
|
11
|
+
spec.homepage = "https://github.com/socketry/async-http"
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
19
|
+
spec.add_dependency("async-io", "~> 0.4")
|
20
20
|
|
21
|
-
spec.
|
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"
|
data/lib/async/http.rb
CHANGED
@@ -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
|
data/lib/async/http/server.rb
CHANGED
@@ -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
|
data/lib/async/http/version.rb
CHANGED
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.
|
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-
|
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.
|
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.
|
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.
|
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.
|
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
|
data/bin/console
DELETED
@@ -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__)
|