soba 0.1.0.pre → 0.1.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 +4 -4
- data/Gemfile.lock +7 -3
- data/README.md +29 -3
- data/benchmark/README.md +69 -0
- data/examples/config.ru +14 -0
- data/lib/rack/handler/soba.rb +40 -0
- data/lib/soba.rb +0 -1
- data/lib/soba/cli.rb +11 -0
- data/lib/soba/const.rb +217 -0
- data/lib/soba/monkey.rb +16 -0
- data/lib/soba/parser.rb +61 -0
- data/lib/soba/rack_default.rb +7 -0
- data/lib/soba/server.rb +134 -11
- data/lib/soba/version.rb +1 -1
- data/soba.gemspec +5 -3
- metadata +45 -9
- data/bin/soba +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bf5254ef757d85ce6742c3ae2f6ca79b90edf225
|
4
|
+
data.tar.gz: 58c573c317ad913d7d575346ccfcaecb4da04eef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7e7139df4df2af1f60a02f055d15479d935f236b31c41280a62394dab5d18422d5f43850688cadb03166ba191d8026d02cd7655d5ce0f0b5eb50718c08308689
|
7
|
+
data.tar.gz: 1bd8204f7a8706890870f306aa9f09f694d83ae5e07417d4dbf3adc8dee23d3957d38bef6d7b521ccac256839f80b20806f7179eca036d90d9525177c0bbb5dc
|
data/Gemfile.lock
CHANGED
@@ -1,16 +1,20 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
soba (0.1.0
|
5
|
-
lightio (~> 0.4)
|
4
|
+
soba (0.1.0)
|
5
|
+
lightio (~> 0.4.3)
|
6
|
+
pico_http_parser (~> 0.0)
|
7
|
+
rack
|
6
8
|
|
7
9
|
GEM
|
8
10
|
remote: https://rubygems.org/
|
9
11
|
specs:
|
10
12
|
diff-lcs (1.3)
|
11
|
-
lightio (0.4.
|
13
|
+
lightio (0.4.3)
|
12
14
|
nio4r (~> 2.2)
|
13
15
|
nio4r (2.2.0)
|
16
|
+
pico_http_parser (0.0.4)
|
17
|
+
rack (2.0.4)
|
14
18
|
rake (10.5.0)
|
15
19
|
rspec (3.7.0)
|
16
20
|
rspec-core (~> 3.7.0)
|
data/README.md
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
# Soba
|
2
2
|
|
3
|
-
|
3
|
+
Soba is an experimental rack server, which build upon green thread.
|
4
4
|
|
5
|
-
|
5
|
+
Green thread can easily achieve high concurrent(especially for IO-heavy app), and save the costs of native threads.
|
6
|
+
|
7
|
+
Soba use [LightIO](https://github.com/socketry/lightio) to provision green threads.
|
6
8
|
|
7
9
|
## Installation
|
8
10
|
|
@@ -22,7 +24,31 @@ Or install it yourself as:
|
|
22
24
|
|
23
25
|
## Usage
|
24
26
|
|
25
|
-
|
27
|
+
This is a example of soba with rack, put these content into `config.ru`, then run `rackup` to start soba.
|
28
|
+
|
29
|
+
Notice:
|
30
|
+
> 1. Even in rails, soba should be started through `rackup`, so we can apply monkey patch before other code loaded.
|
31
|
+
> 2. In `config.ru`, the commented first line is rackup arguments.
|
32
|
+
|
33
|
+
``` ruby
|
34
|
+
#\ -s soba -O Port=3000
|
35
|
+
|
36
|
+
# apply green threads monkey patch before application load
|
37
|
+
require 'soba/monkey'
|
38
|
+
Soba::Monkey.patch!
|
39
|
+
|
40
|
+
use Rack::Reloader, 0
|
41
|
+
use Rack::ContentLength
|
42
|
+
|
43
|
+
app = proc do |env|
|
44
|
+
[ 200, {'Content-Type' => 'text/plain'}, ["你好,世界"] ]
|
45
|
+
end
|
46
|
+
|
47
|
+
run app
|
48
|
+
|
49
|
+
```
|
50
|
+
|
51
|
+
See [examples](/examples/)
|
26
52
|
|
27
53
|
## Development
|
28
54
|
|
data/benchmark/README.md
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
## Benchmark
|
2
|
+
|
3
|
+
### Rack: hello world
|
4
|
+
|
5
|
+
Create a aws `t2.xlarge` machine.
|
6
|
+
|
7
|
+
Use `rackup` to start server.
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
#\ -s soba -q -O Port=3000
|
11
|
+
|
12
|
+
require 'soba/server'
|
13
|
+
|
14
|
+
# apply green threads monkey patch before application load
|
15
|
+
require 'soba/monkey'
|
16
|
+
Soba::Monkey.patch!
|
17
|
+
|
18
|
+
app = proc do |env|
|
19
|
+
[ 200, {'Content-Type' => 'text/plain'}, ["你好,世界"] ]
|
20
|
+
end
|
21
|
+
|
22
|
+
run app
|
23
|
+
```
|
24
|
+
|
25
|
+
run `wrk -t12 -c400 -d30s http://127.0.0.1:3000/`
|
26
|
+
|
27
|
+
```
|
28
|
+
Soba:
|
29
|
+
Running 30s test @ http://127.0.0.1:3000/
|
30
|
+
12 threads and 400 connections
|
31
|
+
Thread Stats Avg Stdev Max +/- Stdev
|
32
|
+
Latency 62.35ms 18.20ms 467.83ms 98.53%
|
33
|
+
Req/Sec 185.17 77.31 660.00 77.91%
|
34
|
+
65904 requests in 30.06s, 6.10MB read
|
35
|
+
Requests/sec: 2192.15
|
36
|
+
Transfer/sec: 207.65KB
|
37
|
+
|
38
|
+
Falcon:
|
39
|
+
Running 30s test @ http://127.0.0.1:3000/
|
40
|
+
12 threads and 400 connections
|
41
|
+
Thread Stats Avg Stdev Max +/- Stdev
|
42
|
+
Latency 115.32ms 28.54ms 1.09s 96.18%
|
43
|
+
Req/Sec 284.39 83.47 666.00 83.16%
|
44
|
+
102006 requests in 30.04s, 9.24MB read
|
45
|
+
Socket errors: connect 0, read 0, write 0, timeout 1
|
46
|
+
Requests/sec: 3395.81
|
47
|
+
Transfer/sec: 315.04KB
|
48
|
+
|
49
|
+
Puma:
|
50
|
+
Running 30s test @ http://127.0.0.1:3000/
|
51
|
+
12 threads and 400 connections
|
52
|
+
Thread Stats Avg Stdev Max +/- Stdev
|
53
|
+
Latency 7.34ms 10.55ms 104.94ms 86.40%
|
54
|
+
Req/Sec 1.10k 441.58 2.00k 56.57%
|
55
|
+
65568 requests in 30.10s, 6.13MB read
|
56
|
+
Requests/sec: 2178.59
|
57
|
+
Transfer/sec: 208.50KB
|
58
|
+
|
59
|
+
Thin:
|
60
|
+
Running 30s test @ http://127.0.0.1:3000/
|
61
|
+
12 threads and 400 connections
|
62
|
+
Thread Stats Avg Stdev Max +/- Stdev
|
63
|
+
Latency 46.76ms 81.43ms 1.11s 98.09%
|
64
|
+
Req/Sec 322.11 239.56 0.97k 64.13%
|
65
|
+
29674 requests in 30.07s, 3.71MB read
|
66
|
+
Socket errors: connect 359, read 0, write 0, timeout 10
|
67
|
+
Requests/sec: 986.78
|
68
|
+
Transfer/sec: 126.24KB
|
69
|
+
```
|
data/examples/config.ru
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#\ -s soba -O Port=3000
|
2
|
+
|
3
|
+
# apply green threads monkey patch before application load
|
4
|
+
require 'soba/monkey'
|
5
|
+
Soba::Monkey.patch!
|
6
|
+
|
7
|
+
use Rack::Reloader, 0
|
8
|
+
use Rack::ContentLength
|
9
|
+
|
10
|
+
app = proc do |env|
|
11
|
+
[ 200, {'Content-Type' => 'text/plain'}, ["你好,世界"] ]
|
12
|
+
end
|
13
|
+
|
14
|
+
run app
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'rack/handler'
|
2
|
+
require 'soba/server'
|
3
|
+
|
4
|
+
module Rack
|
5
|
+
module Handler
|
6
|
+
module Soba
|
7
|
+
DEFAULT_OPTIONS = {
|
8
|
+
:debug => false,
|
9
|
+
}
|
10
|
+
|
11
|
+
|
12
|
+
def self.run(app, options = {})
|
13
|
+
options = DEFAULT_OPTIONS.merge(options)
|
14
|
+
|
15
|
+
host, port = options[:Host], options[:Port]
|
16
|
+
|
17
|
+
server = ::Soba::Server.new(app, host: host, port: port, **options)
|
18
|
+
|
19
|
+
yield server if block_given?
|
20
|
+
begin
|
21
|
+
server.run
|
22
|
+
rescue Interrupt
|
23
|
+
puts "* Stopping..."
|
24
|
+
# server.stop
|
25
|
+
puts "* Cool!"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.valid_options
|
30
|
+
{
|
31
|
+
"Host=HOST" => "Hostname to listen on (default: localhost)",
|
32
|
+
"Port=PORT" => "Port to listen on (default: 8080)",
|
33
|
+
"debug=false" => "Enable debug output (default: false)",
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
register :soba, Soba
|
39
|
+
end
|
40
|
+
end
|
data/lib/soba.rb
CHANGED
data/lib/soba/cli.rb
ADDED
data/lib/soba/const.rb
ADDED
@@ -0,0 +1,217 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
# COPY FROM Puma server
|
3
|
+
require_relative 'version'
|
4
|
+
module Soba
|
5
|
+
# Every standard HTTP code mapped to the appropriate message. These are
|
6
|
+
# used so frequently that they are placed directly in Puma for easy
|
7
|
+
# access rather than Puma::Const itself.
|
8
|
+
|
9
|
+
# Every standard HTTP code mapped to the appropriate message.
|
10
|
+
# Generated with:
|
11
|
+
# curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \
|
12
|
+
# ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \
|
13
|
+
# puts "#{m[1]} => \x27#{m[2].strip}\x27,"'
|
14
|
+
HTTP_STATUS_CODES = {
|
15
|
+
100 => 'Continue',
|
16
|
+
101 => 'Switching Protocols',
|
17
|
+
102 => 'Processing',
|
18
|
+
200 => 'OK',
|
19
|
+
201 => 'Created',
|
20
|
+
202 => 'Accepted',
|
21
|
+
203 => 'Non-Authoritative Information',
|
22
|
+
204 => 'No Content',
|
23
|
+
205 => 'Reset Content',
|
24
|
+
206 => 'Partial Content',
|
25
|
+
207 => 'Multi-Status',
|
26
|
+
208 => 'Already Reported',
|
27
|
+
226 => 'IM Used',
|
28
|
+
300 => 'Multiple Choices',
|
29
|
+
301 => 'Moved Permanently',
|
30
|
+
302 => 'Found',
|
31
|
+
303 => 'See Other',
|
32
|
+
304 => 'Not Modified',
|
33
|
+
305 => 'Use Proxy',
|
34
|
+
307 => 'Temporary Redirect',
|
35
|
+
308 => 'Permanent Redirect',
|
36
|
+
400 => 'Bad Request',
|
37
|
+
401 => 'Unauthorized',
|
38
|
+
402 => 'Payment Required',
|
39
|
+
403 => 'Forbidden',
|
40
|
+
404 => 'Not Found',
|
41
|
+
405 => 'Method Not Allowed',
|
42
|
+
406 => 'Not Acceptable',
|
43
|
+
407 => 'Proxy Authentication Required',
|
44
|
+
408 => 'Request Timeout',
|
45
|
+
409 => 'Conflict',
|
46
|
+
410 => 'Gone',
|
47
|
+
411 => 'Length Required',
|
48
|
+
412 => 'Precondition Failed',
|
49
|
+
413 => 'Payload Too Large',
|
50
|
+
414 => 'URI Too Long',
|
51
|
+
415 => 'Unsupported Media Type',
|
52
|
+
416 => 'Range Not Satisfiable',
|
53
|
+
417 => 'Expectation Failed',
|
54
|
+
418 => 'I\'m A Teapot',
|
55
|
+
421 => 'Misdirected Request',
|
56
|
+
422 => 'Unprocessable Entity',
|
57
|
+
423 => 'Locked',
|
58
|
+
424 => 'Failed Dependency',
|
59
|
+
426 => 'Upgrade Required',
|
60
|
+
428 => 'Precondition Required',
|
61
|
+
429 => 'Too Many Requests',
|
62
|
+
431 => 'Request Header Fields Too Large',
|
63
|
+
451 => 'Unavailable For Legal Reasons',
|
64
|
+
500 => 'Internal Server Error',
|
65
|
+
501 => 'Not Implemented',
|
66
|
+
502 => 'Bad Gateway',
|
67
|
+
503 => 'Service Unavailable',
|
68
|
+
504 => 'Gateway Timeout',
|
69
|
+
505 => 'HTTP Version Not Supported',
|
70
|
+
506 => 'Variant Also Negotiates',
|
71
|
+
507 => 'Insufficient Storage',
|
72
|
+
508 => 'Loop Detected',
|
73
|
+
510 => 'Not Extended',
|
74
|
+
511 => 'Network Authentication Required'
|
75
|
+
}
|
76
|
+
|
77
|
+
STATUS_WITH_NO_ENTITY_BODY = {
|
78
|
+
204 => true,
|
79
|
+
205 => true,
|
80
|
+
304 => true
|
81
|
+
}
|
82
|
+
|
83
|
+
# Frequently used constants when constructing requests or responses. Many times
|
84
|
+
# the constant just refers to a string with the same contents. Using these constants
|
85
|
+
# gave about a 3% to 10% performance improvement over using the strings directly.
|
86
|
+
#
|
87
|
+
# The constants are frozen because Hash#[]= when called with a String key dups
|
88
|
+
# the String UNLESS the String is frozen. This saves us therefore 2 object
|
89
|
+
# allocations when creating the env hash later.
|
90
|
+
#
|
91
|
+
# While Puma does try to emulate the CGI/1.2 protocol, it does not use the REMOTE_IDENT,
|
92
|
+
# REMOTE_USER, or REMOTE_HOST parameters since those are either a security problem or
|
93
|
+
# too taxing on performance.
|
94
|
+
module Const
|
95
|
+
|
96
|
+
SOBA_VERSION = VERSION
|
97
|
+
|
98
|
+
FAST_TRACK_KA_TIMEOUT = 0.2
|
99
|
+
|
100
|
+
# The default number of seconds for another request within a persistent
|
101
|
+
# session.
|
102
|
+
PERSISTENT_TIMEOUT = 20
|
103
|
+
|
104
|
+
# The default number of seconds to wait until we get the first data
|
105
|
+
# for the request
|
106
|
+
FIRST_DATA_TIMEOUT = 30
|
107
|
+
|
108
|
+
# How long to wait when getting some write blocking on the socket when
|
109
|
+
# sending data back
|
110
|
+
WRITE_TIMEOUT = 10
|
111
|
+
|
112
|
+
# The original URI requested by the client.
|
113
|
+
REQUEST_URI = 'REQUEST_URI'.freeze
|
114
|
+
REQUEST_PATH = 'REQUEST_PATH'.freeze
|
115
|
+
QUERY_STRING = 'QUERY_STRING'.freeze
|
116
|
+
|
117
|
+
PATH_INFO = 'PATH_INFO'.freeze
|
118
|
+
|
119
|
+
# Indicate that we couldn't parse the request
|
120
|
+
ERROR_400_RESPONSE = "HTTP/1.1 400 Bad Request\r\n\r\n".freeze
|
121
|
+
|
122
|
+
# The standard empty 404 response for bad requests. Use Error4040Handler for custom stuff.
|
123
|
+
ERROR_404_RESPONSE = "HTTP/1.1 404 Not Found\r\nConnection: close\r\nServer: Puma #{SOBA_VERSION}\r\n\r\nNOT FOUND".freeze
|
124
|
+
|
125
|
+
# The standard empty 408 response for requests that timed out.
|
126
|
+
ERROR_408_RESPONSE = "HTTP/1.1 408 Request Timeout\r\nConnection: close\r\nServer: Puma #{SOBA_VERSION}\r\n\r\n".freeze
|
127
|
+
|
128
|
+
CONTENT_LENGTH = "CONTENT_LENGTH".freeze
|
129
|
+
|
130
|
+
# Indicate that there was an internal error, obviously.
|
131
|
+
ERROR_500_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\n\r\n".freeze
|
132
|
+
|
133
|
+
# A common header for indicating the server is too busy. Not used yet.
|
134
|
+
ERROR_503_RESPONSE = "HTTP/1.1 503 Service Unavailable\r\n\r\nBUSY".freeze
|
135
|
+
|
136
|
+
# The basic max request size we'll try to read.
|
137
|
+
CHUNK_SIZE = 16 * 1024
|
138
|
+
|
139
|
+
# This is the maximum header that is allowed before a client is booted. The parser detects
|
140
|
+
# this, but we'd also like to do this as well.
|
141
|
+
MAX_HEADER = 1024 * (80 + 32)
|
142
|
+
|
143
|
+
# Maximum request body size before it is moved out of memory and into a tempfile for reading.
|
144
|
+
MAX_BODY = MAX_HEADER
|
145
|
+
|
146
|
+
REQUEST_METHOD = "REQUEST_METHOD".freeze
|
147
|
+
HEAD = "HEAD".freeze
|
148
|
+
# ETag is based on the apache standard of hex mtime-size-inode (inode is 0 on win32)
|
149
|
+
LINE_END = "\r\n".freeze
|
150
|
+
REMOTE_ADDR = "REMOTE_ADDR".freeze
|
151
|
+
HTTP_X_FORWARDED_FOR = "HTTP_X_FORWARDED_FOR".freeze
|
152
|
+
|
153
|
+
SERVER_NAME = "SERVER_NAME".freeze
|
154
|
+
SERVER_PORT = "SERVER_PORT".freeze
|
155
|
+
HTTP_HOST = "HTTP_HOST".freeze
|
156
|
+
PORT_80 = "80".freeze
|
157
|
+
PORT_443 = "443".freeze
|
158
|
+
LOCALHOST = "localhost".freeze
|
159
|
+
LOCALHOST_IP = "127.0.0.1".freeze
|
160
|
+
LOCALHOST_ADDR = "127.0.0.1:0".freeze
|
161
|
+
|
162
|
+
SERVER_PROTOCOL = "SERVER_PROTOCOL".freeze
|
163
|
+
HTTP_11 = "HTTP/1.1".freeze
|
164
|
+
|
165
|
+
SERVER_SOFTWARE = "SERVER_SOFTWARE".freeze
|
166
|
+
GATEWAY_INTERFACE = "GATEWAY_INTERFACE".freeze
|
167
|
+
CGI_VER = "CGI/1.2".freeze
|
168
|
+
|
169
|
+
STOP_COMMAND = "?".freeze
|
170
|
+
HALT_COMMAND = "!".freeze
|
171
|
+
RESTART_COMMAND = "R".freeze
|
172
|
+
|
173
|
+
RACK_INPUT = "rack.input".freeze
|
174
|
+
RACK_URL_SCHEME = "rack.url_scheme".freeze
|
175
|
+
RACK_AFTER_REPLY = "rack.after_reply".freeze
|
176
|
+
|
177
|
+
HTTP = "http".freeze
|
178
|
+
HTTPS = "https".freeze
|
179
|
+
|
180
|
+
HTTPS_KEY = "HTTPS".freeze
|
181
|
+
|
182
|
+
HTTP_VERSION = "HTTP_VERSION".freeze
|
183
|
+
HTTP_CONNECTION = "HTTP_CONNECTION".freeze
|
184
|
+
HTTP_EXPECT = "HTTP_EXPECT".freeze
|
185
|
+
CONTINUE = "100-continue".freeze
|
186
|
+
|
187
|
+
HTTP_11_100 = "HTTP/1.1 100 Continue\r\n\r\n".freeze
|
188
|
+
HTTP_11_200 = "HTTP/1.1 200 OK\r\n".freeze
|
189
|
+
HTTP_10_200 = "HTTP/1.0 200 OK\r\n".freeze
|
190
|
+
|
191
|
+
CLOSE = "close".freeze
|
192
|
+
KEEP_ALIVE = "keep-alive".freeze
|
193
|
+
|
194
|
+
CONTENT_LENGTH2 = "content-length".freeze
|
195
|
+
CONTENT_LENGTH_S = "Content-Length: ".freeze
|
196
|
+
TRANSFER_ENCODING = "transfer-encoding".freeze
|
197
|
+
TRANSFER_ENCODING2 = "HTTP_TRANSFER_ENCODING".freeze
|
198
|
+
|
199
|
+
CONNECTION_CLOSE = "Connection: close\r\n".freeze
|
200
|
+
CONNECTION_KEEP_ALIVE = "Connection: Keep-Alive\r\n".freeze
|
201
|
+
|
202
|
+
TRANSFER_ENCODING_CHUNKED = "Transfer-Encoding: chunked\r\n".freeze
|
203
|
+
CLOSE_CHUNKED = "0\r\n\r\n".freeze
|
204
|
+
|
205
|
+
CHUNKED = "chunked".freeze
|
206
|
+
|
207
|
+
COLON = ": ".freeze
|
208
|
+
|
209
|
+
NEWLINE = "\n".freeze
|
210
|
+
|
211
|
+
HIJACK_P = "rack.hijack?".freeze
|
212
|
+
HIJACK = "rack.hijack".freeze
|
213
|
+
HIJACK_IO = "rack.hijack_io".freeze
|
214
|
+
|
215
|
+
EARLY_HINTS = "rack.early_hints".freeze
|
216
|
+
end
|
217
|
+
end
|
data/lib/soba/monkey.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'lightio'
|
2
|
+
module Soba
|
3
|
+
module Monkey
|
4
|
+
class << self
|
5
|
+
def patch!
|
6
|
+
origin_verbose = $VERBOSE
|
7
|
+
$VERBOSE = nil
|
8
|
+
LightIO::Monkey.patch_all! unless LightIO::Monkey.patched?(IO)
|
9
|
+
ensure
|
10
|
+
$VERBOSE = origin_verbose
|
11
|
+
end
|
12
|
+
|
13
|
+
alias patch_all! patch!
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/soba/parser.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'pico_http_parser'
|
2
|
+
|
3
|
+
module Soba
|
4
|
+
class Parser
|
5
|
+
CRLF = "\r\n".freeze
|
6
|
+
|
7
|
+
class InvalidRequest
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :socket
|
11
|
+
|
12
|
+
def initialize(socket)
|
13
|
+
@socket = socket
|
14
|
+
@headers = nil
|
15
|
+
@body = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def headers
|
19
|
+
@headers ||= begin
|
20
|
+
request_body = ""
|
21
|
+
while (line = socket.readline) != CRLF
|
22
|
+
request_body << line
|
23
|
+
end
|
24
|
+
request_body << CRLF
|
25
|
+
debug request_body
|
26
|
+
ret = PicoHTTPParser.parse_http_request(request_body, @headers ||= {})
|
27
|
+
debug ret
|
28
|
+
raise InvalidRequest, "ret: #{ret}" if ret < 0
|
29
|
+
@headers
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
alias env headers
|
34
|
+
|
35
|
+
# http or https
|
36
|
+
def request_schema
|
37
|
+
@request_schema ||= server_protocol.split("/").first.downcase
|
38
|
+
end
|
39
|
+
|
40
|
+
def content_length
|
41
|
+
headers["CONTENT_LENGTH"]&.to_i
|
42
|
+
end
|
43
|
+
|
44
|
+
def body
|
45
|
+
@body ||= begin
|
46
|
+
StringIO.new(content_length ? socket.read(content_length) : '').binmode
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
%w{SERVER_PROTOCOL}.each do |header|
|
51
|
+
define_method header.downcase do
|
52
|
+
headers[header]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
def debug(*args)
|
58
|
+
STDERR.puts(args) if ENV["SOBA_DEBUG"] == '1'
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/soba/server.rb
CHANGED
@@ -1,26 +1,149 @@
|
|
1
1
|
require 'lightio'
|
2
|
+
require 'logger'
|
3
|
+
require_relative 'parser'
|
4
|
+
require_relative 'const'
|
2
5
|
|
3
6
|
module Soba
|
4
7
|
class Server
|
5
|
-
|
6
|
-
|
8
|
+
attr_reader :logger, :host, :port, :app
|
9
|
+
|
10
|
+
def error_stream
|
11
|
+
STDERR
|
12
|
+
end
|
13
|
+
|
14
|
+
include Const
|
15
|
+
|
16
|
+
def initialize(app, host:, port:, **options)
|
17
|
+
@app = app
|
18
|
+
@host, @port = host, port
|
19
|
+
@server = nil
|
20
|
+
@logger = Logger.new(STDOUT, level: is_true?(options[:debug]) ? Logger::DEBUG : Logger::INFO)
|
21
|
+
@options = options
|
7
22
|
end
|
8
23
|
|
9
24
|
def run
|
25
|
+
setup
|
10
26
|
while (socket = @server.accept)
|
11
27
|
_, port, host = socket.peeraddr
|
12
|
-
|
28
|
+
logger.debug "accept connection from #{host}:#{port}"
|
13
29
|
|
14
30
|
LightIO::Beam.new(socket) do |socket|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
31
|
+
begin
|
32
|
+
process_request(socket) until socket.closed?
|
33
|
+
rescue StandardError => e
|
34
|
+
logger.info("Exception: #{e}")
|
35
|
+
raise
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def rack_env
|
42
|
+
@rack_env ||= {
|
43
|
+
"rack.version" => Rack::VERSION,
|
44
|
+
"rack.multithread" => true,
|
45
|
+
"rack.multiprocess" => false,
|
46
|
+
"rack.run_once" => false,
|
47
|
+
"rack.hijack?" => false,
|
48
|
+
"rack.hijack" => nil,
|
49
|
+
"rack.hijack_io" => nil,
|
50
|
+
"rack.logger" => logger,
|
51
|
+
"SERVER_PORT" => port.to_s,
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
def process_request(socket)
|
56
|
+
parser = Parser.new(socket)
|
57
|
+
env = parser.env.merge(rack_env)
|
58
|
+
env["rack.url_scheme"] = parser.request_schema
|
59
|
+
env["rack.input"] = parser.body
|
60
|
+
env["rack.errors"] = error_stream
|
61
|
+
env["SERVER_NAME"] = env[HTTP_HOST].split(":")[0]
|
62
|
+
|
63
|
+
keep_alive = env[HTTP_CONNECTION].to_s.downcase == KEEP_ALIVE
|
64
|
+
|
65
|
+
begin
|
66
|
+
status, headers, res_body = app.call(env)
|
67
|
+
logger.debug("response: #{status} #{headers.inspect} #{res_body.inspect}")
|
68
|
+
rescue Exception => e
|
69
|
+
status = 500
|
70
|
+
headers = {'Content-Type' => 'text/plain'}
|
71
|
+
logger.error("Internal Server Error: #{e}:\n#{e.backtrace.join("\n")}")
|
72
|
+
end
|
73
|
+
|
74
|
+
nobody = parser.env[REQUEST_METHOD] == HEAD || STATUS_WITH_NO_ENTITY_BODY[status]
|
75
|
+
|
76
|
+
outbuf = ''
|
77
|
+
status_line = "#{parser.server_protocol} #{status} #{status_text(status)}\n"
|
78
|
+
outbuf << status_line
|
79
|
+
|
80
|
+
set_content_length = false
|
81
|
+
headers.each do |header, vs|
|
82
|
+
case header.downcase
|
83
|
+
when CONTENT_LENGTH2, CONTENT_LENGTH
|
84
|
+
set_content_length = true
|
22
85
|
end
|
86
|
+
outbuf << "#{header}#{COLON}#{vs}#{LINE_END}"
|
23
87
|
end
|
88
|
+
|
89
|
+
chunked = !set_content_length
|
90
|
+
outbuf << TRANSFER_ENCODING_CHUNKED if chunked
|
91
|
+
|
92
|
+
connection_header = keep_alive ? CONNECTION_KEEP_ALIVE : CONNECTION_CLOSE
|
93
|
+
outbuf << connection_header
|
94
|
+
outbuf << "\n"
|
95
|
+
|
96
|
+
if nobody
|
97
|
+
socket << outbuf
|
98
|
+
socket.flush
|
99
|
+
return
|
100
|
+
end
|
101
|
+
|
102
|
+
res_body.each do |part|
|
103
|
+
if chunked
|
104
|
+
next if part.bytesize.zero?
|
105
|
+
outbuf << part.bytesize.to_s(16)
|
106
|
+
outbuf << LINE_END
|
107
|
+
outbuf << part
|
108
|
+
outbuf << LINE_END
|
109
|
+
else
|
110
|
+
outbuf << part
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
if chunked
|
115
|
+
outbuf << CLOSE_CHUNKED
|
116
|
+
end
|
117
|
+
# write is very slow, don't know why
|
118
|
+
socket << outbuf
|
119
|
+
socket.flush
|
120
|
+
res_body.close if res_body.respond_to?(:close)
|
121
|
+
ensure
|
122
|
+
socket.close unless socket.closed? || keep_alive
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
def status_text(status)
|
127
|
+
HTTP_STATUS_CODES[status.to_i]
|
128
|
+
end
|
129
|
+
|
130
|
+
def setup
|
131
|
+
monkey_patch = LightIO::Monkey.patched?(IO)
|
132
|
+
logger.info "Soba #{Soba::VERSION}"
|
133
|
+
logger.info "ruby #{RUBY_VERSION}"
|
134
|
+
if monkey_patch
|
135
|
+
logger.info "Run in Green thread(monkey patch) mode, engine: #{NIO.engine}, see https://github.com/socketry/lightio"
|
136
|
+
logger.info "Current backend: #{LightIO::IOloop.current.backend}, available backends: #{NIO::Selector.backends} (set `LIGHTIO_BACKEND` env to choose)"
|
137
|
+
else
|
138
|
+
logger.info "Run in normal mode"
|
139
|
+
end
|
140
|
+
logger.info "Server start listen #{host}:#{port}"
|
141
|
+
|
142
|
+
@server = LightIO::TCPServer.new(host, port)
|
143
|
+
end
|
144
|
+
|
145
|
+
def is_true?(v)
|
146
|
+
%w{true on 1}.include?(v.to_s)
|
24
147
|
end
|
25
148
|
end
|
26
|
-
end
|
149
|
+
end
|
data/lib/soba/version.rb
CHANGED
data/soba.gemspec
CHANGED
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.authors = ["Jiang Jinyang"]
|
10
10
|
spec.email = ["jjyruby@gmail.com"]
|
11
11
|
|
12
|
-
spec.summary = %q{
|
13
|
-
spec.description = %q{
|
12
|
+
spec.summary = %q{Soba is an experience rack server, which build upon green thread.}
|
13
|
+
spec.description = %q{Soba use [LightIO](https://github.com/socketry/lightio) to provision green threads.}
|
14
14
|
spec.homepage = "https://github.com/jjyr/soba"
|
15
15
|
spec.license = "MIT"
|
16
16
|
|
@@ -21,7 +21,9 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
22
|
spec.require_paths = ["lib"]
|
23
23
|
|
24
|
-
spec.add_dependency "lightio", "~> 0.4"
|
24
|
+
spec.add_dependency "lightio", "~> 0.4.3"
|
25
|
+
spec.add_dependency "pico_http_parser", "~> 0.0"
|
26
|
+
spec.add_dependency "rack"
|
25
27
|
spec.add_development_dependency "bundler", "~> 1.16"
|
26
28
|
spec.add_development_dependency "rake", "~> 10.0"
|
27
29
|
spec.add_development_dependency "rspec", "~> 3.0"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: soba
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jiang Jinyang
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-02-
|
11
|
+
date: 2018-02-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: lightio
|
@@ -16,14 +16,42 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 0.4.3
|
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:
|
26
|
+
version: 0.4.3
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pico_http_parser
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rack
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
27
55
|
- !ruby/object:Gem::Dependency
|
28
56
|
name: bundler
|
29
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,7 +94,8 @@ dependencies:
|
|
66
94
|
- - "~>"
|
67
95
|
- !ruby/object:Gem::Version
|
68
96
|
version: '3.0'
|
69
|
-
description:
|
97
|
+
description: Soba use [LightIO](https://github.com/socketry/lightio) to provision
|
98
|
+
green threads.
|
70
99
|
email:
|
71
100
|
- jjyruby@gmail.com
|
72
101
|
executables: []
|
@@ -82,10 +111,17 @@ files:
|
|
82
111
|
- LICENSE.txt
|
83
112
|
- README.md
|
84
113
|
- Rakefile
|
114
|
+
- benchmark/README.md
|
85
115
|
- bin/console
|
86
116
|
- bin/setup
|
87
|
-
-
|
117
|
+
- examples/config.ru
|
118
|
+
- lib/rack/handler/soba.rb
|
88
119
|
- lib/soba.rb
|
120
|
+
- lib/soba/cli.rb
|
121
|
+
- lib/soba/const.rb
|
122
|
+
- lib/soba/monkey.rb
|
123
|
+
- lib/soba/parser.rb
|
124
|
+
- lib/soba/rack_default.rb
|
89
125
|
- lib/soba/server.rb
|
90
126
|
- lib/soba/version.rb
|
91
127
|
- soba.gemspec
|
@@ -104,13 +140,13 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
104
140
|
version: '0'
|
105
141
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
106
142
|
requirements:
|
107
|
-
- - "
|
143
|
+
- - ">="
|
108
144
|
- !ruby/object:Gem::Version
|
109
|
-
version:
|
145
|
+
version: '0'
|
110
146
|
requirements: []
|
111
147
|
rubyforge_project:
|
112
148
|
rubygems_version: 2.6.14
|
113
149
|
signing_key:
|
114
150
|
specification_version: 4
|
115
|
-
summary:
|
151
|
+
summary: Soba is an experience rack server, which build upon green thread.
|
116
152
|
test_files: []
|