thick 0.0.4-java

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ ## 0.4 (xy.xz.2012)
2
+
3
+ * make gem Java specific
4
+ * copy-less request body container
5
+ * async responses
6
+
7
+ ## 0.3 (26.3.2012)
8
+
9
+ Update Netty to 3.4.0.Alpha2
10
+
11
+ ## 0.2 (26.3.2012)
12
+
13
+ First public version
14
+
15
+ ## 0.1 (26.3.2012)
16
+
17
+ Claim the gem ;)
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Portions copyright (c) 2012 Marek Jelen
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # Thick
2
+
3
+ Thick is very lightweight web server for JRuby based on excellent Netty library.
4
+
5
+ ## Status
6
+
7
+ Works on several Sinatra and Rails applications. No real testing, yet.
8
+
9
+ ## Speed
10
+
11
+ see PERFORMANCE.md
12
+
13
+ ## Interfaces
14
+
15
+ Thick provides interfaces to control it's behaviour.
16
+
17
+ ### Asynchronous responses
18
+
19
+ Asynchronous responses provide functionality to stream response to the client by chunks.
20
+
21
+ The most basic API looks
22
+
23
+ env['thick.async'].async!(*params, &block)
24
+
25
+ the application has to respond in a standard manner
26
+
27
+ [status, headers, body]
28
+
29
+ the response will be processed and send to the client, however the connection is not closed, instead the block is
30
+ executed with parameters passed as params. From the block the application can stream data to the client
31
+
32
+ env['thick.async'].call(chunk)
33
+
34
+ after the response is completed the application has to close the connection
35
+
36
+ env['thick.async'].close
37
+
38
+ In more complex scenarios the params and block may be omitted. It's up to the application to wait till the response
39
+ header is sent to the client and stream it's data. To simplify the signalization the application may check if it's safe
40
+ to send it's data by.
41
+
42
+ env['thick.async'].ready?
43
+
44
+ if the method's response is positive it's safe to stream the data.
45
+
46
+ In the most complex cases it might be necessary to completely bypass default HTTP response generated by the server. In
47
+ those cases the application calls
48
+
49
+ env['thick.async'].custom!(*params, &block)
50
+
51
+ the method informs the server to not send any response on the connection and do not close the connection.
52
+ The contract is the same as with async! mechanism. The block with parameters params is triggered by the server when
53
+ it's safe to stream data. If the block is not given it's completely up to the application to stream the data.
54
+ The application may check whether the server considers safe to stream data by the same way as with async! response
55
+
56
+ env['thick.async'].ready?
57
+
58
+ In the case of custom responses it should be safe to stream right away without checking the server's opinion. Also with
59
+ custom responses it's not necessary to return standard rack response.
60
+
61
+ ## Starting
62
+
63
+ thick [-o <interface>] [-p <port>] [-E <environment>]
64
+
65
+ ## Installation
66
+
67
+ gem install thick
68
+
69
+ ## License
70
+
71
+ MIT ... let me know if that's a problem for you.
data/ROADMAP.md ADDED
@@ -0,0 +1,12 @@
1
+ ## Must to have
2
+
3
+ * SSL support
4
+ * Tests
5
+ * Code refactorings
6
+ * Optimalizations
7
+ * Replace StringIO
8
+
9
+ ## Nice to have
10
+
11
+ * SPDY support
12
+ * WebSockets support
data/bin/thick ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env ruby
2
+ require 'optparse'
3
+
4
+ require 'rubygems'
5
+ require 'rack'
6
+
7
+ $: << File.expand_path('../../lib', __FILE__)
8
+ require 'thick'
9
+
10
+ options = {}
11
+
12
+ OptionParser.new do |opts|
13
+
14
+ opts.banner = 'Usage: thick [options]'
15
+
16
+ opts.separator ''
17
+ opts.separator 'Ruby options:'
18
+
19
+ opts.separator ''
20
+ opts.separator 'Rack options:'
21
+
22
+ opts.on('-o', '--host HOST', 'listen on HOST (default: 0.0.0.0)') do |host|
23
+ options[:address] = host
24
+ end
25
+
26
+ opts.on('-p', '--port PORT', 'use PORT (default: 9292)') do |port|
27
+ options[:port] = Integer(port)
28
+ end
29
+
30
+ opts.on('-E', '--env ENVIRONMENT', 'use ENVIRONMENT for defaults (default: development)') do |e|
31
+ options[:environment] = e
32
+ end
33
+
34
+ end.parse!
35
+
36
+ options[:address] ||= '0.0.0.0'
37
+ options[:port] ||= 9292
38
+ options[:environment] ||= 'development'
39
+
40
+ ENV['RACK_ENV'] ||= ENV['RAILS_ENV'] ||= options[:environment]
41
+
42
+ puts "Loading app #{options.inspect}"
43
+ app = Rack::Builder.parse_file('config.ru')
44
+
45
+ puts "Starting server"
46
+ Thick.start(app[0], options)
Binary file
@@ -0,0 +1,53 @@
1
+ module Thick
2
+
3
+ class AsyncResponse
4
+
5
+ def initialize(context)
6
+ @context = context
7
+ @ready = false
8
+ @custom = false
9
+ @async = false
10
+ @block = nil
11
+ @params = []
12
+ end
13
+
14
+ def call(chunk)
15
+ @context.channel.write(Thick::Java::ChannelBuffers.copiedBuffer(chunk.to_s, Thick::Java::CharsetUtil::UTF_8))
16
+ end
17
+
18
+ def close
19
+ @context.channel.write(Thick::Java::ChannelBuffers::EMPTY_BUFFER).addListener(Thick::Java::ChannelFutureListener::CLOSE)
20
+ end
21
+
22
+ def ready!
23
+ @ready = true
24
+ @block.call(*@params) if @block
25
+ end
26
+
27
+ def ready?
28
+ @ready
29
+ end
30
+
31
+ def custom!(*params, &block)
32
+ @custom = true
33
+ @block = block
34
+ @params = params
35
+ end
36
+
37
+ def custom?
38
+ @custom
39
+ end
40
+
41
+ def async!(*params, &block)
42
+ @async = true
43
+ @block = block
44
+ @params = params
45
+ end
46
+
47
+ def async?
48
+ @async
49
+ end
50
+
51
+ end
52
+
53
+ end
@@ -0,0 +1,42 @@
1
+ module Thick
2
+
3
+ class Buffer
4
+
5
+ def initialize
6
+ @buffers = []
7
+ @buffer = nil
8
+ @io = nil
9
+ end
10
+
11
+ def io
12
+ @buffer ||= Thick::Java::ChannelBuffers.wrapped_buffer(*@buffers)
13
+ @io ||= Thick::Java::ChannelBufferInputStream.new(@buffer).to_io.binmode
14
+ end
15
+
16
+ def write(data)
17
+ raise ArgumentError unless data.kind_of?(Thick::Java::ChannelBuffer)
18
+ @buffers << data
19
+ @buffer = nil
20
+ @io = nil
21
+ end
22
+
23
+ def gets
24
+ self.io.gets
25
+ end
26
+
27
+ def read(length = nil, buffer = nil)
28
+ buffer ? self.io.read(length, buffer) : self.io.read(length)
29
+ end
30
+
31
+ def each(&block)
32
+ self.io.each(&block)
33
+ end
34
+
35
+ def rewind
36
+ @buffer.reader_index(0) if @buffer
37
+ @io = nil
38
+ end
39
+
40
+ end
41
+
42
+ end
data/lib/thick/java.rb ADDED
@@ -0,0 +1,41 @@
1
+ module Thick
2
+ module Java
3
+
4
+ java_import 'java.io.RandomAccessFile'
5
+ java_import 'java.net.InetSocketAddress'
6
+ java_import 'java.util.concurrent.Executors'
7
+ java_import 'org.jboss.netty.bootstrap.ServerBootstrap'
8
+ java_import 'org.jboss.netty.buffer.ChannelBuffer'
9
+ java_import 'org.jboss.netty.buffer.ChannelBufferInputStream'
10
+ java_import 'org.jboss.netty.buffer.ChannelBuffers'
11
+ java_import 'org.jboss.netty.handler.codec.http.DefaultHttpResponse'
12
+ java_import 'org.jboss.netty.handler.codec.http.HttpContentDecompressor'
13
+ java_import 'org.jboss.netty.handler.codec.http.HttpHeaders'
14
+ java_import 'org.jboss.netty.handler.codec.http.HttpChunk'
15
+ java_import 'org.jboss.netty.handler.codec.http.HttpRequest'
16
+ java_import 'org.jboss.netty.handler.codec.http.HttpRequestDecoder'
17
+ java_import 'org.jboss.netty.handler.codec.http.HttpResponseEncoder'
18
+ java_import 'org.jboss.netty.handler.codec.http.HttpResponseStatus'
19
+ java_import 'org.jboss.netty.handler.codec.http.HttpVersion'
20
+ java_import 'org.jboss.netty.handler.codec.http.QueryStringDecoder'
21
+ java_import 'org.jboss.netty.handler.codec.spdy.SpdyFrameDecoder'
22
+ java_import 'org.jboss.netty.handler.codec.spdy.SpdyFrameEncoder'
23
+ java_import 'org.jboss.netty.handler.codec.spdy.SpdySessionHandler'
24
+ java_import 'org.jboss.netty.handler.execution.ExecutionHandler'
25
+ java_import 'org.jboss.netty.handler.execution.OrderedMemoryAwareThreadPoolExecutor'
26
+ java_import 'org.jboss.netty.handler.logging.LoggingHandler'
27
+ java_import 'org.jboss.netty.handler.stream.ChunkedStream'
28
+ java_import 'org.jboss.netty.handler.stream.ChunkedWriteHandler'
29
+ java_import 'org.jboss.netty.channel.DefaultFileRegion'
30
+ java_import 'org.jboss.netty.channel.ChannelFutureListener'
31
+ java_import 'org.jboss.netty.channel.ChannelPipelineFactory'
32
+ java_import 'org.jboss.netty.channel.Channels'
33
+ java_import 'org.jboss.netty.channel.SimpleChannelUpstreamHandler'
34
+ java_import 'org.jboss.netty.channel.ChannelUpstreamHandler'
35
+ java_import 'org.jboss.netty.channel.ChannelDownstreamHandler'
36
+ java_import 'org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory'
37
+ java_import 'org.jboss.netty.logging.InternalLogLevel'
38
+ java_import 'org.jboss.netty.util.CharsetUtil'
39
+
40
+ end
41
+ end
@@ -0,0 +1,70 @@
1
+ module Thick
2
+
3
+ class TimeProbe
4
+
5
+ include Thick::Java::ChannelUpstreamHandler
6
+ include Thick::Java::ChannelDownstreamHandler
7
+
8
+ def self.setup
9
+ @total = 0
10
+ @count = 0
11
+ end
12
+
13
+ def initialize
14
+ @time = Time.now.to_f
15
+ end
16
+
17
+ TimeProbe.setup
18
+
19
+ def self.time(time)
20
+ @total += time
21
+ @count += 1
22
+ if @count % 100 == 0
23
+ puts "AVG: #{@total / @count}"
24
+ setup
25
+ end
26
+ end
27
+
28
+ def handleUpstream(context, event)
29
+ context.send_upstream(event)
30
+ end
31
+
32
+ def handleDownstream(context, event)
33
+ if event.respond_to?(:getState)
34
+ stop = Time.now.to_f - @time
35
+ self.class.time(stop)
36
+ end
37
+ context.send_downstream(event)
38
+ end
39
+
40
+ end
41
+
42
+ class PipelineFactory
43
+
44
+ include Thick::Java::ChannelPipelineFactory
45
+
46
+ def initialize(application)
47
+ @application = application
48
+ @executor = Thick::Java::ExecutionHandler.new(Thick::Java::OrderedMemoryAwareThreadPoolExecutor.new(20, 0, 0))
49
+ end
50
+
51
+ def getPipeline
52
+ pipeline = Thick::Java::Channels.pipeline
53
+
54
+ pipeline.add_last('time_probe', TimeProbe.new)
55
+
56
+ pipeline.add_last('http_decoder', Thick::Java::HttpRequestDecoder.new)
57
+ pipeline.add_last('http_encoder', Thick::Java::HttpResponseEncoder.new)
58
+
59
+ pipeline.add_last('decompressor', Thick::Java::HttpContentDecompressor.new)
60
+
61
+ pipeline.add_last('executor', @executor)
62
+
63
+ pipeline.add_last('handler', ServerHandler.new(@application))
64
+
65
+ pipeline
66
+ end
67
+
68
+ end
69
+
70
+ end
@@ -0,0 +1,31 @@
1
+ module Thick
2
+
3
+ def self.start(application, options)
4
+ @server = Server.new(application)
5
+ @server.start(:address => options[:address], :port => options[:port])
6
+ end
7
+
8
+ class Server
9
+
10
+ def initialize(application)
11
+ @factory = Thick::Java::NioServerSocketChannelFactory.new(Thick::Java::Executors.new_cached_thread_pool, Thick::Java::Executors.new_cached_thread_pool)
12
+ @bootstrap = Thick::Java::ServerBootstrap.new(@factory)
13
+ @bootstrap.pipeline_factory = PipelineFactory.new(application)
14
+ end
15
+
16
+ def start(options = {})
17
+ @channel = @bootstrap.bind(Thick::Java::InetSocketAddress.new(options[:address], options[:port]))
18
+ puts 'Server started'
19
+ loop do
20
+ # ToDo: Is this hack needed?
21
+ sleep(100)
22
+ end
23
+ end
24
+
25
+ def stop
26
+ @channel.close
27
+ end
28
+
29
+ end
30
+
31
+ end
@@ -0,0 +1,138 @@
1
+ require 'stringio'
2
+
3
+ module Thick
4
+
5
+ class ServerHandler < Thick::Java::SimpleChannelUpstreamHandler
6
+
7
+ def initialize(application)
8
+ super()
9
+ @application = application
10
+ end
11
+
12
+ def channelConnected(context, event)
13
+ end
14
+
15
+ def messageReceived(context, event)
16
+ request = event.message
17
+ case request
18
+ when Thick::Java::HttpRequest
19
+ on_request(context, request)
20
+ when Thick::Java::HttpChunk
21
+ on_chunk(context, request)
22
+ end
23
+ end
24
+
25
+ def serve_file(context, file, from = 0, to = nil)
26
+ raf = Thick::Java::RandomAccessFile.new(file, "r")
27
+ to ||= File.size(file)
28
+ region = Thick::Java::DefaultFileRegion.new(raf.channel, from, to)
29
+ context.channel.write(region)
30
+ end
31
+
32
+ def on_request(context, request)
33
+
34
+ # Serve file if exists in public directory
35
+ file = File.join('public', request.uri)
36
+ return serve_file(context, file).addListener(Thick::Java::ChannelFutureListener::CLOSE) if !request.uri.index('..') && File.exists?(file) && !File.directory?(file)
37
+
38
+ query_string = request.uri.split('?', 2)
39
+ @env = {
40
+ # Rack specific
41
+ 'rack.version' => [1,1],
42
+ 'rack.url_scheme' => 'http', # ToDo: support https?
43
+ 'rack.input' => Buffer.new,
44
+ 'rack.errors' => $stdout, # ToDo: Sure about that?
45
+ 'rack.multithread' => true,
46
+ 'rack.multiprocess' => false,
47
+ 'rack.run_once' => false,
48
+ # HTTP specific
49
+ 'REQUEST_METHOD' => request.get_method.name,
50
+ 'SCRIPT_NAME' => '',
51
+ 'PATH_INFO' => query_string[0],
52
+ 'QUERY_STRING' => if query_string[1] && query_string[1] != '' then "?#{query_string[1]}" else '' end,
53
+ 'SERVER_NAME' => 'localhost', # ToDo: Be more precise!
54
+ 'SERVER_PORT' => '8080', # ToDo: Be more precise!
55
+ # Thick specific
56
+ 'thick.async' => AsyncResponse.new(context)
57
+ }
58
+
59
+ # Get content length if available
60
+ length = Thick::Java::HttpHeaders.get_content_length(request)
61
+ @env['CONTENT_LENGTH'] = length if length > 0
62
+
63
+ # Extract headers and normalize it's names
64
+ request.header_names.each do |name|
65
+ rack_name = name.gsub('-', '_').upcase
66
+ rack_name = "HTTP_#{rack_name}" unless %w(CONTENT_TYPE CONTENT_LENGTH).include?(rack_name)
67
+ @env[rack_name] = request.get_header(name)
68
+ end
69
+
70
+ # Write request content into prepared IO
71
+ @env['rack.input'].write(request.content)
72
+
73
+ # No chunks expected, handle request
74
+ handle_request(context) unless request.chunked?
75
+ end
76
+
77
+ def on_chunk(context, chunk)
78
+ # Write request content into prepared IO
79
+ @env['rack.input'].write(chunk.content)
80
+
81
+ # No more chunks expected, handle request
82
+ handle_request(context) if chunk.last?
83
+ end
84
+
85
+ def handle_request(context)
86
+ response = @application.call(@env)
87
+
88
+ # Let the application make the response as it wants
89
+ if @env['thick.async'].custom?
90
+ @env['thick.async'].ready!
91
+ return
92
+ end
93
+
94
+ # Unpack response
95
+ status, headers, content = response
96
+
97
+ # Create response and set status as requested by application
98
+ @response = Thick::Java::DefaultHttpResponse.new(Thick::Java::HttpVersion::HTTP_1_1, Thick::Java::HttpResponseStatus.value_of(status.to_i))
99
+
100
+ # Set headers as requested by application
101
+ headers.each do |name, value|
102
+ Thick::Java::HttpHeaders.set_header(@response, name, value)
103
+ end
104
+
105
+ context.channel.write(@response)
106
+
107
+ # Set content as requested by application
108
+ case
109
+ when content.respond_to?(:to_path)
110
+ serve_file(context, content.to_path)
111
+ else
112
+ content.each do |chunk|
113
+ context.channel.write(Thick::Java::ChannelBuffers.copiedBuffer(chunk, Thick::Java::CharsetUtil::UTF_8))
114
+ end
115
+ end
116
+
117
+ # When content is closable, close it
118
+ content.close if content.respond_to?(:close)
119
+
120
+ # Trigger async handler and marker if async response requested, close connection otherwise
121
+ if @env['thick.async'].async?
122
+ @env['thick.async'].ready!
123
+ else
124
+ @env['thick.async'].close
125
+ end
126
+ end
127
+
128
+ def channelClosed(context, event)
129
+ end
130
+
131
+ def exceptionCaught(context, e)
132
+ puts e.message if e.respond_to?(:message)
133
+ puts e.backtrace if e.respond_to?(:backtrace)
134
+ end
135
+
136
+ end
137
+
138
+ end
@@ -0,0 +1,3 @@
1
+ module Thick
2
+ VERSION = '0.0.4' unless const_defined?(:VERSION)
3
+ end
data/lib/thick.rb ADDED
@@ -0,0 +1,13 @@
1
+ require 'rack'
2
+
3
+ require 'java'
4
+
5
+ require File.expand_path('../jars/netty-3.4.0.Alpha2.jar', __FILE__)
6
+
7
+ require 'thick/java'
8
+ require 'thick/version'
9
+ require 'thick/buffer'
10
+ require 'thick/async_response'
11
+ require 'thick/server_handler'
12
+ require 'thick/pipeline_factory'
13
+ require 'thick/server'
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: thick
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.4
6
+ platform: java
7
+ authors:
8
+ - Marek Jelen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2012-03-29 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rack
17
+ prerelease: false
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - "="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.4.1
24
+ type: :runtime
25
+ version_requirements: *id001
26
+ description: Very lightweight web server for JRuby based on Netty library.
27
+ email:
28
+ - marek@jelen.biz
29
+ executables:
30
+ - thick
31
+ extensions: []
32
+
33
+ extra_rdoc_files: []
34
+
35
+ files:
36
+ - bin/thick
37
+ - lib/thick.rb
38
+ - lib/jars/netty-3.4.0.Alpha2.jar
39
+ - lib/thick/async_response.rb
40
+ - lib/thick/buffer.rb
41
+ - lib/thick/java.rb
42
+ - lib/thick/pipeline_factory.rb
43
+ - lib/thick/server.rb
44
+ - lib/thick/server_handler.rb
45
+ - lib/thick/version.rb
46
+ - LICENSE
47
+ - README.md
48
+ - ROADMAP.md
49
+ - CHANGELOG.md
50
+ homepage: http://github.com/marekjelen/thick
51
+ licenses: []
52
+
53
+ post_install_message:
54
+ rdoc_options: []
55
+
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ none: false
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: "0"
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: "0"
70
+ requirements: []
71
+
72
+ rubyforge_project:
73
+ rubygems_version: 1.8.15
74
+ signing_key:
75
+ specification_version: 3
76
+ summary: Very lightweight web server for JRuby
77
+ test_files: []
78
+