Class: SimpleRPC::Server

Inherits:
Object
  • Object
show all
Defined in:
lib/simplerpc/server.rb

Overview

SimpleRPC’s server. This wraps an object and exposes its methods to the network.

i.e.:

require 'simplerpc/server'

# Expose the Array api on port 27045
s = SimpleRPC::Server.new( ["thing", "thing2"], :port => 27045 )

# Listen in a thread so we can shut down later
Thread.new(){ s.listen }
sleep(10)

# Tell the server to exit cleanly
s.close

Thread Safety

The server is thread-safe, and will not interrupt any clients when #close is called (instead it will wait for requests to finish, then shut down).

If :threaded is set to true, the server will be able to make many simultaneous calls to the object being proxied.

Controlling a Server

The server is thread-safe, and is designed to be run in a thread when blocking on #listen — calling #close on a listening server will cause the following chain of events:

  1. The current client requests will end

  2. The socket will close

  3. #listen and #close will return (almost) simultaneously

Serialisation Formats

By default both client and server use Marshal. This has proven fast and general, and is capable of sending data directly over sockets.

The serialiser also supports MessagePack (the msgpack gem), and this yields a small performance increase at the expense of generality (restrictions on data type).

Note that JSON and YAML, though they support reading and writing to sockets, do not properly terminate their reads and cause the system to hang. These methods are both slow and limited by comparison anyway, and algorithms needed to support their use require relatively large memory usage. They may be supported in later versions.

Authentication

Setting the :password and :secret options will require authentication to connect.

Clients and servers do not tell one another to use auth (such a system would impact speed) so the results of using mismatched configurations are undefined.

The auth process is simple and not particularly secure, but is designed to deter casual connections and attacks. It uses a password that is sent encrypted against a salt sent by the server to prevent replay attacks. If you want more reliable security, use an SSH tunnel.

The performance impact of auth is small, and takes about the same time as a simple request. This can be mitigated by using always-on mode.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(obj, opts = {}) ⇒ Server

Create a new server for a given proxy object.

The single required parameter, obj, is an object you wish to expose to the network. This is the API that will respond to RPCs.

Takes an option hash with options:

:port

The port on which to listen, or 0 for the OS to set it

:hostname

The hostname of the interface to listen on (omit for all interfaces)

:serialiser

A class supporting #load(IO) and #dump(obj, IO) for serialisation. Defaults to Marshal. I recommend using MessagePack if this is not fast enough. Note that JSON/YAML do not work as they don’t send terminating characters over the socket.

:verbose_errors

Report all socket errors from clients (by default these will be quashed).

:timeout

Socket timeout in seconds. Default is infinite (nil)

:threaded

Accept more than one client at once? Note that proxy object should be thread-safe for this Default is on.

:password

The password clients need to connect

:secret

The encryption key used during password authentication. Should be some long random string. This should be ASCII-8bit encoded (it will be converted if not)

:salt_size

The size of the string used as a nonce during password auth. Defaults to 10 chars

:fast_auth

Use a slightly faster auth system that is incapable of knowing if it has failed or not. By default this is off.



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/simplerpc/server.rb', line 108

def initialize(obj, opts = {})
  @obj                  = obj
  @port                 = opts[:port].to_i
  @hostname             = opts[:hostname]

  # What format to use.
  @serialiser           = opts[:serialiser] || Marshal

  # Silence errors coming from client connections?
  @verbose_errors       = (opts[:verbose_errors] == true)
  @fast_auth            = (opts[:fast_auth] == true)

  # Should we shut down?
  @close                = false
  @close_in, @close_out = UNIXSocket.pair

  # Connect/receive timeouts
  timeout               = opts[:timeout]

  # Auth
  if opts[:password] && opts[:secret]
    require 'securerandom'
    require 'simplerpc/encryption'
    @password   = opts[:password]
    @secret     = opts[:secret]
    @salt_size  = opts[:salt_size] || 10 # size of salt on key.
  end

  # Threaded or not?
  @threaded             = !(opts[:threaded] == false)
  if @threaded
    @clients            = {}
    @mc                 = Mutex.new # Client list mutex
  end

  # Listener mutex
  @ml                   = Mutex.new
