├── Rakefile
├── lib
├── gserver
│ └── version.rb
└── gserver.rb
├── Gemfile
├── .gitignore
├── gserver.gemspec
├── README.md
├── LICENSE.txt
└── sample
└── xmlrpc.rb
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 |
3 |
--------------------------------------------------------------------------------
/lib/gserver/version.rb:
--------------------------------------------------------------------------------
1 | module Gserver
2 | VERSION = "0.0.1"
3 | end
4 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gemspec
4 |
5 | gem "bundler"
6 | gem "rake"
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /Gemfile.lock
4 | /_yardoc/
5 | /coverage/
6 | /doc/
7 | /pkg/
8 | /spec/reports/
9 | /tmp/
10 | *.bundle
11 | *.so
12 | *.o
13 | *.a
14 | mkmf.log
15 |
--------------------------------------------------------------------------------
/gserver.gemspec:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | lib = File.expand_path('../lib', __FILE__)
3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4 | require 'gserver/version'
5 |
6 | Gem::Specification.new do |spec|
7 | spec.name = "gserver"
8 | spec.version = Gserver::VERSION
9 | spec.authors = ["John W. Small", "SHIBATA Hiroshi"]
10 | spec.email = ["hsbt@ruby-lang.org"]
11 | spec.summary = %q{GServer implements a generic server}
12 | spec.description = %q{GServer implements a generic server}
13 | spec.homepage = ""
14 | spec.license = "Ruby"
15 |
16 | spec.files = Dir["{lib,sample}/**/*.rb", "README.md", "LICENSE.txt"]
17 | spec.require_paths = ["lib"]
18 | end
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Gserver
2 |
3 | GServer implements a generic server, featuring thread pool management,
4 | simple logging, and multi-server management. See HttpServer in
5 | sample/xmlrpc.rb in the Ruby standard library for an example of
6 | GServer in action.
7 |
8 | ## Installation
9 |
10 | Add this line to your application's Gemfile:
11 |
12 | ```ruby
13 | gem 'gserver'
14 | ```
15 |
16 | And then execute:
17 |
18 | $ bundle
19 |
20 | Or install it yourself as:
21 |
22 | $ gem install gserver
23 |
24 | ## Usage
25 |
26 | Using GServer is simple. Below we implement a simple time server, run it,
27 | query it, and shut it down. Try this code in `irb`:
28 |
29 | ```ruby
30 | require 'gserver'
31 |
32 | #
33 | # A server that returns the time in seconds since 1970.
34 | #
35 | class TimeServer < GServer
36 | def initialize(port=10001, *args)
37 | super(port, *args)
38 | end
39 | def serve(io)
40 | io.puts(Time.now.to_i)
41 | end
42 | end
43 |
44 | # Run the server with logging enabled (it's a separate thread).
45 | server = TimeServer.new
46 | server.audit = true # Turn logging on.
47 | server.start
48 | ```
49 |
50 | Now, point your browser to http://localhost:10001 to see it working.
51 |
52 | ```ruby
53 | # See if it's still running.
54 | GServer.in_service?(10001) # -> true
55 | server.stopped? # -> false
56 |
57 | # Shut the server down gracefully.
58 | server.shutdown
59 |
60 | # Alternatively, stop it immediately.
61 | GServer.stop(10001)
62 | # or, of course, "server.stop".
63 | ```
64 |
65 | All the business of accepting connections and exception handling is taken
66 | care of. All we have to do is implement the method that actually serves the
67 | client.
68 |
69 | ## Contributing
70 |
71 | 1. Fork it ( https://github.com/ruby/gserver/fork )
72 | 2. Create your feature branch (`git checkout -b my-new-feature`)
73 | 3. Commit your changes (`git commit -am 'Add some feature'`)
74 | 4. Push to the branch (`git push origin my-new-feature`)
75 | 5. Create a new Pull Request
76 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Ruby is copyrighted free software by Yukihiro Matsumoto .
2 | You can redistribute it and/or modify it under either the terms of the
3 | 2-clause BSDL (see the file BSDL), or the conditions below:
4 |
5 | 1. You may make and give away verbatim copies of the source form of the
6 | software without restriction, provided that you duplicate all of the
7 | original copyright notices and associated disclaimers.
8 |
9 | 2. You may modify your copy of the software in any way, provided that
10 | you do at least ONE of the following:
11 |
12 | a) place your modifications in the Public Domain or otherwise
13 | make them Freely Available, such as by posting said
14 | modifications to Usenet or an equivalent medium, or by allowing
15 | the author to include your modifications in the software.
16 |
17 | b) use the modified software only within your corporation or
18 | organization.
19 |
20 | c) give non-standard binaries non-standard names, with
21 | instructions on where to get the original software distribution.
22 |
23 | d) make other distribution arrangements with the author.
24 |
25 | 3. You may distribute the software in object code or binary form,
26 | provided that you do at least ONE of the following:
27 |
28 | a) distribute the binaries and library files of the software,
29 | together with instructions (in the manual page or equivalent)
30 | on where to get the original distribution.
31 |
32 | b) accompany the distribution with the machine-readable source of
33 | the software.
34 |
35 | c) give non-standard binaries non-standard names, with
36 | instructions on where to get the original software distribution.
37 |
38 | d) make other distribution arrangements with the author.
39 |
40 | 4. You may modify and include the part of the software into any other
41 | software (possibly commercial). But some files in the distribution
42 | are not written by the author, so that they are not under these terms.
43 |
44 | For the list of those files and their copying conditions, see the
45 | file LEGAL.
46 |
47 | 5. The scripts and library files supplied as input to or produced as
48 | output from the software do not automatically fall under the
49 | copyright of the software, but belong to whomever generated them,
50 | and may be sold commercially, and may be aggregated with this
51 | software.
52 |
53 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
54 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
55 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
56 | PURPOSE.
57 |
--------------------------------------------------------------------------------
/sample/xmlrpc.rb:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2001, 2002, 2003 by Michael Neumann (mneumann@ntecs.de)
2 | #
3 | # $Id$
4 | #
5 |
6 |
7 | require "gserver"
8 |
9 | # Implements a simple HTTP-server by using John W. Small's (jsmall@laser.net)
10 | # ruby-generic-server: GServer.
11 | class HttpServer < GServer
12 |
13 | ##
14 | # +handle_obj+ specifies the object, that receives calls from +request_handler+
15 | # and +ip_auth_handler+
16 | def initialize(handle_obj, port = 8080, host = DEFAULT_HOST, maxConnections = 4,
17 | stdlog = $stdout, audit = true, debug = true)
18 | @handler = handle_obj
19 | super(port, host, maxConnections, stdlog, audit, debug)
20 | end
21 |
22 | private
23 |
24 | CRLF = "\r\n"
25 | HTTP_PROTO = "HTTP/1.0"
26 | SERVER_NAME = "HttpServer (Ruby #{RUBY_VERSION})"
27 |
28 | # Default header for the server name
29 | DEFAULT_HEADER = {
30 | "Server" => SERVER_NAME
31 | }
32 |
33 | # Mapping of status codes and error messages
34 | StatusCodeMapping = {
35 | 200 => "OK",
36 | 400 => "Bad Request",
37 | 403 => "Forbidden",
38 | 405 => "Method Not Allowed",
39 | 411 => "Length Required",
40 | 500 => "Internal Server Error"
41 | }
42 |
43 | class Request
44 | attr_reader :data, :header, :method, :path, :proto
45 |
46 | def initialize(data, method=nil, path=nil, proto=nil)
47 | @header, @data = Table.new, data
48 | @method, @path, @proto = method, path, proto
49 | end
50 |
51 | def content_length
52 | len = @header['Content-Length']
53 | return nil if len.nil?
54 | return len.to_i
55 | end
56 |
57 | end
58 |
59 | class Response
60 | attr_reader :header
61 | attr_accessor :body, :status, :status_message
62 |
63 | def initialize(status=200)
64 | @status = status
65 | @status_message = nil
66 | @header = Table.new
67 | end
68 | end
69 |
70 | # A case-insensitive Hash class for HTTP header
71 | class Table
72 | include Enumerable
73 |
74 | def initialize(hash={})
75 | @hash = hash
76 | update(hash)
77 | end
78 |
79 | def [](key)
80 | @hash[key.to_s.capitalize]
81 | end
82 |
83 | def []=(key, value)
84 | @hash[key.to_s.capitalize] = value
85 | end
86 |
87 | def update(hash)
88 | hash.each {|k,v| self[k] = v}
89 | self
90 | end
91 |
92 | def each
93 | @hash.each {|k,v| yield k.capitalize, v }
94 | end
95 |
96 | # Output the Hash table for the HTTP header
97 | def writeTo(port)
98 | each { |k,v| port << "#{k}: #{v}" << CRLF }
99 | end
100 | end # class Table
101 |
102 |
103 | # Generates a Hash with the HTTP headers
104 | def http_header(header=nil) # :doc:
105 | new_header = Table.new(DEFAULT_HEADER)
106 | new_header.update(header) unless header.nil?
107 |
108 | new_header["Connection"] = "close"
109 | new_header["Date"] = http_date(Time.now)
110 |
111 | new_header
112 | end
113 |
114 | # Returns a string which represents the time as rfc1123-date of HTTP-date
115 | def http_date( aTime ) # :doc:
116 | aTime.gmtime.strftime( "%a, %d %b %Y %H:%M:%S GMT" )
117 | end
118 |
119 | # Returns a string which includes the status code message as,
120 | # http headers, and body for the response.
121 | def http_resp(status_code, status_message=nil, header=nil, body=nil) # :doc:
122 | status_message ||= StatusCodeMapping[status_code]
123 |
124 | str = ""
125 | str << "#{HTTP_PROTO} #{status_code} #{status_message}" << CRLF
126 | http_header(header).writeTo(str)
127 | str << CRLF
128 | str << body unless body.nil?
129 | str
130 | end
131 |
132 | # Handles the HTTP request and writes the response back to the client, +io+.
133 | #
134 | # If an Exception is raised while handling the request, the client will receive
135 | # a 500 "Internal Server Error" message.
136 | def serve(io) # :doc:
137 | # perform IP authentication
138 | unless @handler.ip_auth_handler(io)
139 | io << http_resp(403, "Forbidden")
140 | return
141 | end
142 |
143 | # parse first line
144 | if io.gets =~ /^(\S+)\s+(\S+)\s+(\S+)/
145 | request = Request.new(io, $1, $2, $3)
146 | else
147 | io << http_resp(400, "Bad Request")
148 | return
149 | end
150 |
151 | # parse HTTP headers
152 | while (line=io.gets) !~ /^(\n|\r)/
153 | if line =~ /^([\w-]+):\s*(.*)$/
154 | request.header[$1] = $2.strip
155 | end
156 | end
157 |
158 | io.binmode
159 | response = Response.new
160 |
161 | # execute request handler
162 | @handler.request_handler(request, response)
163 |
164 | # write response back to the client
165 | io << http_resp(response.status, response.status_message,
166 | response.header, response.body)
167 |
168 | rescue Exception
169 | io << http_resp(500, "Internal Server Error")
170 | end
171 |
172 | end # class HttpServer
173 |
174 |
--------------------------------------------------------------------------------
/lib/gserver.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) 2001 John W. Small All Rights Reserved
3 | #
4 | # Author:: John W. Small
5 | # Documentation:: Gavin Sinclair
6 | # Licence:: Ruby License
7 |
8 | require "socket"
9 | require "thread"
10 |
11 | #
12 | # GServer implements a generic server, featuring thread pool management,
13 | # simple logging, and multi-server management. See HttpServer in
14 | # xmlrpc/httpserver.rb in the Ruby standard library for an example of
15 | # GServer in action.
16 | #
17 | # Any kind of application-level server can be implemented using this class.
18 | # It accepts multiple simultaneous connections from clients, up to an optional
19 | # maximum number. Several _services_ (i.e. one service per TCP port) can be
20 | # run simultaneously, and stopped at any time through the class method
21 | # GServer.stop(port). All the threading issues are handled, saving
22 | # you the effort. All events are optionally logged, but you can provide your
23 | # own event handlers if you wish.
24 | #
25 | # == Example
26 | #
27 | # Using GServer is simple. Below we implement a simple time server, run it,
28 | # query it, and shut it down. Try this code in +irb+:
29 | #
30 | # require 'gserver'
31 | #
32 | # #
33 | # # A server that returns the time in seconds since 1970.
34 | # #
35 | # class TimeServer < GServer
36 | # def initialize(port=10001, *args)
37 | # super(port, *args)
38 | # end
39 | # def serve(io)
40 | # io.puts(Time.now.to_i)
41 | # end
42 | # end
43 | #
44 | # # Run the server with logging enabled (it's a separate thread).
45 | # server = TimeServer.new
46 | # server.audit = true # Turn logging on.
47 | # server.start
48 | #
49 | # # *** Now point your browser to http://localhost:10001 to see it working ***
50 | #
51 | # # See if it's still running.
52 | # GServer.in_service?(10001) # -> true
53 | # server.stopped? # -> false
54 | #
55 | # # Shut the server down gracefully.
56 | # server.shutdown
57 | #
58 | # # Alternatively, stop it immediately.
59 | # GServer.stop(10001)
60 | # # or, of course, "server.stop".
61 | #
62 | # All the business of accepting connections and exception handling is taken
63 | # care of. All we have to do is implement the method that actually serves the
64 | # client.
65 | #
66 | # === Advanced
67 | #
68 | # As the example above shows, the way to use GServer is to subclass it to
69 | # create a specific server, overriding the +serve+ method. You can override
70 | # other methods as well if you wish, perhaps to collect statistics, or emit
71 | # more detailed logging.
72 | #
73 | # * #connecting
74 | # * #disconnecting
75 | # * #starting
76 | # * #stopping
77 | #
78 | # The above methods are only called if auditing is enabled, via #audit=.
79 | #
80 | # You can also override #log and #error if, for example, you wish to use a
81 | # more sophisticated logging system.
82 | #
83 | class GServer
84 |
85 | DEFAULT_HOST = "127.0.0.1"
86 |
87 | def serve(io)
88 | end
89 |
90 | @@services = {} # Hash of opened ports, i.e. services
91 | @@servicesMutex = Mutex.new
92 |
93 | # Stop the server running on the given port, bound to the given host
94 | #
95 | # +port+:: port, as a Fixnum, of the server to stop
96 | # +host+:: host on which to find the server to stop
97 | def GServer.stop(port, host = DEFAULT_HOST)
98 | @@servicesMutex.synchronize {
99 | @@services[host][port].stop
100 | }
101 | end
102 |
103 | # Check if a server is running on the given port and host
104 | #
105 | # +port+:: port, as a Fixnum, of the server to check
106 | # +host+:: host on which to find the server to check
107 | #
108 | # Returns true if a server is running on that port and host.
109 | def GServer.in_service?(port, host = DEFAULT_HOST)
110 | @@services.has_key?(host) and
111 | @@services[host].has_key?(port)
112 | end
113 |
114 | # Stop the server
115 | def stop
116 | @connectionsMutex.synchronize {
117 | if @tcpServerThread
118 | @tcpServerThread.raise "stop"
119 | end
120 | }
121 | end
122 |
123 | # Returns true if the server has stopped.
124 | def stopped?
125 | @tcpServerThread == nil
126 | end
127 |
128 | # Schedule a shutdown for the server
129 | def shutdown
130 | @shutdown = true
131 | end
132 |
133 | # Return the current number of connected clients
134 | def connections
135 | @connections.size
136 | end
137 |
138 | # Join with the server thread
139 | def join
140 | @tcpServerThread.join if @tcpServerThread
141 | end
142 |
143 | # Port on which to listen, as a Fixnum
144 | attr_reader :port
145 | # Host on which to bind, as a String
146 | attr_reader :host
147 | # Maximum number of connections to accept at a time, as a Fixnum
148 | attr_reader :maxConnections
149 | # IO Device on which log messages should be written
150 | attr_accessor :stdlog
151 | # Set to true to cause the callbacks #connecting, #disconnecting, #starting,
152 | # and #stopping to be called during the server's lifecycle
153 | attr_accessor :audit
154 | # Set to true to show more detailed logging
155 | attr_accessor :debug
156 |
157 | # Called when a client connects, if auditing is enabled.
158 | #
159 | # +client+:: a TCPSocket instance representing the client that connected
160 | #
161 | # Return true to allow this client to connect, false to prevent it.
162 | def connecting(client)
163 | addr = client.peeraddr
164 | log("#{self.class} #{@host}:#{@port} client:#{addr[1]} " +
165 | "#{addr[2]}<#{addr[3]}> connect")
166 | true
167 | end
168 |
169 |
170 | # Called when a client disconnects, if audition is enabled.
171 | #
172 | # +clientPort+:: the port of the client that is connecting
173 | def disconnecting(clientPort)
174 | log("#{self.class} #{@host}:#{@port} " +
175 | "client:#{clientPort} disconnect")
176 | end
177 |
178 | protected :connecting, :disconnecting
179 |
180 | # Called when the server is starting up, if auditing is enabled.
181 | def starting()
182 | log("#{self.class} #{@host}:#{@port} start")
183 | end
184 |
185 | # Called when the server is shutting down, if auditing is enabled.
186 | def stopping()
187 | log("#{self.class} #{@host}:#{@port} stop")
188 | end
189 |
190 | protected :starting, :stopping
191 |
192 | # Called if #debug is true whenever an unhandled exception is raised.
193 | # This implementation simply logs the backtrace.
194 | #
195 | # +detail+:: the Exception that was caught
196 | def error(detail)
197 | log(detail.backtrace.join("\n"))
198 | end
199 |
200 | # Log a message to #stdlog, if it's defined. This implementation
201 | # outputs the timestamp and message to the log.
202 | #
203 | # +msg+:: the message to log
204 | def log(msg)
205 | if @stdlog
206 | @stdlog.puts("[#{Time.new.ctime}] %s" % msg)
207 | @stdlog.flush
208 | end
209 | end
210 |
211 | protected :error, :log
212 |
213 | # Create a new server
214 | #
215 | # +port+:: the port, as a Fixnum, on which to listen
216 | # +host+:: the host to bind to
217 | # +maxConnections+:: the maximum number of simultaneous connections to
218 | # accept
219 | # +stdlog+:: IO device on which to log messages
220 | # +audit+:: if true, lifecycle callbacks will be called. See #audit
221 | # +debug+:: if true, error messages are logged. See #debug
222 | def initialize(port, host = DEFAULT_HOST, maxConnections = 4,
223 | stdlog = $stderr, audit = false, debug = false)
224 | @tcpServerThread = nil
225 | @port = port
226 | @host = host
227 | @maxConnections = maxConnections
228 | @connections = []
229 | @connectionsMutex = Mutex.new
230 | @connectionsCV = ConditionVariable.new
231 | @stdlog = stdlog
232 | @audit = audit
233 | @debug = debug
234 | end
235 |
236 | # Start the server if it isn't already running
237 | #
238 | # +maxConnections+::
239 | # override +maxConnections+ given to the constructor. A negative
240 | # value indicates that the value from the constructor should be used.
241 | def start(maxConnections = -1)
242 | raise "server is already running" if !stopped?
243 | @shutdown = false
244 | @maxConnections = maxConnections if maxConnections > 0
245 | @@servicesMutex.synchronize {
246 | if GServer.in_service?(@port,@host)
247 | raise "Port already in use: #{host}:#{@port}!"
248 | end
249 | @tcpServer = TCPServer.new(@host,@port)
250 | @port = @tcpServer.addr[1]
251 | @@services[@host] = {} unless @@services.has_key?(@host)
252 | @@services[@host][@port] = self;
253 | }
254 | @tcpServerThread = Thread.new {
255 | begin
256 | starting if @audit
257 | while !@shutdown
258 | @connectionsMutex.synchronize {
259 | while @connections.size >= @maxConnections
260 | @connectionsCV.wait(@connectionsMutex)
261 | end
262 | }
263 | client = @tcpServer.accept
264 | Thread.new(client) { |myClient|
265 | @connections << Thread.current
266 | begin
267 | myPort = myClient.peeraddr[1]
268 | serve(myClient) if !@audit or connecting(myClient)
269 | rescue => detail
270 | error(detail) if @debug
271 | ensure
272 | begin
273 | myClient.close
274 | rescue
275 | end
276 | @connectionsMutex.synchronize {
277 | @connections.delete(Thread.current)
278 | @connectionsCV.signal
279 | }
280 | disconnecting(myPort) if @audit
281 | end
282 | }
283 | end
284 | rescue => detail
285 | error(detail) if @debug
286 | ensure
287 | begin
288 | @tcpServer.close
289 | rescue
290 | end
291 | if @shutdown
292 | @connectionsMutex.synchronize {
293 | while @connections.size > 0
294 | @connectionsCV.wait(@connectionsMutex)
295 | end
296 | }
297 | else
298 | @connections.each { |c| c.raise "stop" }
299 | end
300 | @tcpServerThread = nil
301 | @@servicesMutex.synchronize {
302 | @@services[@host].delete(@port)
303 | }
304 | stopping if @audit
305 | end
306 | }
307 | self
308 | end
309 |
310 | end
311 |
--------------------------------------------------------------------------------