soba 0.1.0.pre → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|