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.
- data/README.md +24 -0
- data/ext/extconf.rb +3 -0
- data/ext/romp_helper.c +822 -0
- data/lib/romp-rpc.rb +548 -0
- data/sample/client.rb +115 -0
- data/sample/server.rb +67 -0
- metadata +74 -0
data/lib/romp-rpc.rb
ADDED
@@ -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
|