├── 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 | --------------------------------------------------------------------------------