dat-tcp 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +6 -0
- data/Rakefile +6 -0
- data/dat-tcp.gemspec +25 -0
- data/lib/dat-tcp.rb +158 -0
- data/lib/dat-tcp/logger.rb +36 -0
- data/lib/dat-tcp/version.rb +3 -0
- data/lib/dat-tcp/workers.rb +99 -0
- metadata +101 -0
data/Gemfile
ADDED
data/Rakefile
ADDED
data/dat-tcp.gemspec
ADDED
@@ -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
|
data/lib/dat-tcp.rb
ADDED
@@ -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,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
|
+
|