tserver 0.2.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/CHANGELOG +21 -0
- data/LICENSE +21 -0
- data/README +51 -0
- data/lib/tserver.rb +373 -0
- data/test/example_server.rb +51 -0
- data/test/tserver_test.rb +412 -0
- metadata +53 -0
data/CHANGELOG
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
= SVN
|
2
|
+
|
3
|
+
= 0.2.0
|
4
|
+
|
5
|
+
* Server.process do not have argument now
|
6
|
+
* TServer.connection, TServer.connection_addr and TServer.terminate_listener? is available.
|
7
|
+
* Reload method (terminate all exists listerners and spawn new without interupt service and established connection)
|
8
|
+
* TServer.connections work when a listener don't have active connection
|
9
|
+
* Testing server Logger output in './test.log'
|
10
|
+
* Plurialize methods: Tserver.listeners, TServer.waiting_listeners
|
11
|
+
* Add 'server' task to Rakefile: run 'test/exemple_server.rb'
|
12
|
+
* Add multiple callback to log event, can be overrited
|
13
|
+
* Use Logger for logging implementation
|
14
|
+
|
15
|
+
= 0.1.1
|
16
|
+
|
17
|
+
* Tests pass with 'win32' platform
|
18
|
+
|
19
|
+
= 0.1.0
|
20
|
+
|
21
|
+
* First release
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2007 Yann Lugrin
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
= TServer
|
2
|
+
|
3
|
+
Author:: Yann Lugrin (mailto:yann.lugrin@sans-savoir.net)
|
4
|
+
Copyright:: Copyright (c) 2007 Yann Lugrin
|
5
|
+
Licence:: MIT[link://files/LICENSE.html]
|
6
|
+
Version:: 0.2.0
|
7
|
+
|
8
|
+
This librarie implements a persistant multithread TCP server, it is alternative
|
9
|
+
to 'gserver'[http://ruby-doc.org/stdlib/libdoc/gserver/rdoc/index.html] standard
|
10
|
+
librarie. TServer is designed to be inherited by your custom server class. The
|
11
|
+
server can accepts multiple simultaneous connections from clients, can be
|
12
|
+
configured to have a maximum connection and a minimum permanent listener thread.
|
13
|
+
Can be imediatly stopped, gracefull shutdown (dont accept new connection but
|
14
|
+
wait established connection is closed before realy stop) or reloaded (terminate
|
15
|
+
listener after established connection is closed and respawn new).
|
16
|
+
|
17
|
+
== Example
|
18
|
+
|
19
|
+
This example can receive simple string from telnet connection
|
20
|
+
|
21
|
+
require 'tserver'
|
22
|
+
|
23
|
+
# A server can return
|
24
|
+
class ExampleServer < TServer
|
25
|
+
def process
|
26
|
+
connection.each do |line|
|
27
|
+
break if line =~ /(quit|exit|close)/
|
28
|
+
|
29
|
+
log '> ' + line.chomp
|
30
|
+
conn.puts Time.now.to_s + '> ' + line.chomp
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Create the server with logging enabled (server activity is displayed
|
36
|
+
# in console with received data)
|
37
|
+
server = ExampleServer.new
|
38
|
+
server.verbose = true
|
39
|
+
|
40
|
+
# Shutdown the server when script is interupted
|
41
|
+
Signal.trap('SIGINT') do
|
42
|
+
server.shutdown
|
43
|
+
end
|
44
|
+
|
45
|
+
# Start the server (joined is set to true and the line wait on server
|
46
|
+
# thread before continue, the default values of this parameter is set to
|
47
|
+
# false, you can also use 'server.join' after server.start)
|
48
|
+
server.start(true)
|
49
|
+
|
50
|
+
# Now you can open a telnet connection to 127.0.0.1:10001 (telnet 127.0.0.1 10001)
|
51
|
+
# and send text (use exit to close the connection)
|
data/lib/tserver.rb
ADDED
@@ -0,0 +1,373 @@
|
|
1
|
+
#--
|
2
|
+
# The MIT License
|
3
|
+
#
|
4
|
+
# Copyright (c) 2007 Yann Lugrin
|
5
|
+
#
|
6
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
7
|
+
# of this software and associated documentation files (the "Software"), to deal
|
8
|
+
# in the Software without restriction, including without limitation the rights
|
9
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
+
# copies of the Software, and to permit persons to whom the Software is
|
11
|
+
# furnished to do so, subject to the following conditions:
|
12
|
+
#
|
13
|
+
# The above copyright notice and this permission notice shall be included in
|
14
|
+
# all copies or substantial portions of the Software.
|
15
|
+
#
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
22
|
+
# THE SOFTWARE.
|
23
|
+
#++
|
24
|
+
|
25
|
+
require 'socket'
|
26
|
+
require 'thread'
|
27
|
+
require 'thwait'
|
28
|
+
require 'monitor'
|
29
|
+
require 'logger'
|
30
|
+
|
31
|
+
# Show README[link://files/README.html] for implementation example.
|
32
|
+
class TServer
|
33
|
+
|
34
|
+
# Server port (default: 10001).
|
35
|
+
attr_reader :port
|
36
|
+
|
37
|
+
# Server host (default: 127.0.0.1).
|
38
|
+
attr_reader :host
|
39
|
+
|
40
|
+
# Maximum simultaneous connection can be established with server (default: 4, minimum: 1).
|
41
|
+
attr_reader :max_connection
|
42
|
+
|
43
|
+
# Minimum listener permanently spawned (default: 1, minimum: 0).
|
44
|
+
attr_reader :min_listener
|
45
|
+
|
46
|
+
# Server logger instance (default level: Logger:WARN, default output: stderr).
|
47
|
+
attr_reader :logger
|
48
|
+
|
49
|
+
DEFAULT_OPTIONS = {
|
50
|
+
:port => 10001,
|
51
|
+
:host => '127.0.0.1',
|
52
|
+
:max_connection => 4,
|
53
|
+
:min_listener => 1,
|
54
|
+
:log_level => Logger::WARN,
|
55
|
+
:stdlog => $stderr }
|
56
|
+
|
57
|
+
# Initialize a new server (use start to run the server).
|
58
|
+
#
|
59
|
+
# Options are:
|
60
|
+
# * <tt>:port</tt> - Port which the server listen on (default: 10001).
|
61
|
+
# * <tt>:host</tt> - IP which the server listen on (default: 127.0.0.1).
|
62
|
+
# * <tt>:max_connection</tt> - Maximum number of simultaneous connection to server (default: 4, minimum: 1).
|
63
|
+
# * <tt>:min_listener</tt> - Minimum number of listener thread (default: 1, minimum: 0).
|
64
|
+
# * <tt>:log_level</tt> - Use Logger constants DEBUG, INFO, WARN, ERROR or FATAL to set log level (default: Logger:WARN).
|
65
|
+
# * <tt>:stdlog</tt> - IO or filepath for log output (default: $stderr).
|
66
|
+
def initialize(options = {})
|
67
|
+
options = DEFAULT_OPTIONS.merge(options)
|
68
|
+
|
69
|
+
@port = options[:port]
|
70
|
+
@host = options[:host]
|
71
|
+
|
72
|
+
@max_connection = options[:max_connection] < 1 ? 1 : options[:max_connection]
|
73
|
+
@min_listener = options[:min_listener] < 0 ? 0 : (options[:min_listener] > @max_connection ? @max_connection : options[:min_listener])
|
74
|
+
|
75
|
+
@logger = Logger.new(options[:stdlog])
|
76
|
+
@logger.level = options[:log_level]
|
77
|
+
|
78
|
+
@tcp_server = nil
|
79
|
+
@tcp_server_thread = nil
|
80
|
+
@connections = Queue.new
|
81
|
+
|
82
|
+
@listener_threads = []
|
83
|
+
@listener_threads.extend(MonitorMixin)
|
84
|
+
@listener_cond = @listener_threads.new_cond
|
85
|
+
|
86
|
+
@shutdown = false
|
87
|
+
end
|
88
|
+
|
89
|
+
# Start the server, if joined is set at true this method return only when
|
90
|
+
# the server is stopped (you can also use join method after start)
|
91
|
+
def start(joined = false)
|
92
|
+
@shutdown = false
|
93
|
+
@tcp_server = TCPServer.new(@host, @port)
|
94
|
+
|
95
|
+
@min_listener.times { spawn_listener }
|
96
|
+
Thread.pass while @connections.num_waiting < @min_listener
|
97
|
+
|
98
|
+
@tcp_server_thread = Thread.new do
|
99
|
+
begin
|
100
|
+
server_started
|
101
|
+
|
102
|
+
loop do
|
103
|
+
@listener_threads.synchronize do
|
104
|
+
if @connections.num_waiting == 0 && @listener_threads.size >= @max_connection
|
105
|
+
server_waiting_listener
|
106
|
+
@listener_cond.wait
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
server_waiting_connection
|
111
|
+
@connections << @tcp_server.accept rescue Thread.exit
|
112
|
+
spawn_listener if !@connections.empty? && @connections.num_waiting == 0
|
113
|
+
end
|
114
|
+
ensure
|
115
|
+
@tcp_server = nil
|
116
|
+
@tcp_server_thread = nil
|
117
|
+
|
118
|
+
server_stopped
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
join if joined
|
123
|
+
true
|
124
|
+
end
|
125
|
+
|
126
|
+
# Join the main thread of the server and return only when the server is stopped.
|
127
|
+
def join
|
128
|
+
@tcp_server_thread.join if @tcp_server_thread
|
129
|
+
end
|
130
|
+
|
131
|
+
# Stop imediatly the server (all established connection is interrupted).
|
132
|
+
def stop
|
133
|
+
@tcp_server.close rescue nil
|
134
|
+
@listener_threads.synchronize { @listener_threads.each {|l| l.exit} }
|
135
|
+
@tcp_server_thread.exit rescue nil
|
136
|
+
|
137
|
+
true
|
138
|
+
end
|
139
|
+
|
140
|
+
# Gracefull shutdown, the server can't accept new connection but wait current
|
141
|
+
# connection before exit.
|
142
|
+
def shutdown
|
143
|
+
return if stopped?
|
144
|
+
server_shutdown
|
145
|
+
|
146
|
+
@tcp_server.close rescue nil
|
147
|
+
Thread.pass until @connections.empty?
|
148
|
+
|
149
|
+
@listener_threads.synchronize do
|
150
|
+
@listener_threads.each do |listener|
|
151
|
+
listener[:terminate] = true
|
152
|
+
@connections << false
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
ThreadsWait.all_waits(*@listener_threads)
|
157
|
+
@tcp_server_thread.exit rescue nil
|
158
|
+
@tcp_server_thread = nil
|
159
|
+
|
160
|
+
true
|
161
|
+
end
|
162
|
+
|
163
|
+
# Reload the server
|
164
|
+
# * Spawn new listeners.
|
165
|
+
# * Terminate existing listeners when current connection is closed.
|
166
|
+
def reload
|
167
|
+
return if stopped?
|
168
|
+
|
169
|
+
listeners_to_exit = nil
|
170
|
+
@listener_threads.synchronize do
|
171
|
+
listeners_to_exit = @listener_threads.dup
|
172
|
+
@listener_threads.clear
|
173
|
+
end
|
174
|
+
|
175
|
+
listeners_to_exit.each do |listener|
|
176
|
+
listener[:connection].nil? ? listener.terminate : listener[:terminate] = true
|
177
|
+
end
|
178
|
+
|
179
|
+
@listener_threads.synchronize do
|
180
|
+
spawn_listener while @listener_threads.size < @min_listener
|
181
|
+
end
|
182
|
+
|
183
|
+
true
|
184
|
+
end
|
185
|
+
|
186
|
+
# Return the number of spawned listener.
|
187
|
+
def listeners
|
188
|
+
@listener_threads.synchronize { @listener_threads.size }
|
189
|
+
end
|
190
|
+
|
191
|
+
# Return the number of spawned listener waiting on new connection.
|
192
|
+
def waiting_listeners
|
193
|
+
@connections.num_waiting
|
194
|
+
end
|
195
|
+
|
196
|
+
# Returns an array of arrays, where each subarray contains:
|
197
|
+
# * address family: A string like "AF_INET" or "AF_INET6" if it is one of the commonly used families, the string "unknown:#" (where '#' is the address family number) if it is not one of the common ones. The strings map to the Socket::AF_* constants.
|
198
|
+
# * port: The port number.
|
199
|
+
# * name: Either the canonical name from looking the address up in the DNS, or the address in presentation format.
|
200
|
+
# * address: The address in presentation format (a dotted decimal string for IPv4, a hex string for IPv6).
|
201
|
+
def connections
|
202
|
+
@listener_threads.synchronize { @listener_threads.collect{|l| l[:connection].nil? ? nil : l[:connection].peeraddr } }.compact
|
203
|
+
end
|
204
|
+
|
205
|
+
# Return true if server running.
|
206
|
+
def started?
|
207
|
+
@listener_threads.synchronize { !@tcp_server_thread.nil? || @listener_threads.size > 0 }
|
208
|
+
end
|
209
|
+
|
210
|
+
# Return true if server dont running.
|
211
|
+
def stopped?
|
212
|
+
!started?
|
213
|
+
end
|
214
|
+
|
215
|
+
protected
|
216
|
+
|
217
|
+
# Override this method to implement a server, conn is a TCPSocket instance and
|
218
|
+
# is closed when this method return. Attribute 'connection' is available.
|
219
|
+
#
|
220
|
+
# Example (send 'Hello world!' string to client):
|
221
|
+
# def process
|
222
|
+
# connection.puts 'Hello world!'
|
223
|
+
# end
|
224
|
+
#
|
225
|
+
# For persistant connection, use loop and Timeout.timeout or Tserver.terminate_listener?
|
226
|
+
# to break (and terminate listener) when server shutdown or reload. If server stop,
|
227
|
+
# listener is killed but begin/ensure can be used to terminate current process.
|
228
|
+
def process
|
229
|
+
end
|
230
|
+
|
231
|
+
# Callback (call when server is started)
|
232
|
+
def server_started
|
233
|
+
@logger.info do
|
234
|
+
"server:#{Thread.current} [#{@host}:#{@port}] is started"
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
# Callback (call when server is stopped)
|
239
|
+
def server_stopped
|
240
|
+
@logger.info do
|
241
|
+
"server:#{Thread.current} [#{@host}:#{@port}] is stopped"
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# Callback (call when server shutdown, before is stopped)
|
246
|
+
def server_shutdown
|
247
|
+
@logger.info do
|
248
|
+
"server:#{Thread.current} [#{@host}:#{@port}] shutdown"
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# Callback (call when server wait new connection)
|
253
|
+
def server_waiting_connection
|
254
|
+
@logger.info do
|
255
|
+
"server:#{Thread.current} [#{@host}:#{@port}] wait on connection"
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# Callback (call when server wait free listener, don't accept new connection)
|
260
|
+
def server_waiting_listener
|
261
|
+
@logger.info do
|
262
|
+
"server:#{Thread.current} [#{@host}:#{@port}] wait on listener"
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# Callback (call when listener is spawned)
|
267
|
+
def listener_spawned
|
268
|
+
@logger.info do
|
269
|
+
"listener:#{Thread.current} is spawned by server:#{Thread.current} [#{@host}:#{@port}]"
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
# Callback (call when listener exit)
|
274
|
+
def listener_terminated
|
275
|
+
@logger.info do
|
276
|
+
"listener:#{Thread.current} is terminated"
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
# Callback (call when listener wait connection - free listener)
|
281
|
+
def listener_waiting_connection
|
282
|
+
@logger.info do
|
283
|
+
"listener:#{Thread.current} wait on connection from server:#{Thread.current} [#{@host}:#{@port}]"
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
# Callback (call when a connection is established with listener)
|
288
|
+
def connection_established
|
289
|
+
@logger.info do
|
290
|
+
"client:#{connection_addr[1]} #{connection_addr[2]}<#{connection_addr[3]}> is connected to listener:#{Thread.current}"
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
# Callback (call when the connection with listener close normally)
|
295
|
+
def connection_normally_closed
|
296
|
+
@logger.info do
|
297
|
+
"client:#{connection_addr[1]} #{connection_addr[2]}<#{connection_addr[3]}> is disconnected from listener:#{Thread.current}"
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
# Callback (call when the connection with listener do not close normally,
|
302
|
+
# reveive 'error' instance from rescue)
|
303
|
+
def connection_not_normally_closed(error)
|
304
|
+
@logger.warn do
|
305
|
+
"client:#{connection_addr[1]} #{connection_addr[2]}<#{connection_addr[3]}> make an error and is disconnected from listener:#{Thread.current}"
|
306
|
+
end
|
307
|
+
|
308
|
+
@logger.debug do
|
309
|
+
"#{error.class.to_s}: #{error.to_s}\n" +
|
310
|
+
error.backtrace.join("\n")
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
private
|
315
|
+
|
316
|
+
def spawn_listener #:nodoc:
|
317
|
+
listener_thread = Thread.new do
|
318
|
+
begin
|
319
|
+
listener_spawned
|
320
|
+
loop do
|
321
|
+
begin
|
322
|
+
listener_waiting_connection
|
323
|
+
self.connection = (@connections.empty? && (terminate_listener? || @connections.num_waiting >= @min_listener)) ? Thread.exit : @connections.pop
|
324
|
+
|
325
|
+
if connection.is_a?(TCPSocket)
|
326
|
+
connection_established
|
327
|
+
process
|
328
|
+
connection_normally_closed
|
329
|
+
else
|
330
|
+
Thread.exit
|
331
|
+
end
|
332
|
+
rescue => e
|
333
|
+
connection_not_normally_closed(e)
|
334
|
+
ensure
|
335
|
+
connection.close rescue nil
|
336
|
+
self.connection = nil
|
337
|
+
end
|
338
|
+
|
339
|
+
@listener_threads.synchronize { @listener_cond.signal }
|
340
|
+
end
|
341
|
+
ensure
|
342
|
+
@listener_threads.synchronize { @listener_threads.delete(Thread.current) }
|
343
|
+
listener_terminated
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
@listener_threads.synchronize { @listener_threads << listener_thread }
|
348
|
+
end
|
349
|
+
|
350
|
+
# Set connection for current Thread
|
351
|
+
def connection=(conn) #:nodoc:
|
352
|
+
Thread.current[:connection] = conn
|
353
|
+
end
|
354
|
+
|
355
|
+
# Return connection of current listener thread (or nil)
|
356
|
+
def connection
|
357
|
+
Thread.current[:connection]
|
358
|
+
end
|
359
|
+
|
360
|
+
# Return array with information for current thread connection:
|
361
|
+
# * address family: A string like "AF_INET" or "AF_INET6" if it is one of the commonly used families, the string "unknown:#" (where '#' is the address family number) if it is not one of the common ones. The strings map to the Socket::AF_* constants.
|
362
|
+
# * port: The port number.
|
363
|
+
# * name: Either the canonical name from looking the address up in the DNS, or the address in presentation format.
|
364
|
+
# * address: The address in presentation format (a dotted decimal string for IPv4, a hex string for IPv6).
|
365
|
+
def connection_addr
|
366
|
+
Thread.current[:connection_addr] ||= (Thread.current[:connection].nil? ? [nil] * 4 : Thread.current[:connection].peeraddr)
|
367
|
+
end
|
368
|
+
|
369
|
+
# Return true if server ask listener to exit (when shutdown or reload)
|
370
|
+
def terminate_listener?
|
371
|
+
Thread.current[:terminate] == true
|
372
|
+
end
|
373
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../lib/tserver')
|
2
|
+
|
3
|
+
# Example server, lauch this script and open a telnet to communicate
|
4
|
+
# with the server. The server print logging information and received data
|
5
|
+
# in console. The client receive a copy of sending data.
|
6
|
+
#
|
7
|
+
# Syntax example :
|
8
|
+
# ruby example_server.rb
|
9
|
+
# ruby example_server.rb 127.0.0.1 10001
|
10
|
+
# ruby example_server.rb 127.0.0.1
|
11
|
+
# ruby example_server.rb 10001
|
12
|
+
# ruby example_server.rb 10001 127.0.0.1
|
13
|
+
#
|
14
|
+
# Default values :
|
15
|
+
host = '127.0.0.1'
|
16
|
+
port = 10001
|
17
|
+
|
18
|
+
ARGV.each do |argv|
|
19
|
+
if argv =~ /^\d+$/
|
20
|
+
port = argv.to_i
|
21
|
+
elsif argv =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
|
22
|
+
host = argv
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# ExampleServer return string received from client.
|
27
|
+
# Send quit, exit or close to close connection or
|
28
|
+
# stop to kill server.
|
29
|
+
class ExampleServer < TServer
|
30
|
+
def process
|
31
|
+
connection.each do |line|
|
32
|
+
stop if line =~ /stop/
|
33
|
+
break if line =~ /(quit|exit|close)/
|
34
|
+
|
35
|
+
puts '> ' + line.chomp
|
36
|
+
connection.puts Time.now.to_s + ' > ' + line.chomp
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Start server
|
42
|
+
server = ExampleServer.new(:port => port, :host => host)
|
43
|
+
server.logger.level = Logger::DEBUG
|
44
|
+
|
45
|
+
Signal.trap('SIGINT') do
|
46
|
+
server.shutdown
|
47
|
+
end
|
48
|
+
|
49
|
+
server.start
|
50
|
+
|
51
|
+
sleep 0.5 while server.started?
|
@@ -0,0 +1,412 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'timeout'
|
3
|
+
require 'thread'
|
4
|
+
|
5
|
+
require File.expand_path(File.dirname(__FILE__) + '/../lib/tserver')
|
6
|
+
|
7
|
+
TEST_LOG = 'test.log'
|
8
|
+
SERVER_READER = Queue.new
|
9
|
+
|
10
|
+
# The test server can send received data to an IO
|
11
|
+
class TestServer < TServer
|
12
|
+
def initialize(options= {})
|
13
|
+
super(options)
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
# Send received data on IO and return the data to client
|
19
|
+
def process
|
20
|
+
loop do
|
21
|
+
SERVER_READER << string = connection.readline.chomp
|
22
|
+
connection.puts string
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# The test client can send and receive data to a server
|
28
|
+
class TestClient
|
29
|
+
def initialize(host, port)
|
30
|
+
@host = host
|
31
|
+
@port = port
|
32
|
+
|
33
|
+
@socket = nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def connect
|
37
|
+
@socket = TCPSocket.new(@host, @port)
|
38
|
+
Thread.pass
|
39
|
+
end
|
40
|
+
|
41
|
+
def close
|
42
|
+
@socket.close if @socket
|
43
|
+
Thread.pass
|
44
|
+
end
|
45
|
+
|
46
|
+
def send(string)
|
47
|
+
@socket.puts string
|
48
|
+
Thread.pass
|
49
|
+
end
|
50
|
+
|
51
|
+
def receive
|
52
|
+
@socket.readline.chomp
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class TServerTest < Test::Unit::TestCase
|
57
|
+
def setup
|
58
|
+
SERVER_READER.clear
|
59
|
+
|
60
|
+
@server = TestServer.new(:log_level => Logger::DEBUG, :stdlog => TEST_LOG)
|
61
|
+
@client = TestClient.new(@server.host, @server.port)
|
62
|
+
end
|
63
|
+
|
64
|
+
def teardown
|
65
|
+
@client.close rescue nil
|
66
|
+
|
67
|
+
@server.stop rescue nil
|
68
|
+
@server.join rescue nil # join the server to ensure is stopped before start the next test
|
69
|
+
end
|
70
|
+
|
71
|
+
def test_should_can_create_with_default_values
|
72
|
+
server = TServer.new
|
73
|
+
|
74
|
+
assert_equal 10001, server.port
|
75
|
+
assert_equal '127.0.0.1', server.host
|
76
|
+
|
77
|
+
assert_equal 4, server.max_connection
|
78
|
+
assert_equal 1, server.min_listener
|
79
|
+
|
80
|
+
assert_kind_of Logger, server.logger
|
81
|
+
assert_equal Logger::WARN, server.logger.level
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_should_can_create_with_custom_values
|
85
|
+
# Set all options
|
86
|
+
server = TServer.new(:port => 10002, :host => '192.168.1.1', :max_connection => 10, :min_listener => 2, :log_level => Logger::DEBUG)
|
87
|
+
|
88
|
+
assert_equal 10002, server.port
|
89
|
+
assert_equal '192.168.1.1', server.host
|
90
|
+
|
91
|
+
assert_equal 10, server.max_connection
|
92
|
+
assert_equal 2, server.min_listener
|
93
|
+
|
94
|
+
assert_kind_of Logger, server.logger
|
95
|
+
assert_equal Logger::DEBUG, server.logger.level
|
96
|
+
end
|
97
|
+
|
98
|
+
def test_should_dont_have_more_min_listener_that_of_max_connection
|
99
|
+
server = TServer.new(:max_connection => 5, :min_listener => 6)
|
100
|
+
|
101
|
+
assert_equal 5, server.max_connection
|
102
|
+
assert_equal 5, server.min_listener
|
103
|
+
end
|
104
|
+
|
105
|
+
def test_should_dont_have_minimum_max_connection_and_min_listener
|
106
|
+
server = TServer.new(:max_connection => 0, :min_listener => -1)
|
107
|
+
|
108
|
+
assert_equal 1, server.max_connection
|
109
|
+
assert_equal 0, server.min_listener
|
110
|
+
end
|
111
|
+
|
112
|
+
def test_should_be_started
|
113
|
+
# Start a server
|
114
|
+
assert_not_timeout('Server do not start') { @server.start }
|
115
|
+
assert @server.started?
|
116
|
+
assert !@server.stopped?
|
117
|
+
|
118
|
+
# Listener is spawned
|
119
|
+
assert_equal @server.min_listener, @server.listeners
|
120
|
+
assert_equal @server.min_listener, @server.waiting_listeners
|
121
|
+
end
|
122
|
+
|
123
|
+
def test_should_be_stopped
|
124
|
+
# Stop a non started server
|
125
|
+
assert_not_timeout('Server do not stop') { @server.stop }
|
126
|
+
|
127
|
+
# Start the server
|
128
|
+
assert_not_timeout('Server do not start') { @server.start }
|
129
|
+
|
130
|
+
# Stop the server
|
131
|
+
assert_not_timeout('Server do not stop'){ @server.stop }
|
132
|
+
|
133
|
+
# The server is stopped and dont accept connection
|
134
|
+
assert !@server.started?
|
135
|
+
assert @server.stopped?
|
136
|
+
assert_raise(RUBY_PLATFORM =~ /win32/ ? Errno::EBADF : Errno::ECONNREFUSED) do
|
137
|
+
@client.connect
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def test_should_be_stopped_with_established_connection
|
142
|
+
# Start the server and a client
|
143
|
+
assert_not_timeout('Server do not start') { @server.start }
|
144
|
+
assert_not_timeout('Client do not connect') { @client.connect }
|
145
|
+
|
146
|
+
# Shutdown the server
|
147
|
+
assert_not_timeout('Server do not stop') { @server.stop }
|
148
|
+
|
149
|
+
# The server is stopped and dont accept connection
|
150
|
+
assert !@server.started?
|
151
|
+
assert @server.stopped?
|
152
|
+
assert_raise(RUBY_PLATFORM =~ /win32/ ? Errno::EBADF : Errno::ECONNREFUSED) do
|
153
|
+
@client.connect
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def test_should_be_shutdown
|
158
|
+
# Shutdown a non started server
|
159
|
+
assert_not_timeout('Server do not shutdown') { @server.shutdown }
|
160
|
+
|
161
|
+
# Start the server
|
162
|
+
assert_not_timeout('Server do not start') { @server.start }
|
163
|
+
|
164
|
+
# Shutdown the server
|
165
|
+
assert_not_timeout('Server do not shutdown') { @server.shutdown }
|
166
|
+
|
167
|
+
# The server is stopped and dont accept connection
|
168
|
+
assert !@server.started?
|
169
|
+
assert @server.stopped?
|
170
|
+
assert_raise(RUBY_PLATFORM =~ /win32/ ? Errno::EBADF : Errno::ECONNREFUSED) do
|
171
|
+
@client.connect
|
172
|
+
end
|
173
|
+
|
174
|
+
# Shutdown a shutdowned server
|
175
|
+
assert_not_timeout('Server do not shutdown') { @server.shutdown }
|
176
|
+
end
|
177
|
+
|
178
|
+
def test_should_be_shutdown_with_established_connection
|
179
|
+
# Start server and client
|
180
|
+
assert_not_timeout('Server do not start') { @server.start }
|
181
|
+
assert_not_timeout('Client do not connect') { @client.connect }
|
182
|
+
|
183
|
+
# Shutdown the server
|
184
|
+
shutdown_thread = nil
|
185
|
+
shutdown_thread = Thread.new do
|
186
|
+
assert_not_timeout('Server do not shutdown') { @server.shutdown }
|
187
|
+
end
|
188
|
+
|
189
|
+
# The server isn't stopped because a connection is established
|
190
|
+
assert @server.started?
|
191
|
+
assert !@server.stopped?
|
192
|
+
|
193
|
+
# The client can communicate with server
|
194
|
+
assert_not_timeout 'Client do not communicate with server' do
|
195
|
+
@client.send 'test string'
|
196
|
+
assert_equal 'test string', SERVER_READER.pop.chomp
|
197
|
+
assert_equal 'test string', @client.receive
|
198
|
+
end
|
199
|
+
|
200
|
+
# Close client
|
201
|
+
assert_not_timeout('Client do not close connection') { @client.close }
|
202
|
+
wait_listeners
|
203
|
+
|
204
|
+
# Wait server shutdown
|
205
|
+
assert_not_timeout('Server do not shutdown') { shutdown_thread.join }
|
206
|
+
|
207
|
+
# The server is stopped and dont accept connection
|
208
|
+
assert !@server.started?
|
209
|
+
assert @server.stopped?
|
210
|
+
assert_raise(RUBY_PLATFORM =~ /win32/ ? Errno::EBADF : Errno::ECONNREFUSED) do
|
211
|
+
@client.connect
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def test_should_be_reload
|
216
|
+
# Reload a non started server
|
217
|
+
assert_not_timeout('Server do not reload') { @server.reload }
|
218
|
+
|
219
|
+
# Do not spawn listeners!
|
220
|
+
assert_equal 0, @server.instance_variable_get(:@listener_threads).size
|
221
|
+
|
222
|
+
# Start the server
|
223
|
+
assert_not_timeout('Server do not start') { @server.start }
|
224
|
+
|
225
|
+
# The server is started and accept connection
|
226
|
+
assert_not_timeout('Client do not connect') { @client.connect }
|
227
|
+
|
228
|
+
# Copy list of current listeners
|
229
|
+
listeners_to_exit = @server.instance_variable_get(:@listener_threads).dup
|
230
|
+
|
231
|
+
# Reload the server
|
232
|
+
assert_not_timeout('Server do not reload') { @server.reload }
|
233
|
+
|
234
|
+
# Old listener is not terminated (connection with a client is established)
|
235
|
+
wait_listeners 2
|
236
|
+
assert_not_equal listeners_to_exit.first, @server.instance_variable_get(:@listener_threads).first
|
237
|
+
assert_not_equal, listeners_to_exit.first[:conn] = @server.instance_variable_get(:@listener_threads).first[:conn]
|
238
|
+
|
239
|
+
# The client can communicate with server
|
240
|
+
assert_not_timeout 'Client do not communicate with server' do
|
241
|
+
@client.send 'test string'
|
242
|
+
assert_equal 'test string', SERVER_READER.pop.chomp
|
243
|
+
assert_equal 'test string', @client.receive
|
244
|
+
end
|
245
|
+
|
246
|
+
# Close client
|
247
|
+
assert_not_timeout('Client do not close connection') { @client.close }
|
248
|
+
|
249
|
+
# Old listeners exit
|
250
|
+
ThreadsWait.all_waits(*listeners_to_exit)
|
251
|
+
|
252
|
+
# The server is started and accept connection
|
253
|
+
assert_not_timeout('Client do not connect') { @client.connect }
|
254
|
+
|
255
|
+
# The client can communicate with server
|
256
|
+
assert_not_timeout 'Client do not communicate with server' do
|
257
|
+
@client.send 'test string'
|
258
|
+
assert_equal 'test string', SERVER_READER.pop.chomp
|
259
|
+
assert_equal 'test string', @client.receive
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def test_should_be_receive_connection
|
264
|
+
# Start server and client
|
265
|
+
assert_not_timeout('Server do not start') { @server.start }
|
266
|
+
assert_not_timeout('Client do not connect') { @client.connect }
|
267
|
+
|
268
|
+
# Client can communicate with the server
|
269
|
+
assert_not_timeout 'Client do not communicate with server' do
|
270
|
+
@client.send 'test string'
|
271
|
+
assert_equal 'test string', SERVER_READER.pop.chomp
|
272
|
+
assert_equal 'test string', @client.receive
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def test_should_be_receive_multiple_connection
|
277
|
+
# Create multiple clients
|
278
|
+
@client_2 = TestClient.new(@server.host, @server.port)
|
279
|
+
@client_3 = TestClient.new(@server.host, @server.port)
|
280
|
+
@client_4 = TestClient.new(@server.host, @server.port)
|
281
|
+
@client_5 = TestClient.new(@server.host, @server.port)
|
282
|
+
|
283
|
+
# Start server and clients
|
284
|
+
assert_not_timeout('Server do not start') { @server.start }
|
285
|
+
assert_not_timeout('Client do not connect') { @client.connect }
|
286
|
+
assert_not_timeout('Client do not connect') { @client_2.connect }
|
287
|
+
assert_not_timeout('Client do not connect') { @client_3.connect }
|
288
|
+
assert_not_timeout('Client do not connect') { @client_4.connect }
|
289
|
+
assert_not_timeout('Client do not connect') { @client_5.connect }
|
290
|
+
wait_listeners(4)
|
291
|
+
|
292
|
+
# Only 4 listerner for 5 client
|
293
|
+
assert_equal 0, @server.waiting_listeners
|
294
|
+
assert_equal 4, @server.listeners
|
295
|
+
|
296
|
+
# All clients send data to server
|
297
|
+
assert_not_timeout('Client do not communicate with server') { @client.send 'test string 1' }
|
298
|
+
assert_not_timeout('Client do not communicate with server') { @client_2.send 'test string 2' }
|
299
|
+
assert_not_timeout('Client do not communicate with server') { @client_3.send 'test string 3' }
|
300
|
+
assert_not_timeout('Client do not communicate with server') { @client_4.send 'test string 4' }
|
301
|
+
assert_not_timeout('Client do not communicate with server') { @client_5.send 'test string 5' }
|
302
|
+
|
303
|
+
# Server receive data from 4 clients (but the last client waiting)
|
304
|
+
1.upto(4) do |i|
|
305
|
+
assert_not_timeout 'Do not receive data from client' do
|
306
|
+
assert_match(/test string [1-4]/, SERVER_READER.pop)
|
307
|
+
end
|
308
|
+
end
|
309
|
+
assert SERVER_READER.empty?, 'Server receive data from client 5'
|
310
|
+
|
311
|
+
# Close client
|
312
|
+
assert_not_timeout('Client do not close connection') { @client.close }
|
313
|
+
|
314
|
+
# Server can recerive data from last client
|
315
|
+
assert_not_timeout 'Do not receive data from client' do
|
316
|
+
assert_equal 'test string 5', SERVER_READER.pop
|
317
|
+
end
|
318
|
+
|
319
|
+
# Close all clients [<client>, <number of listener after close>]
|
320
|
+
[@client_2, @client_3, @client_4, @client_5].each do |client|
|
321
|
+
assert_not_timeout('Client do not close connection') { client.close }
|
322
|
+
end
|
323
|
+
wait_listeners
|
324
|
+
|
325
|
+
# min_listener waiting connection
|
326
|
+
assert_equal @server.min_listener, @server.listeners
|
327
|
+
assert_equal @server.min_listener, @server.waiting_listeners
|
328
|
+
end
|
329
|
+
|
330
|
+
def test_should_be_receive_connection_with_log_level_to_debug
|
331
|
+
@server.logger.level = Logger::DEBUG
|
332
|
+
|
333
|
+
# Start server and client
|
334
|
+
assert_not_timeout('Server do not start') { @server.start }
|
335
|
+
assert_not_timeout('Client do not connect') { @client.connect }
|
336
|
+
|
337
|
+
# Client can communicate with the server
|
338
|
+
assert_not_timeout 'Client do not communicate with server' do
|
339
|
+
@client.send 'test string'
|
340
|
+
assert_equal 'test string', SERVER_READER.pop.chomp
|
341
|
+
assert_equal 'test string', @client.receive
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
def test_should_works_with_min_listener_at_0
|
346
|
+
@server = TestServer.new(:min_listener => 0, :stdlog => TEST_LOG)
|
347
|
+
assert_equal 0, @server.min_listener
|
348
|
+
|
349
|
+
# Start server and client
|
350
|
+
assert_not_timeout('Server do not start') { @server.start }
|
351
|
+
assert_not_timeout('Client do not connect') { @client.connect }
|
352
|
+
|
353
|
+
# Client can communicate with the server
|
354
|
+
assert_not_timeout 'Client do not communicate with server' do
|
355
|
+
@client.send 'test string'
|
356
|
+
assert_equal 'test string', SERVER_READER.pop.chomp
|
357
|
+
assert_equal 'test string', @client.receive
|
358
|
+
end
|
359
|
+
|
360
|
+
# Close client
|
361
|
+
assert_not_timeout('Client do not close connection') { @client.close }
|
362
|
+
wait_listeners
|
363
|
+
|
364
|
+
# min_listener
|
365
|
+
assert_equal @server.min_listener, @server.listeners
|
366
|
+
assert_equal @server.min_listener, @server.waiting_listeners
|
367
|
+
end
|
368
|
+
|
369
|
+
def test_should_have_connection_list
|
370
|
+
# Start server
|
371
|
+
assert_not_timeout('Server do not start') { @server.start }
|
372
|
+
|
373
|
+
# Zero connection
|
374
|
+
assert_equal @server.min_listener, @server.listeners
|
375
|
+
assert_equal @server.min_listener, @server.waiting_listeners
|
376
|
+
assert_equal [], @server.connections
|
377
|
+
|
378
|
+
# Start client
|
379
|
+
assert_not_timeout('Client do not connect') { @client.connect }
|
380
|
+
wait_connection
|
381
|
+
|
382
|
+
# Connection information
|
383
|
+
assert_equal 1, @server.connections.size
|
384
|
+
assert_equal 'AF_INET', @server.connections.first[0]
|
385
|
+
assert_match(/^\d+$/, @server.connections.first[1].to_s)
|
386
|
+
assert_match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, @server.connections.first[3])
|
387
|
+
end
|
388
|
+
|
389
|
+
protected
|
390
|
+
|
391
|
+
def assert_not_timeout(msg = 'Timeout')
|
392
|
+
assert_nothing_raised(msg) do
|
393
|
+
Timeout.timeout(5) do
|
394
|
+
yield
|
395
|
+
end
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
# Wait listener spawn
|
400
|
+
def wait_listeners(number = @server.min_listener)
|
401
|
+
assert_not_timeout 'Listener do not exit' do
|
402
|
+
sleep 0.1 until @server.listeners == number
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
# Wait connection established with listener
|
407
|
+
def wait_connection(number = @server.listeners)
|
408
|
+
assert_not_timeout 'Listener do not exit' do
|
409
|
+
sleep 0.1 until @server.connections.size == number
|
410
|
+
end
|
411
|
+
end
|
412
|
+
end
|
metadata
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.4
|
3
|
+
specification_version: 1
|
4
|
+
name: tserver
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.2.0
|
7
|
+
date: 2007-10-11 00:00:00 +02:00
|
8
|
+
summary: A persistant multithread TCP server
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: yann.lugrin@sans-savoir.net
|
12
|
+
homepage: http://dev.sans-savoir.net/trac/tserver
|
13
|
+
rubyforge_project:
|
14
|
+
description:
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- Yann Lugrin
|
31
|
+
files:
|
32
|
+
- lib/tserver.rb
|
33
|
+
- test/example_server.rb
|
34
|
+
- test/tserver_test.rb
|
35
|
+
- README
|
36
|
+
- CHANGELOG
|
37
|
+
- LICENSE
|
38
|
+
test_files: []
|
39
|
+
|
40
|
+
rdoc_options: []
|
41
|
+
|
42
|
+
extra_rdoc_files:
|
43
|
+
- README
|
44
|
+
- CHANGELOG
|
45
|
+
- LICENSE
|
46
|
+
executables: []
|
47
|
+
|
48
|
+
extensions: []
|
49
|
+
|
50
|
+
requirements: []
|
51
|
+
|
52
|
+
dependencies: []
|
53
|
+
|