diff --git a/.document b/.document new file mode 100644 index 0000000..b3114f5 --- /dev/null +++ b/.document @@ -0,0 +1,3 @@ +- +ChangeLog.* +LICENSE.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac5b454 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +doc/ +pkg/ diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..660778b --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--colour --format documentation diff --git a/.yardopts b/.yardopts new file mode 100644 index 0000000..e5f2505 --- /dev/null +++ b/.yardopts @@ -0,0 +1 @@ +--markup markdown --title "threaded_server Documentation" --protected diff --git a/ChangeLog.md b/ChangeLog.md new file mode 100644 index 0000000..bc9296f --- /dev/null +++ b/ChangeLog.md @@ -0,0 +1,4 @@ +### 0.1.0 / 2011-05-09 + +* Initial release: + diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..896c424 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,20 @@ +Copyright (c) 2011 Hal Brodigan + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d79c2da --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# threaded_server + +* [Homepage](http://github.com/postmodern/threaded_server) +* [Documentation](http://rubydoc.info/gems/threaded_server/frames) +* [Email](mailto:postmodern.mod3 at gmail.com) + +## Description + +A generic TCP Server with a fixed-size Thread Pool. + +## Examples + + require 'threaded_server' + + ThreadedServer.open('127.0.0.1',8080) do |socket| + socket.puts Time.now + end + +## Install + + $ gem install threaded_server + +## Copyright + +Copyright (c) 2011 Hal Brodigan + +See {file:LICENSE.txt} for details. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..182a621 --- /dev/null +++ b/Rakefile @@ -0,0 +1,38 @@ +require 'rubygems' +require 'rake' + +begin + gem 'ore-tasks', '~> 0.4' + require 'ore/tasks' + + Ore::Tasks.new +rescue LoadError => e + STDERR.puts e.message + STDERR.puts "Run `gem install ore-tasks` to install 'ore/tasks'." +end + +begin + gem 'rspec', '~> 2.4' + require 'rspec/core/rake_task' + + RSpec::Core::RakeTask.new +rescue LoadError => e + task :spec do + abort "Please run `gem install rspec` to install RSpec." + end +end + +task :test => :spec +task :default => :spec + +begin + gem 'yard', '~> 0.6.0' + require 'yard' + + YARD::Rake::YardocTask.new +rescue LoadError => e + task :yard do + abort "Please run `gem install yard` to install YARD." + end +end +task :doc => :yard diff --git a/gemspec.yml b/gemspec.yml new file mode 100644 index 0000000..b5e7e8f --- /dev/null +++ b/gemspec.yml @@ -0,0 +1,16 @@ +name: threaded_server +version: 0.1.0 +summary: Similar to GServer, but with a Thread Pool. +description: + A generic TCP Server with a fixed-size Thread Pool. + +license: MIT +authors: Postmodern +email: postmodern.mod3@gmail.com +homepage: http://github.com/postmodern/threaded_server +has_yard: true + +development_dependencies: + ore-tasks: ~> 0.4 + rspec: ~> 2.4 + yard: ~> 0.6.0 diff --git a/lib/threaded_server.rb b/lib/threaded_server.rb new file mode 100644 index 0000000..5b6c23f --- /dev/null +++ b/lib/threaded_server.rb @@ -0,0 +1,164 @@ +require 'socket' +require 'thread' + +# +# A generic TCP Server with a fixed-size Thread Pool. +# +class ThreadedServer + + # Default maximum number of Connections + MAX_CONNECTIONS = 64 + + # The host the server will listen on + attr_reader :host + + # The port the server will listen on + attr_reader :port + + # The maximum number of active connections + attr_reader :max_connections + + # + # Creates a new Threaded Server. + # + # @param [IPAddr, String] host + # The host to listen on. + # + # @param [Integer] port + # The port to listen on. + # + # @param [Hash] options + # Additional options. + # + # @option options [Integer] :max_connections (MAX_CONNECTIONS) + # The maximum number of active connections. + # + # @option options [#call] :handler + # The handler that will be passed new connections. + # + # @yield [connection] + # If a block is given, it will be passed the new connections. + # + # @yieldparam [TCPSocket] connection + # A new connection. + # + def initialize(host,port,options={},&block) + @host = host.to_s + @port = port + + @max_connections = options.fetch(:max_connections,MAX_CONNECTIONS) + @queue = SizedQueue.new(@max_connections) + @handler = options.fetch(:handler,block) + + @thread_pool = Array.new(@max_connections) do + Thread.new do + loop do + # receive a pending connection + client = @queue.pop + + begin + # handle the connection + @handler.call(client) + rescue => error + on_error(client,error) + end + + # close the connection + client.close + end + end + end + end + + # + # Creates a new Threaded Server and begins listening for connections. + # + # @param [IPAddr, String] host + # The host to listen on. + # + # @param [Integer] port + # The port to listen on. + # + # @param [Hash] options + # Additional options. + # + # @option options [Integer] :max_connections (MAX_CONNECTIONS) + # The maximum number of active connections. + # + # @option options [#call] :handler + # The handler that will be passed new connections. + # + # @yield [connection] + # If a block is given, it will be passed the new connections. + # + # @yieldparam [TCPSocket] connection + # A new connection. + # + # @see #listen + # + def self.open(host,port,options={},&block) + server = new(host,port,options,&block) + server.listen + end + + # + # Opens a listening TCP socket and begins accepting connections. + # + def listen + @server = TCPServer.new(@host,@port) + + @server.listen(@max_connections) + + loop do + client = begin + @server.accept + rescue + next + end + + on_connect(client) + @queue.push(client) + end + end + + # + # Closes the listening TCP socket. + # + def close + @server.close + end + + protected + + # + # Place holder method that will be passed newly connected sockets. + # + # @param [TCPSocket] socket + # The newly connected socket. + # + def on_connect(socket) + end + + # + # Place holder method that will be passed soon to be closed sockets. + # + # @param [TCPSocket] socket + # The socket that will be closed. + # + def on_disconnect(socket) + end + + # + # Place holder method that will be passed any exceptions that occurred. + # + # @param [TCPSocket] socket + # The socket that raised the exception. + # + # @param [Exception] error + # The exception. + # + def on_error(socket,error) + STDERR.puts "#{error.class}: #{error.message}" + end + +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..cf6ec7b --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,5 @@ +gem 'rspec', '~> 2.4' +require 'rspec' +require 'threaded_server/version' + +include ThreadedServer diff --git a/spec/threaded_server_spec.rb b/spec/threaded_server_spec.rb new file mode 100644 index 0000000..c4ce4c0 --- /dev/null +++ b/spec/threaded_server_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'threaded_server' + +describe ThreadedServer do + it "should have a VERSION constant" do + subject.const_get('VERSION').should_not be_empty + end +end diff --git a/threaded_server.gemspec b/threaded_server.gemspec new file mode 100644 index 0000000..32b45f0 --- /dev/null +++ b/threaded_server.gemspec @@ -0,0 +1,15 @@ +# -*- encoding: utf-8 -*- + +begin + Ore::Specification.new do |gemspec| + # custom logic here + end +rescue NameError + begin + require 'ore/specification' + retry + rescue LoadError + STDERR.puts "The '#{__FILE__}' file requires Ore." + STDERR.puts "Run `gem install ore-core` to install Ore." + end +end