end

Instance Attribute Details

#fast_authObject

Returns the value of attribute fast_auth.



81
82
83
# File 'lib/simplerpc/server.rb', line 81

def fast_auth
  @fast_auth
end

#hostnameObject (readonly)

Returns the value of attribute hostname.



80
81
82
# File 'lib/simplerpc/server.rb', line 80

def hostname
  @hostname
end

#objObject (readonly)

Returns the value of attribute obj.



80
81
82
# File 'lib/simplerpc/server.rb', line 80

def obj
  @obj
end

#password=(value) ⇒ Object (writeonly)

Sets the attribute password

Parameters:

  • value

    the value to set the attribute password to.



82
83
84
# File 'lib/simplerpc/server.rb', line 82

def password=(value)
  @password = value
end

#portObject (readonly)

Returns the value of attribute port.



80
81
82
# File 'lib/simplerpc/server.rb', line 80

def port
  @port
end

#secret=(value) ⇒ Object (writeonly)

Sets the attribute secret

Parameters:

  • value

    the value to set the attribute secret to.



82
83
84
# File 'lib/simplerpc/server.rb', line 82

def secret=(value)
  @secret = value
end

#serialiserObject

Returns the value of attribute serialiser.



81
82
83
# File 'lib/simplerpc/server.rb', line 81

def serialiser
  @serialiser
end

#threadedObject (readonly)

Returns the value of attribute threaded.



80
81
82
# File 'lib/simplerpc/server.rb', line 80

def threaded
  @threaded
end

#timeoutObject

Returns the value of attribute timeout.



80
81
82
# File 'lib/simplerpc/server.rb', line 80

def timeout
  @timeout
end

#verbose_errorsObject

Returns the value of attribute verbose_errors.



81
82
83
# File 'lib/simplerpc/server.rb', line 81

def verbose_errors
  @verbose_errors
end

Instance Method Details

#active_client_threadsObject

Return the number of active client threads.

Returns 0 if :threaded is set to false.



229
230
231
232
233
234
235
# File 'lib/simplerpc/server.rb', line 229

def active_client_threads
  # If threaded return a count from the clients list
  return @clients.length if @threaded

  # Else return 0 if not threaded
  return 0
end

#close(timeout = false) ⇒ Object

Close the server object nicely, waiting on threads if necessary



239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/simplerpc/server.rb', line 239

def close(timeout = false)
  # Return immediately if the server isn't listening
  return unless @ml.locked?
  
  # Ask the loop to close
  @close_in.putc 'x' # Tell select to close

  
  # Wait for loop to end
  elapsed_time = 0
  while @ml.locked? do
    sleep(0.05)
    elapsed_time += 0.05

    # If a timeout is given, try killing threads at this point
    if timeout && elapsed_time > timeout
      @clients.each {|id, thread| thread.kill() }
    end
  end
end

#listenObject

Start listening forever.

Use threads and .close to stop the server.

Throws AlreadyListeningError when the server is already busy listening for connections



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/simplerpc/server.rb', line 168

def listen
  raise 'Server is already listening' unless @ml.try_lock

  # Listen on one interface only if hostname given
  s = create_server_socket

  # Handle clients
  loop do

    # Accept in an interruptable manner
    if (c = interruptable_accept(s))

      # Set timeout directly on socket
      if @socket_timeout
        c.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, @socket_timeout)
        c.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, @socket_timeout)
      end

      # Threaded
      if @threaded

        # Create the id
        id = rand.hash

        # Add to the client list
        @mc.synchronize do
          @clients[id] = Thread.new() do
            # puts "[#{@clients.length+1}->#{id}"
            begin
              handle_client(c)
            ensure
              # Remove self from list
              @mc.synchronize { @clients.delete(id) }
              # puts "[#{@clients.length}<-#{id}"
            end
          end
          @clients[id].abort_on_exception = true
        end

      # Single-threaded
      else
        # Handle client
        handle_client(c)
      end
    end

    break if @close
  end

  # Wait for threads to end
  @clients.each {|id, thread| thread.join } if @threaded

  # Close socket
  @close = false if @close  # say we've closed
ensure
  @ml.unlock
end