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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: '052379d3ef962797c865da3c1757c372c6f15b69'
4
- data.tar.gz: 3c5240d046b932c173e33ba8f8162aba18c8adee
3
+ metadata.gz: bf5254ef757d85ce6742c3ae2f6ca79b90edf225
4
+ data.tar.gz: 58c573c317ad913d7d575346ccfcaecb4da04eef
5
5
  SHA512:
6
- metadata.gz: b0a95f068a7d263d24451c3045c4a330a21090d7691bf48a11f3ff2055387a78e114f29b8c439bd471ef6c769d8b577d4ac74dc5f9bd753987f0df47155ae77e
7
- data.tar.gz: 6ea0694d524551fd876f8f70a9937e1b4ad975680708e74eb61a3558558da9dee42c2238dce2d2d8d22e38d0c83cd63cc323d838b23534b10d2e1e9a50543dad
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.pre)
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.2)
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
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/soba`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ Soba is an experimental rack server, which build upon green thread.
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
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
- TODO: Write usage instructions here
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
 
@@ -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
+ ```
@@ -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
@@ -1,5 +1,4 @@
1
1
  require "soba/version"
2
- require "soba/server"
3
2
 
4
3
  module Soba
5
4
  # Your code goes here...
data/lib/soba/cli.rb ADDED
@@ -0,0 +1,11 @@
1
+ require_relative 'server'
2
+
3
+ module Soba
4
+ class CLI
5
+ def run
6
+ host, port = ARGV
7
+ app = lambda {|env| [200, {'Host' => 'securet'}, "nihaoooo"]}
8
+ Soba::Server.new(app, host, port).run
9
+ end
10
+ end
11
+ end
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
@@ -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
@@ -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
@@ -0,0 +1,7 @@
1
+ require 'rack/handler/soba'
2
+
3
+ module Rack::Handler
4
+ def self.default(options = {})
5
+ Rack::Handler::Soba
6
+ end
7
+ 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
- def initialize(host, port)
6
- @server = LightIO::TCPServer.new(host, port)
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
- puts "accept connection from #{host}:#{port}"
28
+ logger.debug "accept connection from #{host}:#{port}"
13
29
 
14
30
  LightIO::Beam.new(socket) do |socket|
15
- socket << "HTTP/1.1 200 OK\n"
16
- socket << "Content-Type: text/html\n"
17
- socket << "Content-Length: 2\n"
18
- socket << "Accept-Ranges: bytes\n"
19
- socket << "\n"
20
- socket << "OK"
21
- socket.close
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
@@ -1,3 +1,3 @@
1
1
  module Soba
2
- VERSION = "0.1.0.pre"
2
+ VERSION = "0.1.0"
3
3
  end
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{A tiny web server.}
13
- spec.description = %q{A tiny web server, build upon green threads.}
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.pre
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-24 00:00:00.000000000 Z
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: '0.4'
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: '0.4'
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: A tiny web server, build upon green threads.
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
- - bin/soba
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: 1.3.1
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: A tiny web server.
151
+ summary: Soba is an experience rack server, which build upon green thread.
116
152
  test_files: []
data/bin/soba DELETED
@@ -1,5 +0,0 @@
1
- require "bundler/setup"
2
- require "soba"
3
-
4
- host, port = ARGV
5
- Soba::Server.new(host, port).run