dat-tcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|