dat-tcp 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in tcp_server.gemspec
4
+ gemspec
5
+
6
+ gem 'rake'
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'bench/tasks'
4
+
5
+ require "assert/rake_tasks"
6
+ Assert::RakeTasks.install
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'dat-tcp/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "dat-tcp"
8
+ gem.version = DatTCP::VERSION
9
+ gem.authors = ["Collin Redding"]
10
+ gem.email = ["collin.redding@me.com"]
11
+ gem.description = "DatTCP is a generic threaded TCP server implementation. It provides a " \
12
+ "simple to use interface for defining a TCP server. It is intended to be " \
13
+ "used as a base for application-level servers."
14
+ gem.summary = "DatTCP is a generic threaded TCP server for defining application-level " \
15
+ "servers."
16
+ gem.homepage = ""
17
+
18
+ gem.files = `git ls-files -- lib/* Gemfile Rakefile *.gemspec`.split($/)
19
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
20
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
21
+ gem.require_paths = ["lib"]
22
+
23
+ gem.add_development_dependency('assert', ['~>0.8'])
24
+ gem.add_development_dependency('assert-mocha', ['~>0.1'])
25
+ end
@@ -0,0 +1,158 @@
1
+ # DatTCP Server module is the main interface for defining a new server. It
2
+ # should be mixed in and provides methods for starting and stopping the main
3
+ # server loop. The `serve` method is intended to be overwritten so users can
4
+ # define handling connections. It's primary loop is:
5
+ #
6
+ # 1. Wait for worker
7
+ # 1. Accept connection
8
+ # 2. Process connection by handing off to worker
9
+ #
10
+ # This is repeated until the server is stopped.
11
+ #
12
+ # Options:
13
+ # `max_workers` - (integer) The maximum number of workers for processing
14
+ # connections. More threads causes more concurrency but also
15
+ # more overhead. This defaults to 4 workers.
16
+ # `debug` - (boolean) Whether or not to have the server log debug
17
+ # messages for when the server starts and stops and when a
18
+ # client connects and disconnects.
19
+ # `ready_timeout` - (float) The timeout used with `IO.select` waiting for a
20
+ # connection to occur. This can be set to 0 to not wait at
21
+ # all. Defaults to 1 (second).
22
+ #
23
+ require 'logger'
24
+ require 'socket'
25
+ require 'thread'
26
+
27
+ require 'dat-tcp/logger'
28
+ require 'dat-tcp/workers'
29
+ require 'dat-tcp/version'
30
+
31
+ module DatTCP
32
+
33
+ module Server
34
+ attr_reader :host, :port, :workers, :debug, :logger, :ready_timeout
35
+ attr_reader :tcp_server, :thread
36
+
37
+ def initialize(host, port, options = nil)
38
+ options ||= {}
39
+ options[:max_workers] ||= 4
40
+
41
+ @host, @port = [ host, port ]
42
+ @logger = DatTCP::Logger.new(options[:debug])
43
+ @workers = DatTCP::Workers.new(options[:max_workers], self.logger)
44
+ @ready_timeout = options[:ready_timeout] || 1
45
+
46
+ @mutex = Mutex.new
47
+ @condition_variable = ConditionVariable.new
48
+ end
49
+
50
+ def start
51
+ if !self.running?
52
+ @shutdown = false
53
+ !!self.start_server_thread
54
+ else
55
+ false
56
+ end
57
+ end
58
+
59
+ def stop
60
+ if self.running?
61
+ @shutdown = true
62
+ @mutex.synchronize do
63
+ while self.thread
64
+ @condition_variable.wait(@mutex)
65
+ end
66
+ end
67
+ else
68
+ false
69
+ end
70
+ end
71
+
72
+ def join_thread(limit = nil)
73
+ @thread.join(limit) if self.running?
74
+ end
75
+
76
+ def running?
77
+ !!@thread
78
+ end
79
+
80
+ # This method should be overwritten to handle new connections
81
+ def serve(socket)
82
+ end
83
+
84
+ def name
85
+ "#{self.class}|#{self.host}:#{self.port}"
86
+ end
87
+
88
+ def inspect
89
+ reference = '0x0%x' % (self.object_id << 1)
90
+ "#<#{self.class}:#{reference} @host=#{self.host.inspect} @port=#{self.port.inspect}>"
91
+ end
92
+
93
+ protected
94
+
95
+ def start_server_thread
96
+ @tcp_server = TCPServer.new(self.host, self.port)
97
+ @mutex.synchronize do
98
+ @thread = Thread.new{ self.work_loop }
99
+ end
100
+ end
101
+
102
+ # Notes:
103
+ # * If the server has been shutdown, then `accept_connection` will return
104
+ # `nil` always. This will exit the loop and begin shutting down the server.
105
+ def work_loop
106
+ self.logger.info("Starting...")
107
+ while !@shutdown
108
+ connection = self.accept_connection
109
+ self.workers.process(connection){|client| self.serve(client) } if connection
110
+ end
111
+ rescue Exception => exception
112
+ self.logger.info("Exception occurred, stopping server!")
113
+ ensure
114
+ self.shutdown_server_thread(exception)
115
+ end
116
+
117
+ # This method is a accept-loop waiting for a new connection. It uses
118
+ # `IO.select` with a timeout to wait for a socket to be ready. Once the
119
+ # socket is ready, it calls `accept` and returns the connection. If the
120
+ # server socket doesn't have a new connection waiting, the loop starts over.
121
+ # In the case the server has been shutdown, it will also break out of the
122
+ # loop.
123
+ #
124
+ # Notes:
125
+ # * If the server has been shutdown this will return `nil`.
126
+ def accept_connection
127
+ loop do
128
+ if IO.select([ @tcp_server ], nil, nil, self.ready_timeout)
129
+ return @tcp_server.accept
130
+ elsif @shutdown
131
+ return
132
+ end
133
+ end
134
+ end
135
+
136
+ # Notes:
137
+ # * Stopping the workers is a graceful shutdown. It will let them each finish
138
+ # processing by joining their threads.
139
+ def shutdown_server_thread(exception = nil)
140
+ self.logger.info("Stopping...")
141
+ @tcp_server.close rescue false
142
+ self.logger.info(" letting any running workers finish...")
143
+ self.workers.finish
144
+ self.logger.info("Stopped")
145
+ if exception
146
+ self.logger.error("#{exception.class}: #{exception.message}")
147
+ self.logger.error(exception.backtrace.join("\n"))
148
+ end
149
+ @mutex.synchronize do
150
+ @thread = nil
151
+ @condition_variable.signal
152
+ end
153
+ true
154
+ end
155
+
156
+ end
157
+
158
+ end
@@ -0,0 +1,36 @@
1
+ # DatTCP's logger module acts as a generator for either a debug or null logger.
2
+ # This allows the server and workers to always assume they have some logger
3
+ # object and not have to worry about conditionally checking if a logger is
4
+ # present. The null logger takes all messages and does nothing with them. When
5
+ # debug mode is turned off, this logger is used, which keeps the server from
6
+ # logging. The debug logger uses an instance of ruby's standard logger and
7
+ # writes to STDOUT.
8
+ #
9
+ require 'logger'
10
+
11
+ module DatTCP::Logger
12
+
13
+ def self.new(debug)
14
+ !!debug ? DatTCP::Logger::Debug.new : DatTCP::Logger::Null.new
15
+ end
16
+
17
+ module Debug
18
+
19
+ def self.new
20
+ ::Logger.new(STDOUT).tap do |logger|
21
+ logger.progname = "[#{self.name}]"
22
+ logger.datetime_format = "%m/%d/%Y %H:%M:%S%p "
23
+ end
24
+ end
25
+
26
+ end
27
+
28
+ class Null
29
+
30
+ Logger::Severity.constants.each do |name|
31
+ define_method(name.downcase){|*args| } # no-op
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,3 @@
1
+ module DatTCP
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,99 @@
1
+ # DatTCP's workers is a class for managing the worker threads that are
2
+ # spun up to handle clients. It manages a list of working threads and provides
3
+ # external methods for working with them. Working threads are managed by
4
+ # creating a new one when `process` is called. A client connection and a block
5
+ # are passed to the worker thread for it to handle the connection. Once it's
6
+ # done handling the thread, the connection is closed and the thread is removed
7
+ # from the list. This iignals some of the other methods that the workers class
8
+ # provides that wait on threads to finish.
9
+ #
10
+ require 'thread'
11
+
12
+ require 'dat-tcp/logger'
13
+
14
+ module DatTCP
15
+
16
+ class Workers
17
+ attr_reader :max, :list, :logger
18
+
19
+ def initialize(max = 1, logger = nil)
20
+ @max = max
21
+ @logger = logger || DatTCP::Logger::Null.new
22
+ @list = []
23
+
24
+ @mutex = Mutex.new
25
+ @condition_variable = ConditionVariable.new
26
+ end
27
+
28
+ # This uses the mutex and condition variable to sleep the current thread
29
+ # until signaled. When a thread closes down, it signals, causing this thread
30
+ # to wakeup. This will run the loop again, and as long as a worker is
31
+ # available, the method will return. Otherwise it will sleep again waiting
32
+ # to be signaled.
33
+ def wait_for_available
34
+ @mutex.synchronize do
35
+ while self.list.size >= self.max
36
+ @condition_variable.wait(@mutex)
37
+ end
38
+ end
39
+ end
40
+
41
+ def process(connecting_socket, &block)
42
+ self.wait_for_available
43
+ worker_id = self.list.size + 1
44
+ @list << Thread.new{ self.serve_client(worker_id, connecting_socket, &block) }
45
+ end
46
+
47
+ # Finish simply sleeps the current thread until signaled. Again, when a
48
+ # worker thread closes down, it signals. This will cause this to wake up and
49
+ # continue running the loop. Once the list is empty, the method will return.
50
+ # Otherwise this will sleep until signaled again. This is a graceful
51
+ # shutdown, letting the threads finish their processing.
52
+ def finish
53
+ @mutex.synchronize do
54
+ @list.reject!{|thread| !thread.alive? }
55
+ while !self.list.empty?
56
+ @condition_variable.wait(@mutex)
57
+ end
58
+ end
59
+ end
60
+
61
+ protected
62
+
63
+ def log(message, worker_id)
64
+ self.logger.info("[Worker##{worker_id}] #{message}") if self.logger
65
+ end
66
+
67
+ def serve_client(worker_id, connecting_socket, &block)
68
+ begin
69
+ Thread.current["client_address"] = connecting_socket.peeraddr[1, 2].reverse.join(':')
70
+ self.log("Connecting #{Thread.current["client_address"]}", worker_id)
71
+ block.call(connecting_socket)
72
+ rescue Exception => exception
73
+ self.log("Exception occurred, stopping worker", worker_id)
74
+ ensure
75
+ self.disconnect_client(worker_id, connecting_socket, exception)
76
+ end
77
+ end
78
+
79
+ # Closes the client connection and also shuts the thread down. This is done
80
+ # by removing the thread from the list. This is wrapped in a mutex
81
+ # synchronize, to ensure only one thread interacts with list at a time. Also
82
+ # the condition variable is signaled to trigger the `finish` or
83
+ # `wait_for_available` methods.
84
+ def disconnect_client(worker_id, connecting_socket, exception)
85
+ connecting_socket.close rescue false
86
+ @mutex.synchronize do
87
+ @list.delete(Thread.current)
88
+ @condition_variable.signal
89
+ end
90
+ if exception
91
+ self.log("#{exception.class}: #{exception.message}", worker_id)
92
+ self.log(exception.backtrace.join("\n"), worker_id)
93
+ end
94
+ self.log("Disconnecting #{Thread.current["client_address"]}", worker_id)
95
+ end
96
+
97
+ end
98
+
99
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dat-tcp
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Collin Redding
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-11-14 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ prerelease: false
22
+ version_requirements: &id001 !ruby/object:Gem::Requirement
23
+ none: false
24
+ requirements:
25
+ - - ~>
26
+ - !ruby/object:Gem::Version
27
+ hash: 27
28
+ segments:
29
+ - 0
30
+ - 8
31
+ version: "0.8"
32
+ requirement: *id001
33
+ name: assert
34
+ type: :development
35
+ - !ruby/object:Gem::Dependency
36
+ prerelease: false
37
+ version_requirements: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ~>
41
+ - !ruby/object:Gem::Version
42
+ hash: 9
43
+ segments:
44
+ - 0
45
+ - 1
46
+ version: "0.1"
47
+ requirement: *id002
48
+ name: assert-mocha
49
+ type: :development
50
+ description: DatTCP is a generic threaded TCP server implementation. It provides a simple to use interface for defining a TCP server. It is intended to be used as a base for application-level servers.
51
+ email:
52
+ - collin.redding@me.com
53
+ executables: []
54
+
55
+ extensions: []
56
+
57
+ extra_rdoc_files: []
58
+
59
+ files:
60
+ - Gemfile
61
+ - Rakefile
62
+ - dat-tcp.gemspec
63
+ - lib/dat-tcp.rb
64
+ - lib/dat-tcp/logger.rb
65
+ - lib/dat-tcp/version.rb
66
+ - lib/dat-tcp/workers.rb
67
+ homepage: ""
68
+ licenses: []
69
+
70
+ post_install_message:
71
+ rdoc_options: []
72
+
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ hash: 3
81
+ segments:
82
+ - 0
83
+ version: "0"
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ hash: 3
90
+ segments:
91
+ - 0
92
+ version: "0"
93
+ requirements: []
94
+
95
+ rubyforge_project:
96
+ rubygems_version: 1.8.15
97
+ signing_key:
98
+ specification_version: 3
99
+ summary: DatTCP is a generic threaded TCP server for defining application-level servers.
100
+ test_files: []
101
+