h1p 0.2 → 0.3
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 +4 -4
- data/CHANGELOG.md +4 -1
- data/Gemfile.lock +1 -1
- data/README.md +28 -14
- data/Rakefile +1 -1
- data/benchmarks/bm_http1_parser.rb +1 -1
- data/benchmarks/pipelined.rb +101 -0
- data/examples/callable.rb +1 -1
- data/examples/http_server.rb +2 -2
- data/ext/h1p/h1p.c +340 -196
- data/ext/h1p/limits.rb +7 -6
- data/lib/h1p/version.rb +1 -1
- data/test/run.rb +5 -0
- data/test/test_h1p_client.rb +532 -0
- data/test/{test_h1p.rb → test_h1p_server.rb} +36 -36
- metadata +11 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3c4b5a2d00d0bb28e75e75a1bb5c5c0bf4ef060d837e33606408d92bcb70deca
|
4
|
+
data.tar.gz: 73332efbff1047e8f5e3a65b6a718a187b332b405f943f6d53fc9acdf6b55b7e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5b1616a34cf1f43c1dece307cabccd882f4805824a54e58a70e476fc37db0738542f260cf22a88a8c8d7e8fb310cdc3a67d3f4e50a22285ecdd4a08b43fbb46c
|
7
|
+
data.tar.gz: ca10eb8b6c764f15c5f816068e84719cb7b242d6e5cd521fb941995cc2b8fca7c48abe5e0791ba0d06d7331a3601adbf860373a550a132a13fb073c6684c66f8
|
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# H1P - a blocking HTTP/1 parser for Ruby
|
2
2
|
|
3
3
|
[](http://rubygems.org/gems/h1p)
|
4
|
-
[](https://github.com/digital-fabric/h1p/actions?query=workflow%3ATests)
|
5
5
|
[](https://github.com/digital-fabric/h1p/blob/master/LICENSE)
|
6
6
|
|
7
7
|
H1P is a blocking/synchronous HTTP/1 parser for Ruby with a simple and intuitive
|
@@ -23,6 +23,7 @@ The H1P was originally written as part of
|
|
23
23
|
- Simple, blocking/synchronous API
|
24
24
|
- Zero dependencies
|
25
25
|
- Transport-agnostic
|
26
|
+
- Parses both HTTP request and HTTP response
|
26
27
|
- Support for chunked encoding
|
27
28
|
- Support for both `LF` and `CRLF` line breaks
|
28
29
|
- Track total incoming traffic
|
@@ -41,15 +42,21 @@ You can then run `bundle install` to install it. Otherwise, just run `gem instal
|
|
41
42
|
|
42
43
|
## Usage
|
43
44
|
|
44
|
-
Start by creating an instance of H1P::Parser
|
45
|
+
Start by creating an instance of `H1P::Parser`, passing a connection instance and the parsing mode:
|
45
46
|
|
46
47
|
```ruby
|
47
48
|
require 'h1p'
|
48
49
|
|
49
|
-
parser = H1P::Parser.new(conn)
|
50
|
+
parser = H1P::Parser.new(conn, :server)
|
50
51
|
```
|
51
52
|
|
52
|
-
|
53
|
+
In order to parse HTTP responses, change the mode to `:client`:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
parser = H1P::Parser.new(conn, :client)
|
57
|
+
```
|
58
|
+
|
59
|
+
To read the next message from the connection, call `#parse_headers`:
|
53
60
|
|
54
61
|
```ruby
|
55
62
|
loop do
|
@@ -65,13 +72,21 @@ headers. In case the client has closed the connection, `#parse_headers` will
|
|
65
72
|
return `nil` (see the guard clause above).
|
66
73
|
|
67
74
|
In addition to the header keys and values, the resulting hash also contains the
|
68
|
-
following "pseudo-headers":
|
75
|
+
following "pseudo-headers" (in server mode):
|
69
76
|
|
70
77
|
- `:method`: the HTTP method (in upper case)
|
71
78
|
- `:path`: the request target
|
72
79
|
- `:protocol`: the protocol used (either `'http/1.0'` or `'http/1.1'`)
|
73
80
|
- `:rx`: the total bytes read by the parser
|
74
81
|
|
82
|
+
In client mode, the following pseudo-headers will be present:
|
83
|
+
|
84
|
+
- `:protocol`: the protocol used (either `'http/1.0'` or `'http/1.1'`)
|
85
|
+
- `:status': the HTTP status as an integer
|
86
|
+
- `:status_message`: the HTTP status message
|
87
|
+
- `:rx`: the total bytes read by the parser
|
88
|
+
|
89
|
+
|
75
90
|
The header keys are always lower-cased. Consider the following HTTP request:
|
76
91
|
|
77
92
|
```
|
@@ -101,24 +116,24 @@ where the value is an array containing the corresponding values. For example,
|
|
101
116
|
multiple `Cookie` headers will appear in the hash as a single `"cookie"` entry,
|
102
117
|
e.g. `{ "cookie" => ['a=1', 'b=2'] }`
|
103
118
|
|
104
|
-
### Handling of invalid
|
119
|
+
### Handling of invalid message
|
105
120
|
|
106
|
-
When an invalid
|
107
|
-
exception. An incoming
|
108
|
-
has been encountered at any point in parsing the
|
121
|
+
When an invalid message is encountered, the parser will raise a `H1P::Error`
|
122
|
+
exception. An incoming message may be considered invalid if an invalid character
|
123
|
+
has been encountered at any point in parsing the message, or if any of the
|
109
124
|
tokens have an invalid length. You can consult the limits used by the parser
|
110
125
|
[here](https://github.com/digital-fabric/h1p/blob/main/ext/h1p/limits.rb).
|
111
126
|
|
112
|
-
### Reading the
|
127
|
+
### Reading the message body
|
113
128
|
|
114
|
-
To read the
|
129
|
+
To read the message body use `#read_body`:
|
115
130
|
|
116
131
|
```ruby
|
117
132
|
# read entire body
|
118
133
|
body = parser.read_body
|
119
134
|
```
|
120
135
|
|
121
|
-
The H1P parser knows how to read both
|
136
|
+
The H1P parser knows how to read both message bodies with a specified
|
122
137
|
`Content-Length` and request bodies in chunked encoding. The method call will
|
123
138
|
return when the entire body has been read. If the body is incomplete or has
|
124
139
|
invalid formatting, the parser will raise a `H1P::Error` exception.
|
@@ -211,8 +226,7 @@ performance.
|
|
211
226
|
Here are some of the features and enhancements planned for H1P:
|
212
227
|
|
213
228
|
- Add conformance and security tests
|
214
|
-
- Add ability to
|
215
|
-
- Add ability to splice the request body into an arbitrary fd
|
229
|
+
- Add ability to splice the message body into an arbitrary fd
|
216
230
|
(Polyphony-specific)
|
217
231
|
- Improve performance
|
218
232
|
|
data/Rakefile
CHANGED
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
HTTP_REQUEST = "GET /foo HTTP/1.1\r\nHost: example.com\r\nAccept: */*\r\nUser-Agent: foobar\r\n\r\n" +
|
4
|
+
"GET /bar HTTP/1.1\r\nHost: example.com\r\nAccept: */*\r\nUser-Agent: foobar\r\n\r\n"
|
5
|
+
|
6
|
+
def measure_time_and_allocs
|
7
|
+
4.times { GC.start }
|
8
|
+
GC.disable
|
9
|
+
|
10
|
+
t0 = Time.now
|
11
|
+
a0 = object_count
|
12
|
+
yield
|
13
|
+
t1 = Time.now
|
14
|
+
a1 = object_count
|
15
|
+
[t1 - t0, a1 - a0]
|
16
|
+
ensure
|
17
|
+
GC.enable
|
18
|
+
end
|
19
|
+
|
20
|
+
def object_count
|
21
|
+
count = ObjectSpace.count_objects
|
22
|
+
count[:TOTAL] - count[:FREE]
|
23
|
+
end
|
24
|
+
|
25
|
+
def benchmark_other_http1_parser(iterations)
|
26
|
+
STDOUT << "http_parser.rb: "
|
27
|
+
require 'http_parser.rb'
|
28
|
+
|
29
|
+
i, o = IO.pipe
|
30
|
+
parser = Http::Parser.new
|
31
|
+
done = false
|
32
|
+
queue = nil
|
33
|
+
rx = 0
|
34
|
+
req_count = 0
|
35
|
+
parser.on_headers_complete = proc do |h|
|
36
|
+
h[':method'] = parser.http_method
|
37
|
+
h[':path'] = parser.request_url
|
38
|
+
h[':rx'] = rx
|
39
|
+
queue << h
|
40
|
+
end
|
41
|
+
parser.on_message_complete = proc { done = true }
|
42
|
+
|
43
|
+
writer = Thread.new do
|
44
|
+
iterations.times { o << HTTP_REQUEST }
|
45
|
+
o.close
|
46
|
+
end
|
47
|
+
|
48
|
+
elapsed, allocated = measure_time_and_allocs do
|
49
|
+
queue = []
|
50
|
+
done = false
|
51
|
+
rx = 0
|
52
|
+
loop do
|
53
|
+
data = i.readpartial(4096) rescue nil
|
54
|
+
break unless data
|
55
|
+
|
56
|
+
rx += data.bytesize
|
57
|
+
parser << data
|
58
|
+
while (req = queue.shift)
|
59
|
+
req_count += 1
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
puts(format('count: %d, elapsed: %f, allocated: %d (%f/req), rate: %f ips', req_count, elapsed, allocated, allocated.to_f / iterations, iterations / elapsed))
|
64
|
+
end
|
65
|
+
|
66
|
+
def benchmark_h1p_parser(iterations)
|
67
|
+
STDOUT << "H1P parser: "
|
68
|
+
require_relative '../lib/h1p'
|
69
|
+
i, o = IO.pipe
|
70
|
+
parser = H1P::Parser.new(i)
|
71
|
+
req_count = 0
|
72
|
+
|
73
|
+
writer = Thread.new do
|
74
|
+
iterations.times { o << HTTP_REQUEST }
|
75
|
+
o.close
|
76
|
+
end
|
77
|
+
|
78
|
+
elapsed, allocated = measure_time_and_allocs do
|
79
|
+
while (headers = parser.parse_headers)
|
80
|
+
req_count += 1
|
81
|
+
end
|
82
|
+
end
|
83
|
+
puts(format('count: %d, elapsed: %f, allocated: %d (%f/req), rate: %f ips', req_count, elapsed, allocated, allocated.to_f / iterations, iterations / elapsed))
|
84
|
+
end
|
85
|
+
|
86
|
+
def fork_benchmark(method, iterations)
|
87
|
+
pid = fork do
|
88
|
+
send(method, iterations)
|
89
|
+
rescue Exception => e
|
90
|
+
p e
|
91
|
+
p e.backtrace
|
92
|
+
exit!
|
93
|
+
end
|
94
|
+
Process.wait(pid)
|
95
|
+
end
|
96
|
+
|
97
|
+
x = 100000
|
98
|
+
fork_benchmark(:benchmark_other_http1_parser, x)
|
99
|
+
fork_benchmark(:benchmark_h1p_parser, x)
|
100
|
+
|
101
|
+
# benchmark_h1p_parser(x)
|
data/examples/callable.rb
CHANGED
data/examples/http_server.rb
CHANGED
@@ -10,13 +10,13 @@ trap('SIGINT') { exit! }
|
|
10
10
|
|
11
11
|
def handle_client(conn)
|
12
12
|
Thread.new do
|
13
|
-
parser = H1P::Parser.new(conn)
|
13
|
+
parser = H1P::Parser.new(conn, :server)
|
14
14
|
loop do
|
15
15
|
headers = parser.parse_headers
|
16
16
|
break unless headers
|
17
17
|
|
18
18
|
req_body = parser.read_body
|
19
|
-
|
19
|
+
|
20
20
|
p headers: headers
|
21
21
|
p body: req_body
|
22
22
|
|