simplerpc 0.1.0c
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.
- checksums.yaml +7 -0
- data/LICENSE +4 -0
- data/lib/simplerpc/client.rb +124 -0
- data/lib/simplerpc/serialiser.rb +67 -0
- data/lib/simplerpc/server.rb +138 -0
- data/lib/simplerpc/socket_protocol.rb +42 -0
- metadata +48 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ff810496dda2e7d4adb2c9a423bc13784986e2c3
|
4
|
+
data.tar.gz: 84a9a286a976103d6912999ebbfdbcb1fcb11a8d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6cc60d3b1eb680a8c04aa94b21d7da6cabe3efcfdc367a145e66d60d7bb612cd2da29e9982cd29aa484c608e649f96adb0a66067d4ab65cbade0f57d0953ba2c
|
7
|
+
data.tar.gz: 88b99defcca647ee221abb9901fc37eb391b60455f17ed1629df9400398f934323291ff6a5911d9d7b7cd8ba74a9f9752008814e4ef01bb4563c2082e4008a84
|
data/LICENSE
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'socket' # Sockets are in standard library
|
2
|
+
require 'simplerpc/serialiser'
|
3
|
+
require 'simplerpc/socket_protocol'
|
4
|
+
|
5
|
+
module SimpleRPC
|
6
|
+
|
7
|
+
# The SimpleRPC client connects to a server, either persistently on on-demand, and makes
|
8
|
+
# calls to its proxy object.
|
9
|
+
#
|
10
|
+
# Once created, you should be able to interact with the client as if it were the remote
|
11
|
+
# object.
|
12
|
+
#
|
13
|
+
class Client
|
14
|
+
|
15
|
+
# Create a new client for the network
|
16
|
+
#
|
17
|
+
# hostname:: The hostname of the server
|
18
|
+
# port:: The port to connect to
|
19
|
+
# serialiser:: An object supporting load/dump for serialising objects. Defaults to
|
20
|
+
# SimpleRPC::Serialiser
|
21
|
+
# timeout:: The socket timeout. Throws Timeout::TimeoutErrors when exceeded. Set
|
22
|
+
# to nil to disable.
|
23
|
+
def initialize(hostname, port, serialiser=Serialiser.new, timeout=nil)
|
24
|
+
@hostname = hostname
|
25
|
+
@port = port
|
26
|
+
@serialiser = serialiser
|
27
|
+
@timeout = timeout
|
28
|
+
|
29
|
+
@m = Mutex.new
|
30
|
+
end
|
31
|
+
|
32
|
+
# Connect to the server,
|
33
|
+
# or do nothing if already connected
|
34
|
+
def connect
|
35
|
+
@m.synchronize{
|
36
|
+
_connect
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
# Disconnect from the server
|
41
|
+
# or do nothing if already disconnected
|
42
|
+
def close
|
43
|
+
@m.synchronize{
|
44
|
+
_disconnect
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
# Alias for close
|
49
|
+
alias :disconnect :close
|
50
|
+
|
51
|
+
# Is the client currently connected?
|
52
|
+
def connected?
|
53
|
+
@m.synchronize{
|
54
|
+
not @s == nil
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
# Call a method that is otherwise clobbered
|
59
|
+
# by the client object
|
60
|
+
def call(m, *args)
|
61
|
+
method_missing(m, *args)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Calls RPC on the remote object
|
65
|
+
def method_missing(m, *args, &block)
|
66
|
+
valid_call = false
|
67
|
+
result = nil
|
68
|
+
|
69
|
+
@m.synchronize{
|
70
|
+
already_connected = (not (@s == nil))
|
71
|
+
_connect if not already_connected
|
72
|
+
# send method name and arity
|
73
|
+
# #puts "c: METHOD: #{m}. ARITY: #{args.length}"
|
74
|
+
_send([m, args.length])
|
75
|
+
|
76
|
+
# receive yey/ney
|
77
|
+
valid_call = _recv
|
78
|
+
|
79
|
+
#puts "=--> #{valid_call}"
|
80
|
+
|
81
|
+
# call with args
|
82
|
+
if valid_call then
|
83
|
+
_send( args )
|
84
|
+
result = _recv
|
85
|
+
end
|
86
|
+
|
87
|
+
# Then d/c
|
88
|
+
_disconnect if not already_connected
|
89
|
+
}
|
90
|
+
|
91
|
+
# If the call wasn't valid, call super
|
92
|
+
if not valid_call then
|
93
|
+
result = super
|
94
|
+
end
|
95
|
+
|
96
|
+
return result
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
# Connect to the server
|
101
|
+
def _connect
|
102
|
+
@s = TCPSocket.open( @hostname, @port)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Receive data from the server
|
106
|
+
def _recv
|
107
|
+
@serialiser.load( SocketProtocol::recv(@s, @timeout) )
|
108
|
+
end
|
109
|
+
|
110
|
+
# Send data to the server
|
111
|
+
def _send(obj)
|
112
|
+
SocketProtocol::send(@s, @serialiser.dump(obj), @timeout)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Disconnect from the server
|
116
|
+
def _disconnect
|
117
|
+
return if not @s
|
118
|
+
@s.close
|
119
|
+
@s = nil
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
|
@@ -0,0 +1,67 @@
|
|
1
|
+
|
2
|
+
module SimpleRPC
|
3
|
+
|
4
|
+
# This class wraps three possible serialisation systems, providing a common interface to them all.
|
5
|
+
#
|
6
|
+
# It's not a necessary part of SimpleRPC---you may use any object that supports load/dump---but it is
|
7
|
+
# rather handy.
|
8
|
+
class Serialiser
|
9
|
+
|
10
|
+
SUPPORTED_METHODS = %w{marshal json msgpack yaml}.map{|s| s.to_sym}
|
11
|
+
|
12
|
+
# Create a new Serialiser with the given method. Optionally provide a binding to have
|
13
|
+
# the serialisation method execute within another context, i.e. for it to pick up
|
14
|
+
# on various libraries and classes.
|
15
|
+
#
|
16
|
+
# Supported methods are:
|
17
|
+
#
|
18
|
+
# :marshal:: Use ruby's Marshal system. A good mix of speed and generality.
|
19
|
+
# :yaml:: Use YAML. Very slow but very general
|
20
|
+
# :msgpack:: Use MessagePack gem. Very fast but not very general (limited data format support)
|
21
|
+
# :json:: Use JSON. Also slow, but better for interoperability than YAML.
|
22
|
+
#
|
23
|
+
def initialize(method = :marshal, binding=nil)
|
24
|
+
@method = method
|
25
|
+
@binding = nil
|
26
|
+
raise "Unrecognised serialisation method" if not SUPPORTED_METHODS.include?(method)
|
27
|
+
|
28
|
+
# Require prerequisites and handle msgpack not installed-iness.
|
29
|
+
case method
|
30
|
+
when :msgpack
|
31
|
+
begin
|
32
|
+
gem "msgpack", "~> 0.5"
|
33
|
+
rescue Gem::LoadError => e
|
34
|
+
$stderr.puts "The :msgpack serialisation method requires the MessagePack gem (msgpack)."
|
35
|
+
$stderr.puts "Please install it or use another serialisation method."
|
36
|
+
raise e
|
37
|
+
end
|
38
|
+
require 'msgpack'
|
39
|
+
@cls = MessagePack
|
40
|
+
when :yaml
|
41
|
+
require 'yaml'
|
42
|
+
@cls = YAML
|
43
|
+
when :json
|
44
|
+
require 'json'
|
45
|
+
@cls = JSON
|
46
|
+
else
|
47
|
+
# marshal is alaways available
|
48
|
+
@cls = Marshal
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Serialise to a string
|
53
|
+
def dump(obj)
|
54
|
+
return eval("#{@cls.to_s}.dump(obj)", @binding) if @binding
|
55
|
+
return @cls.send(:dump, obj)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Deserialise from a string
|
59
|
+
def load(bits)
|
60
|
+
return eval("#{@cls.to_s}.load(bits)", @binding) if @binding
|
61
|
+
return @cls.send(:load, bits)
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
@@ -0,0 +1,138 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
require 'socket' # Get sockets from stdlib
|
4
|
+
require 'simplerpc/serialiser'
|
5
|
+
require 'simplerpc/socket_protocol'
|
6
|
+
|
7
|
+
module SimpleRPC
|
8
|
+
|
9
|
+
# SimpleRPC's server. This wraps an object and exposes its methods to the network.
|
10
|
+
class Server
|
11
|
+
|
12
|
+
# Create a new server for a given proxy object
|
13
|
+
#
|
14
|
+
# proxy:: The object to proxy the API for---any ruby object
|
15
|
+
# port:: The port to listen on
|
16
|
+
# hostname:: The ip of the interface to listen on, or nil for all
|
17
|
+
# serialiser:: The serialiser to use
|
18
|
+
# threaded:: Should the server support multiple clients at once?
|
19
|
+
# timeout:: Socket timeout
|
20
|
+
def initialize(proxy, port, hostname=nil, serialiser=Serialiser.new, threaded=false, timeout = nil)
|
21
|
+
@proxy = proxy
|
22
|
+
@port = port
|
23
|
+
@hostname = hostname
|
24
|
+
|
25
|
+
# What format to use.
|
26
|
+
@serialiser = serialiser
|
27
|
+
|
28
|
+
# Silence errors?
|
29
|
+
@silence_errors = true
|
30
|
+
|
31
|
+
# Should we shut down?
|
32
|
+
@close = false
|
33
|
+
|
34
|
+
# Connect/receive timeouts
|
35
|
+
@timeout = timeout
|
36
|
+
|
37
|
+
# Threaded or not?
|
38
|
+
@threaded = (threaded == true)
|
39
|
+
@clients = {} if @threaded
|
40
|
+
@m = Mutex.new if @threaded
|
41
|
+
end
|
42
|
+
|
43
|
+
# Start listening forever
|
44
|
+
def listen
|
45
|
+
# Listen on one interface only if hostname given
|
46
|
+
if @hostname
|
47
|
+
@s = TCPServer.open( @hostname, @port )
|
48
|
+
else
|
49
|
+
@s = TCPServer.open(@port)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Handle clients
|
53
|
+
loop{
|
54
|
+
|
55
|
+
begin
|
56
|
+
if @threaded
|
57
|
+
|
58
|
+
# Create the thread
|
59
|
+
id = rand.hash
|
60
|
+
thread = Thread.new(id, @m, @s.accept){|id, m, c|
|
61
|
+
handle_client(c)
|
62
|
+
|
63
|
+
puts "#{id} closing 1"
|
64
|
+
m.synchronize{
|
65
|
+
@clients.delete(id)
|
66
|
+
}
|
67
|
+
puts "#{id} closing 2"
|
68
|
+
}
|
69
|
+
|
70
|
+
# Add to the client list
|
71
|
+
@m.synchronize{
|
72
|
+
@clients[id] = thread
|
73
|
+
}
|
74
|
+
else
|
75
|
+
handle_client(@s.accept)
|
76
|
+
end
|
77
|
+
rescue StandardError => e
|
78
|
+
raise e if not @silence_errors
|
79
|
+
end
|
80
|
+
|
81
|
+
break if @close
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
# Return the number of active clients.
|
86
|
+
def active_clients
|
87
|
+
@m.synchronize{
|
88
|
+
@clients.length
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
# Close the server object nicely,
|
93
|
+
# waiting on threads if necessary
|
94
|
+
def close
|
95
|
+
# Ask the loop to close
|
96
|
+
@close = true
|
97
|
+
|
98
|
+
# Wait on threads
|
99
|
+
if @threaded then
|
100
|
+
@clients.each{|id, thread|
|
101
|
+
thread.join
|
102
|
+
}
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
# Handle the protocol for client c
|
108
|
+
def handle_client(c)
|
109
|
+
m, arity = recv(c)
|
110
|
+
|
111
|
+
# Check the call is valid for the proxy object
|
112
|
+
valid_call = (@proxy.respond_to?(m) and @proxy.method(m).arity == arity)
|
113
|
+
|
114
|
+
send(c, valid_call)
|
115
|
+
|
116
|
+
# Make the call if valid and send the result back
|
117
|
+
if valid_call then
|
118
|
+
args = recv(c)
|
119
|
+
send(c, @proxy.send(m, *args) )
|
120
|
+
end
|
121
|
+
|
122
|
+
c.close
|
123
|
+
end
|
124
|
+
|
125
|
+
# Receive data from a client
|
126
|
+
def recv(c)
|
127
|
+
@serialiser.load( SocketProtocol::recv(c, @timeout) )
|
128
|
+
end
|
129
|
+
|
130
|
+
# Send data to a client
|
131
|
+
def send(c, obj)
|
132
|
+
SocketProtocol::send(c, @serialiser.dump(obj), @timeout)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
|
137
|
+
end
|
138
|
+
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# Low-level protocol specification for SimpleRPC.
|
2
|
+
#
|
3
|
+
# This defines how data is sent at the socket level, primarily controlling what happens with partial sends/timeouts.
|
4
|
+
module SimpleRPC::SocketProtocol
|
5
|
+
|
6
|
+
# Send already-serialised payload to socket s
|
7
|
+
def self.send(s, payload, timeout=nil)
|
8
|
+
# Send length
|
9
|
+
raise Timeout::TimeoutError if not IO.select(nil, [s], nil, timeout)
|
10
|
+
s.puts(payload.length.to_s)
|
11
|
+
|
12
|
+
# Send rest incrementally
|
13
|
+
#puts "[s] send(#{payload})"
|
14
|
+
len = payload.length
|
15
|
+
while( len > 0 and x = IO.select(nil, [s], nil, timeout) )
|
16
|
+
len -= s.write( payload )
|
17
|
+
end
|
18
|
+
raise Timeout::TimeoutError if not x
|
19
|
+
#puts "[s] sent(#{payload})"
|
20
|
+
end
|
21
|
+
|
22
|
+
# Receive raw data from socket s.
|
23
|
+
def self.recv(s, timeout=nil)
|
24
|
+
# Read the length of the data
|
25
|
+
raise Timeout::TimeoutError if not IO.select([s], nil, nil, timeout)
|
26
|
+
len = s.gets.chomp.to_i
|
27
|
+
|
28
|
+
# Then read the rest incrementally
|
29
|
+
buf = ""
|
30
|
+
while( len > 0 and x = IO.select([s], nil, nil, timeout) )
|
31
|
+
#puts "[s (#{len})]"
|
32
|
+
x = s.read(len)
|
33
|
+
len -= x.length
|
34
|
+
buf += x
|
35
|
+
end
|
36
|
+
raise Timeout::TimeoutError if not x
|
37
|
+
|
38
|
+
return buf
|
39
|
+
#puts "[s] recv(#{buf})"
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: simplerpc
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0c
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Stephen Wattam
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-04-23 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: A very simple and fast RPC library
|
14
|
+
email: stephenwattam@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- lib/simplerpc/client.rb
|
20
|
+
- lib/simplerpc/server.rb
|
21
|
+
- lib/simplerpc/socket_protocol.rb
|
22
|
+
- lib/simplerpc/serialiser.rb
|
23
|
+
- LICENSE
|
24
|
+
homepage: http://stephenwattam.com/projects/simplerpc
|
25
|
+
licenses:
|
26
|
+
- Beer
|
27
|
+
metadata: {}
|
28
|
+
post_install_message: Have fun Cing RPs :-)
|
29
|
+
rdoc_options: []
|
30
|
+
require_paths:
|
31
|
+
- lib
|
32
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
33
|
+
requirements:
|
34
|
+
- - '>='
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '1.9'
|
37
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - '>'
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: 1.3.1
|
42
|
+
requirements: []
|
43
|
+
rubyforge_project:
|
44
|
+
rubygems_version: 2.0.0
|
45
|
+
signing_key:
|
46
|
+
specification_version: 4
|
47
|
+
summary: Simple RPC library
|
48
|
+
test_files: []
|