romp-rpc 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.
@@ -0,0 +1,548 @@
1
+ require 'socket'
2
+ require 'thread'
3
+ require 'fcntl'
4
+ require 'romp_helper'
5
+
6
+ ##
7
+ # ROMP - The Ruby Object Message Proxy
8
+ # @author Paul Brannan
9
+ # @version 0.1
10
+ # (C) Copyright 2001 Paul Brannan (cout at rm-f.net)
11
+ #
12
+ # <pre>
13
+ # ROMP is a set of classes for providing distributed object support to a
14
+ # Ruby program. You may distribute and/or modify it under the same terms as
15
+ # Ruby (see http://www.ruby-lang.org/en/LICENSE.txt). Example:
16
+ #
17
+ # Client
18
+ # ------
19
+ # client = ROMP::Client.new('localhost', 4242)
20
+ # obj = client.resolve("foo")
21
+ # puts obj.foo("foo")
22
+ # obj.oneway(:foo, "foo")
23
+ #
24
+ # Server
25
+ # ------
26
+ # class Foo
27
+ # def foo(x); return x; end
28
+ # end
29
+ # obj = Foo.new
30
+ # server = ROMP::Server.new('localhost', 4242)
31
+ # server.bind(obj, "foo")
32
+ # server.thread.join
33
+ #
34
+ # You can do all sorts of cool things with ROMP, including passing blocks to
35
+ # the functions, throwing exceptions and propogating them from server to
36
+ # client, and more. Unlike CORBA, where you must create an interface
37
+ # definition and strictly adhere to it, ROMP uses marshalling, so you can
38
+ # use almost any object with it. But, alas, it is not as powerful as CORBA.
39
+ #
40
+ # On a fast machine, you should expect around 7000 messages per second with
41
+ # normal method calls, up to 10000 messages per second with oneway calls with
42
+ # sync, and up to 40000 messages per second for oneway calls without sync.
43
+ # These numbers can vary depending on various factors, so YMMV.
44
+ #
45
+ # The ROMP message format is broken into 3 components:
46
+ # [ msg_type, obj_id, message ]
47
+ # For each msg_type, there is a specific format to the message. Additionally,
48
+ # certain msg_types are only valid when being sent to the server, and others
49
+ # are valid only when being sent back to the client. Here is a summary:
50
+ #
51
+ # msg_type send to meaning of obj_id msg format
52
+ # ----------------------------------------------------------------------------
53
+ # REQUEST server obj to talk to [:method, *args]
54
+ # REQUEST_BLOCK server obj to talk to [:method, *args]
55
+ # ONEWAY server obj to talk to [:method, *args]
56
+ # ONEWAY_SYNC server obj to talk to [:method, *args]
57
+ # RETVAL client always 0 retval
58
+ # EXCEPTION client always 0 $!
59
+ # YIELD client always 0 [value, value, ...]
60
+ # SYNC either 0=request, 1=response nil
61
+ # NULL_MSG either always 0 n/a
62
+ #
63
+ # BUGS:
64
+ # - On a 2.2 kernel, oneway calls without sync is very slow.
65
+ # - UDP support does not currently work.
66
+ # </pre>
67
+
68
+ module ROMP
69
+
70
+ public
71
+
72
+ ##
73
+ # The ROMP server class. Like its drb equivalent, this class spawns off
74
+ # a new thread which processes requests, allowing the server to do other
75
+ # things while it is doing processing for a distributed object. This
76
+ # means, though, that all objects used with ROMP must be thread-safe.
77
+ #
78
+ class Server
79
+
80
+ public
81
+ attr_reader :obj, :thread
82
+
83
+ ##
84
+ # Start a ROMP server.
85
+ #
86
+ # @param endpoint An endpoint for the server to listen on; should be specified in URI notation.
87
+ # @param acceptor A proc object that can accept or reject connections; it should take a Socket as an argument and returns true or false.
88
+ # @param debug Turns on debugging messages if enabled.
89
+ #
90
+ def initialize(endpoint, acceptor=nil, debug=false)
91
+ @mutex = Mutex.new
92
+ @debug = debug
93
+ @resolve_server = Resolve_Server.new
94
+ @resolve_obj = Resolve_Obj.new(@resolve_server)
95
+ @resolve_server.register(@resolve_obj)
96
+
97
+ @thread = Thread.new do
98
+ server = Generic_Server.new(endpoint)
99
+ while(socket = server.accept)
100
+ puts "Got a connection" if @debug
101
+ if acceptor then
102
+ if !acceptor.call(socket) then
103
+ socket.close
104
+ next
105
+ end
106
+ end
107
+ puts "Accepted the connection" if @debug
108
+ session = Session.new(socket)
109
+ session.set_nonblock(true)
110
+ Thread.new(socket) do |socket|
111
+ Thread.current.abort_on_exception = true
112
+ begin
113
+ # TODO: Send a sync message to the client so it
114
+ # knows we are ready to receive data.
115
+ server_loop(session)
116
+ rescue Exception
117
+ ROMP::print_exception($!) if @debug
118
+ end
119
+ puts "Connection closed" if @debug
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ ##
126
+ # Register an object with the server. The object will be given an
127
+ # id of @next_id, and @next_id will be incremented. We could use the
128
+ # object's real id, but this is insecure. The supplied object must
129
+ # be thread-safe.
130
+ #
131
+ # @param obj The object to register.
132
+ #
133
+ # @return A new Object_Reference that should be returned to the client.
134
+ #
135
+ def create_reference(obj)
136
+ @mutex.synchronize do
137
+ id = @resolve_server.register(obj)
138
+ Object_Reference.new(id) #return
139
+ end
140
+ end
141
+
142
+ ##
143
+ # Find an object in linear time and unregister it. Be careful with
144
+ # this function, because the client may not know the object has
145
+ # gone away.
146
+ #
147
+ # @param obj The object to unregister.
148
+ #
149
+ def delete_reference(obj)
150
+ @mutex.synchronize do
151
+ @resolve_server.unregister(obj)
152
+ end
153
+ nil #return
154
+ end
155
+
156
+ ##
157
+ # Register an object with the server and bind it to name.
158
+ #
159
+ # @param obj The object to bind.
160
+ # @param name The name of to bind the object to.
161
+ #
162
+ def bind(obj, name)
163
+ id = @resolve_server.register(obj)
164
+ @resolve_server.bind(name, id)
165
+ nil #return
166
+ end
167
+
168
+ ##
169
+ # This keeps the client from seeing our objects when they call inspect
170
+ #
171
+ alias_method :__inspect__, :inspect
172
+ def inspect()
173
+ return ""
174
+ end
175
+
176
+ private
177
+ if false then # the following functions are implemented in C:
178
+
179
+ ##
180
+ # The server_loop function is the guts of the server. It takes in
181
+ # requests from the client and forwards them to already-registered
182
+ # objects.
183
+ #
184
+ # @param session The session to run the loop with.
185
+ #
186
+ def server_loop(session)
187
+ end
188
+
189
+ end # if false
190
+ end
191
+
192
+ ##
193
+ # The ROMP client class. A ROMP server must be started on the given
194
+ # host and port before instantiating a ROMP client.
195
+ #
196
+ class Client
197
+
198
+ ##
199
+ # Connect to a ROMP server
200
+ #
201
+ # @param endpoint The endpoint the server is listening on.
202
+ # @param sync Specifies whether to synchronize between threads; turn this off to get a 20% performance boost.
203
+ #
204
+ def initialize(endpoint, sync=true)
205
+ @server = Generic_Client.new(endpoint)
206
+ @session = Session.new(@server)
207
+ @session.set_nonblock(true)
208
+ @mutex = sync ? Mutex.new : Null_Mutex.new
209
+ @resolve_obj = Proxy_Object.new(@session, @mutex, 0)
210
+ end
211
+
212
+ ##
213
+ # Given a string, return a proxy object that will forward requests
214
+ # for an object on the server with that name.
215
+ #
216
+ # @param object_name The name of the object to resolve.
217
+ #
218
+ # @return A Proxy_Object that can be used to make method calls on the object in the server.
219
+ #
220
+ def resolve(object_name)
221
+ @mutex.synchronize do
222
+ object_id = @resolve_obj.resolve(object_name)
223
+ return Proxy_Object.new(@session, @mutex, object_id)
224
+ end
225
+ end
226
+ end
227
+
228
+ private
229
+
230
+ ##
231
+ # In case the user does not want synchronization.
232
+ #
233
+ class Null_Mutex
234
+ def synchronize
235
+ yield
236
+ end
237
+
238
+ def lock
239
+ end
240
+
241
+ def unlock
242
+ end
243
+ end
244
+
245
+ ##
246
+ # All the special functions we have to keep track of
247
+ #
248
+ class Functions
249
+ GOOD = [
250
+ :inspect, :class_variables, :instance_eval, :instance_variables,
251
+ :to_a, :to_s
252
+ ]
253
+
254
+ BAD = [
255
+ :clone, :dup, :display
256
+ ]
257
+
258
+ METHOD = [
259
+ :methods, :private_methods, :protected_methods, :public_methods,
260
+ :singleton_methods
261
+ ]
262
+
263
+ RESPOND = [
264
+ [ :method, "raise NameError" ],
265
+ [ :respond_to?, "false" ]
266
+ ]
267
+ end
268
+
269
+ ##
270
+ # A ROMP::Object_Reference is created on the server side to represent an
271
+ # object in the system. It can be returned from a server object to a
272
+ # client object, at which point it is converted into a ROMP::Proxy_Object.
273
+ #
274
+ class Object_Reference
275
+ attr_reader :object_id
276
+
277
+ def initialize(object_id)
278
+ @object_id = object_id
279
+ end
280
+ end
281
+
282
+ ##
283
+ # A ROMP::Object acts as a proxy; it forwards most methods to the server
284
+ # for execution. When you make calls to a ROMP server, you will be
285
+ # making the calls through a Proxy_Object.
286
+ #
287
+ class Proxy_Object
288
+
289
+ if false then # the following functions are implemented in C:
290
+
291
+ ##
292
+ # The method_missing function is called for any method that is not
293
+ # defined on the client side. It will forward requests to the server
294
+ # for processing, and can iterate through a block, raise an exception,
295
+ # or return a value.
296
+ #
297
+ def method_missing(function, *args)
298
+ end
299
+
300
+ ##
301
+ # The oneway function is called to make a oneway call to the server
302
+ # without synchronization.
303
+ #
304
+ def onweway(function, *args)
305
+ end
306
+
307
+ ##
308
+ # The oneway_sync function is called to make a oneway call to the
309
+ # server with synchronization (the server will return a null message
310
+ # to the client before it begins processing). This is slightly safer
311
+ # than a normal oneway call, but it is slower (except on a linux 2.2
312
+ # kernel; see the bug list above).
313
+ #
314
+ def oneway_sync(function, *args)
315
+ end
316
+
317
+ ##
318
+ # The sync function will synchronize with the server. It sends a sync
319
+ # request and waits for a response.
320
+ #
321
+ def sync()
322
+ end
323
+
324
+ end # if false
325
+
326
+ # Make sure certain methods get passed down the wire.
327
+ Functions::GOOD.each do |method|
328
+ eval %{
329
+ def #{method}(*args)
330
+ method_missing(:#{method}, *args) #return
331
+ end
332
+ }
333
+ end
334
+
335
+ # And make sure others never get called.
336
+ Functions::BAD.each do |method|
337
+ eval %{
338
+ def #{method}(*args)
339
+ raise(NameError,
340
+ "undefined method `#{method}' for " +
341
+ "\#<#{self.class}:#{self.id}>")
342
+ end
343
+ }
344
+ end
345
+
346
+ # And remove these function names from any method lists that get
347
+ # returned; there's nothing we can do about people who decide to
348
+ # return them from other functions.
349
+ Functions::METHOD.each do |method|
350
+ eval %{
351
+ def #{method}(*args)
352
+ retval = method_missing(:#{method}, *args)
353
+ retval.each do |item|
354
+ Functions::BAD.each do |bad|
355
+ retval.delete(bad.to_s)
356
+ end
357
+ end
358
+ retval #return
359
+ end
360
+ }
361
+ end
362
+
363
+ # Same here, except don't let the call go through in the first place.
364
+ Functions::RESPOND.each do |method, action|
365
+ eval %{
366
+ def #{method}(arg, *args)
367
+ Functions::BAD.each do |bad|
368
+ if arg === bad.to_s then
369
+ return eval("#{action}")
370
+ end
371
+ end
372
+ method_missing(:#{method}, arg, *args) #return
373
+ end
374
+ }
375
+ end
376
+
377
+ end
378
+
379
+ ##
380
+ # The Resolve_Server class registers objects for the server. You will
381
+ # never have to use this class directly.
382
+ #
383
+ class Resolve_Server
384
+ def initialize
385
+ @next_id = 0
386
+ @unused_ids = Array.new
387
+ @id_to_object = Hash.new
388
+ @name_to_id = Hash.new
389
+ end
390
+
391
+ def register(obj)
392
+ if @next_id >= Session::MAX_ID then
393
+ if @unused_ids.size == 0 then
394
+ raise "Object limit exceeded"
395
+ else
396
+ id = @unused_ids.pop
397
+ end
398
+ end
399
+ @id_to_object[@next_id] = obj
400
+ old_id = @next_id
401
+ @next_id = @next_id.succ()
402
+ old_id #return
403
+ end
404
+
405
+ def get_object(object_id)
406
+ @id_to_object[object_id] #return
407
+ end
408
+
409
+ def unregister(obj)
410
+ delete_obj_from_array_private(@id_to_object, obj)
411
+ end
412
+
413
+ def bind(name, id)
414
+ @name_to_id[name] = id
415
+ end
416
+
417
+ def resolve(name)
418
+ @name_to_id[name] #return
419
+ end
420
+
421
+ def delete_obj_from_array_private(array, obj)
422
+ index = array.index(obj)
423
+ array[index] = nil unless index == nil
424
+ end
425
+ end
426
+
427
+ ##
428
+ # The Resolve_Obj class handles resolve requests for the client. It is
429
+ # a special ROMP object with an object id of 0. You will never have to
430
+ # make calls on it directly, but will instead make calls on it through
431
+ # the Client object.
432
+ #
433
+ class Resolve_Obj
434
+ def initialize(resolve_server)
435
+ @resolve_server = resolve_server
436
+ end
437
+
438
+ def resolve(name)
439
+ @resolve_server.resolve(name) #return
440
+ end
441
+ end
442
+
443
+ ##
444
+ # A Generic_Server creates an endpoint to listen on, waits for connections,
445
+ # and accepts them when requested. It can operate on different kinds of
446
+ # connections. You will never have to use this object directly.
447
+ #
448
+ class Generic_Server
449
+ def initialize(endpoint)
450
+ case endpoint
451
+ when %r{^(tcp)?romp://(.*?):(.*)}
452
+ @type = "tcp"
453
+ @host = $2 == "" ? nil : $2
454
+ @port = $3
455
+ @server = TCPServer.new(@host, @port)
456
+ when %r{^(udp)romp://(.*?):(.*)}
457
+ @type = "udp"
458
+ @host = $2 == "" ? nil : $2
459
+ @port = $3
460
+ @server = UDPSocket.open()
461
+ @server.bind(@host, @port)
462
+ @mutex = Mutex.new
463
+ when %r{^(unix)romp://(.*)}
464
+ @type = "unix"
465
+ @path = $2
466
+ @server = UNIXServer.open(@path)
467
+ else
468
+ raise ArgumentError, "Invalid endpoint"
469
+ end
470
+ end
471
+ def accept
472
+ case @type
473
+ when "tcp"
474
+ socket = @server.accept
475
+ socket.setsockopt(Socket::SOL_TCP, Socket::TCP_NODELAY, 1)
476
+ socket.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK)
477
+ socket.sync = true
478
+ socket #return
479
+ when "udp"
480
+ @mutex.lock
481
+ socket = @server
482
+ socket.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK)
483
+ socket #return
484
+ when "unix"
485
+ socket = @server.accept
486
+ socket.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK)
487
+ socket.sync = true
488
+ socket #return
489
+ end
490
+ end
491
+ end
492
+
493
+ ##
494
+ # A Generic_Client connects to a Generic_Server on a given endpoint.
495
+ # You will never have to use this object directly.
496
+ #
497
+ class Generic_Client
498
+ def self.new(endpoint)
499
+ case endpoint
500
+ when %r{^(tcp)?romp://(.*?):(.*)}
501
+ socket = TCPSocket.open($2, $3)
502
+ socket.sync = true
503
+ socket.setsockopt(Socket::SOL_TCP, Socket::TCP_NODELAY, 1)
504
+ socket.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK)
505
+ socket #return
506
+ when %r{^(udp)romp://(.*?):(.*)}
507
+ socket = UDPSocket.open
508
+ socket.connect($2, $3)
509
+ socket #return
510
+ when %r{^(unix)romp://(.*)}
511
+ socket = UNIXSocket.open($2)
512
+ else
513
+ raise ArgumentError, "Invalid endpoint"
514
+ end
515
+ end
516
+ end
517
+
518
+
519
+ ##
520
+ # Print an exception to the screen. This is necessary, because Ruby does
521
+ # not give us access to its error_print function from within Ruby.
522
+ #
523
+ # @param exc The exception object to print.
524
+ #
525
+ def self.print_exception(exc)
526
+ first = true
527
+ $!.backtrace.each do |bt|
528
+ if first then
529
+ puts "#{bt}: #{$!} (#{$!.message})"
530
+ else
531
+ puts "\tfrom #{bt}"
532
+ end
533
+ first = false
534
+ end
535
+ end
536
+
537
+ if false then # the following classes are implemented in C:
538
+
539
+ ##
540
+ # The Sesssion class is defined in romp_helper.so. You should never have
541
+ # to use it directly.
542
+ #
543
+ class Session
544
+ end
545
+
546
+ end # if false
547
+
548
+ end