arpie 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README +31 -9
- data/Rakefile +3 -3
- data/lib/arpie.rb +2 -2
- data/lib/arpie/client.rb +193 -0
- data/lib/arpie/protocol.rb +243 -14
- data/lib/arpie/proxy.rb +29 -19
- data/lib/arpie/server.rb +156 -0
- data/tools/benchmark.rb +12 -4
- data/tools/protocol_benchmark.rb +45 -0
- metadata +8 -7
- data/lib/arpie/endpoint.rb +0 -94
- data/lib/arpie/transport.rb +0 -37
data/README
CHANGED
@@ -13,6 +13,9 @@ Source code is in git[http://git.swordcoast.net/?p=lib/ruby/arpie.git;a=summary]
|
|
13
13
|
|
14
14
|
You can contact me via email at elven@swordcoast.net.
|
15
15
|
|
16
|
+
arpie is available on the rubygems gem server - just do <tt>gem1.8 install arpie</tt>
|
17
|
+
to get the newest version.
|
18
|
+
|
16
19
|
|
17
20
|
== Simple, contrived example: A string reverse server
|
18
21
|
|
@@ -22,22 +25,23 @@ You can contact me via email at elven@swordcoast.net.
|
|
22
25
|
|
23
26
|
server = TCPServer.new(51210)
|
24
27
|
|
25
|
-
e = Arpie::
|
28
|
+
e = Arpie::Server.new(Arpie::MarshalProtocol.new)
|
26
29
|
|
27
|
-
e.handle do |ep, msg|
|
28
|
-
msg.reverse
|
30
|
+
e.handle do |server, ep, msg|
|
31
|
+
ep.write_message msg.reverse
|
29
32
|
end
|
30
33
|
|
31
34
|
e.accept do
|
32
35
|
server.accept
|
33
36
|
end
|
34
37
|
|
35
|
-
c = Arpie::
|
38
|
+
c = Arpie::Client.new(Arpie::MarshalProtocol.new)
|
36
39
|
c.connect do |transport|
|
37
40
|
TCPSocket.new("127.0.0.1", 51210)
|
38
41
|
end
|
39
42
|
|
40
|
-
|
43
|
+
c.write_message "hi"
|
44
|
+
puts c.read_message
|
41
45
|
# => "ih"
|
42
46
|
|
43
47
|
== Advanced, but still simple example: Using Proxy to access remote objects
|
@@ -54,7 +58,7 @@ You can contact me via email at elven@swordcoast.net.
|
|
54
58
|
|
55
59
|
server = TCPServer.new(51210)
|
56
60
|
|
57
|
-
e = Arpie::
|
61
|
+
e = Arpie::ProxyServer.new(Arpie::MarshalProtocol.new)
|
58
62
|
|
59
63
|
e.handle MyHandler.new
|
60
64
|
|
@@ -62,15 +66,33 @@ You can contact me via email at elven@swordcoast.net.
|
|
62
66
|
server.accept
|
63
67
|
end
|
64
68
|
|
65
|
-
|
66
|
-
|
69
|
+
p = Arpie::ProxyClient.new(Arpie::MarshalProtocol.new)
|
70
|
+
p.connect do |transport|
|
67
71
|
TCPSocket.new("127.0.0.1", 51210)
|
68
72
|
end
|
69
|
-
p = Arpie::Proxy.new(c)
|
70
73
|
|
71
74
|
puts p.reverse "hi"
|
72
75
|
# => "ih"
|
73
76
|
|
77
|
+
|
78
|
+
== Replay protection
|
79
|
+
|
80
|
+
It can happen that a Client loses connection to a Server.
|
81
|
+
In that case, the Transport tries transparently reconnecting by simply
|
82
|
+
invoking the block again that was given to Client#connect.
|
83
|
+
See the Client accessors for modifying this behaviour.
|
84
|
+
|
85
|
+
It is assumed that each call, that is being placed, is atomic - eg, no
|
86
|
+
connection losses in between message send and receive; lost messages
|
87
|
+
will be retransmitted. Some Protocol classes provide support for replay
|
88
|
+
protection through in-band serials; though it is not a requirement to implement it.
|
89
|
+
If a serial is provided in the data stream, the Protocol will not call
|
90
|
+
the handler again for retransmissions, but instead reply with the old,
|
91
|
+
already evaluated value.
|
92
|
+
|
93
|
+
Not all protocols support serials; those who do not offer no replay protection,
|
94
|
+
and special care has to be taken elsewhere.
|
95
|
+
|
74
96
|
== Benchmarks
|
75
97
|
|
76
98
|
There is a benchmark script included in the git repository (and in the gem
|
data/Rakefile
CHANGED
@@ -9,10 +9,10 @@ include FileUtils
|
|
9
9
|
# Configuration
|
10
10
|
##############################################################################
|
11
11
|
NAME = "arpie"
|
12
|
-
VERS = "0.0.
|
12
|
+
VERS = "0.0.2"
|
13
13
|
CLEAN.include ["**/.*.sw?", "pkg", ".config", "rdoc", "coverage"]
|
14
14
|
RDOC_OPTS = ["--quiet", "--line-numbers", "--inline-source", '--title', \
|
15
|
-
"#{NAME}: A high-performing layered
|
15
|
+
"#{NAME}: A high-performing layered networking protocol framework. Simple to use, simple to extend.", \
|
16
16
|
'--main', 'README']
|
17
17
|
|
18
18
|
DOCS = ["README", "COPYING"]
|
@@ -34,7 +34,7 @@ spec = Gem::Specification.new do |s|
|
|
34
34
|
s.has_rdoc = true
|
35
35
|
s.extra_rdoc_files = DOCS + Dir["doc/*.rdoc"]
|
36
36
|
s.rdoc_options += RDOC_OPTS + ["--exclude", "^(examples|extras)\/"]
|
37
|
-
s.summary = "
|
37
|
+
s.summary = "A high-performing layered networking protocol framework. Simple to use, simple to extend."
|
38
38
|
s.description = s.summary
|
39
39
|
s.author = "Bernhard Stoeckner"
|
40
40
|
s.email = "elven@swordcoast.net"
|
data/lib/arpie.rb
CHANGED
data/lib/arpie/client.rb
ADDED
@@ -0,0 +1,193 @@
|
|
1
|
+
module Arpie
|
2
|
+
|
3
|
+
# A Client is a connection manager, and acts as the
|
4
|
+
# glue between a user-defined medium (for example, a TCP
|
5
|
+
# socket), and a protocol, with automatic reconnecting
|
6
|
+
# and fault handling.
|
7
|
+
#
|
8
|
+
# See README for examples.
|
9
|
+
class Client
|
10
|
+
attr_reader :protocol
|
11
|
+
|
12
|
+
# How often should this Client retry a connection.
|
13
|
+
# 0 for never, greater than 0 for that many attempts,
|
14
|
+
# nil for infinite (default).
|
15
|
+
# Values other than nil will raise network exceptions
|
16
|
+
# to the caller.
|
17
|
+
attr_accessor :connect_retry
|
18
|
+
|
19
|
+
# How long should the caller sleep after each reconnect
|
20
|
+
# attempt. (default: 1.0). The default value is probably
|
21
|
+
# okay. Do not set this to 0; that will produce
|
22
|
+
# unnecessary load in case of network failure.
|
23
|
+
attr_accessor :connect_sleep
|
24
|
+
|
25
|
+
def initialize protocol
|
26
|
+
@protocol = protocol
|
27
|
+
@read_io = nil
|
28
|
+
@write_io = nil
|
29
|
+
@connector = lambda { raise ArgumentError, "No connector specified, cannot connect to Endpoint." }
|
30
|
+
@connect_retry = nil
|
31
|
+
@connect_sleep = 1.0
|
32
|
+
@on_error = lambda {|client, exception|
|
33
|
+
$stderr.puts "Error in Transport IO: #{exception.message.to_s}"
|
34
|
+
$stderr.puts exception.backtrace.join("\n")
|
35
|
+
$stderr.puts "Set Transport#on_error &block to override this."
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
# Provide a connector block, which will be called
|
40
|
+
# each time a connection is needed.
|
41
|
+
# Expectes an IO object.
|
42
|
+
# Alternatively, you can return a two-item array.
|
43
|
+
# To test something without involving any networking,
|
44
|
+
# simply run IO.pipe in this block.
|
45
|
+
# Set +connect_immediately+ to true to connect
|
46
|
+
# immediately, instead on the first message.
|
47
|
+
def connect connect_immediately = false, &connector
|
48
|
+
@connector = connector
|
49
|
+
_connect if connect_immediately
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
# Set an error handler. It will be called with two
|
54
|
+
# parameters, the client, and the exception that occured.
|
55
|
+
# Optional, and just for notification.
|
56
|
+
def on_error &handler #:yields: client, exception
|
57
|
+
@on_error = handler
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
# Send a message. Returns immediately.
|
62
|
+
def write_message message
|
63
|
+
io_retry do
|
64
|
+
@protocol.write_message(@write_io, message)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
alias_method :<<, :write_message
|
68
|
+
|
69
|
+
# Receive a message. Blocks until received.
|
70
|
+
def read_message
|
71
|
+
io_retry do
|
72
|
+
message = @protocol.read_message(@read_io)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Execute the given block until all connection attempts
|
77
|
+
# have been exceeded.
|
78
|
+
# Yields self.
|
79
|
+
# You do not usually want to use this.
|
80
|
+
def io_retry &block
|
81
|
+
try = 0
|
82
|
+
|
83
|
+
begin
|
84
|
+
_connect
|
85
|
+
yield self
|
86
|
+
rescue IOError => e
|
87
|
+
try += 1
|
88
|
+
@on_error.call(self, e) if @on_error
|
89
|
+
p e
|
90
|
+
|
91
|
+
if @connect_retry == 0 || (@connect_retry && try > @connect_retry)
|
92
|
+
raise EOFError, "Cannot read from io: lost connection after #{try} attempts (#{e.message.to_s})"
|
93
|
+
end
|
94
|
+
|
95
|
+
sleep @connect_sleep
|
96
|
+
begin; @read_io.close if @read_io; rescue; end
|
97
|
+
@read_io = nil
|
98
|
+
begin; @write_io.close if @write_io; rescue; end
|
99
|
+
@write_io = nil
|
100
|
+
retry
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def _connect
|
107
|
+
@read_io and return
|
108
|
+
@read_io, @write_io = @connector.call(self)
|
109
|
+
@write_io ||= @read_io
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# A simple pseudo event-based client, using a thread
|
114
|
+
# with a callback.
|
115
|
+
class EventedClient < Client
|
116
|
+
private :read_message
|
117
|
+
|
118
|
+
# Set a callback for incoming messages.
|
119
|
+
def handle &handler #:yields: client, message
|
120
|
+
@handler = handler
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def _read_thread
|
126
|
+
loop do
|
127
|
+
io_retry do
|
128
|
+
message = read_message
|
129
|
+
@handler and @handler.call(self, message)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def _connect
|
135
|
+
super
|
136
|
+
@read_thread ||= Thread.new { _read_thread }
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
|
141
|
+
# A Client extension which provides a RPC-like
|
142
|
+
# interface. Used by ProxyClient.
|
143
|
+
class RPCClient < Client
|
144
|
+
private :read_message, :write_message
|
145
|
+
|
146
|
+
def initialize protocol
|
147
|
+
super(protocol)
|
148
|
+
|
149
|
+
@on_pre_call = lambda {|client, message| }
|
150
|
+
@on_post_call = lambda {|client, message, reply| }
|
151
|
+
end
|
152
|
+
|
153
|
+
# Callback that gets invoked before placing a call to the
|
154
|
+
# Server. You can stop the call from happening by raising
|
155
|
+
# an exception (which will be passed on to the caller).
|
156
|
+
def pre_call &handler #:yields: client, message
|
157
|
+
@on_pre_call = handler
|
158
|
+
self
|
159
|
+
end
|
160
|
+
|
161
|
+
# Callback that gets invoked after receiving an answer.
|
162
|
+
# You can raise an exception here; and it will be passed
|
163
|
+
# to the caller, instead of returning the value.
|
164
|
+
def post_call &handler #:yields: client, message, reply
|
165
|
+
@on_post_call = handler
|
166
|
+
self
|
167
|
+
end
|
168
|
+
|
169
|
+
|
170
|
+
# Send a message and receive a reply in a synchronous
|
171
|
+
# fashion. Will block until transmitted, or until
|
172
|
+
# all reconnect attempts failed.
|
173
|
+
def request message
|
174
|
+
reply = nil
|
175
|
+
|
176
|
+
@on_pre_call.call(self, message) if @on_pre_call
|
177
|
+
|
178
|
+
io_retry do
|
179
|
+
write_message(message)
|
180
|
+
reply = read_message
|
181
|
+
end
|
182
|
+
|
183
|
+
@on_post_call.call(self, message, reply) if @on_post_call
|
184
|
+
|
185
|
+
case reply
|
186
|
+
when Exception
|
187
|
+
raise reply
|
188
|
+
else
|
189
|
+
reply
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
data/lib/arpie/protocol.rb
CHANGED
@@ -1,51 +1,280 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
require 'yaml'
|
3
|
+
|
1
4
|
module Arpie
|
2
5
|
|
3
6
|
# A Protocol converts messages (which are arbitary objects)
|
4
7
|
# to a suitable on-the-wire format, and back.
|
5
8
|
class Protocol
|
9
|
+
MTU = 1024
|
10
|
+
|
6
11
|
private_class_method :new
|
7
12
|
|
13
|
+
attr_reader :message
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@message = nil
|
17
|
+
@buffer = ""
|
18
|
+
reset
|
19
|
+
end
|
20
|
+
|
21
|
+
# Reads data from +io+. Returns true, if a whole
|
22
|
+
# message has been read, or false if more data is needed.
|
23
|
+
# The read message can be retrieved via Protocol#message.
|
24
|
+
def read_partial io
|
25
|
+
@buffer << io.readpartial(MTU)
|
26
|
+
|
27
|
+
if idx = complete?(@buffer)
|
28
|
+
@message = from @buffer[0, idx]
|
29
|
+
@buffer = @buffer[idx, -1] || ""
|
30
|
+
return true
|
31
|
+
end
|
32
|
+
|
33
|
+
return false
|
34
|
+
end
|
35
|
+
|
8
36
|
# Read a message from +io+. Block until a message
|
9
37
|
# has been received.
|
38
|
+
# Returns the message.
|
10
39
|
def read_message io
|
40
|
+
select([io]) until read_partial(io)
|
41
|
+
@message
|
11
42
|
end
|
12
43
|
|
13
|
-
|
44
|
+
def write_raw_partial io, message
|
45
|
+
io.write(message)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Write +message+ to +io+.
|
14
49
|
def write_message io, message
|
50
|
+
io.write(to message)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Convert obj to on-the-wire format.
|
54
|
+
def to obj
|
55
|
+
obj
|
56
|
+
end
|
57
|
+
|
58
|
+
# Convert obj from on-the-wire-format.
|
59
|
+
def from obj
|
60
|
+
obj
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns a Fixnum if the given obj contains a complete message.
|
64
|
+
# The Fixnum is the index up to where the message runs; the rest
|
65
|
+
# is assumed to be (part of) the next message.
|
66
|
+
# Returns nil if obj does not describe a complete message (eg,
|
67
|
+
# more data needs to be read).
|
68
|
+
def complete? obj
|
69
|
+
nil
|
70
|
+
end
|
71
|
+
|
72
|
+
# Reset all state buffers. This is usually called
|
73
|
+
# when the underlying connection drops, and any half-read
|
74
|
+
# messages need to be discarded.
|
75
|
+
def reset
|
76
|
+
@message = nil
|
77
|
+
@buffer = ""
|
78
|
+
end
|
79
|
+
|
80
|
+
def endpoint_klass
|
81
|
+
Arpie::Endpoint
|
15
82
|
end
|
16
83
|
end
|
17
84
|
|
18
|
-
# A
|
19
|
-
#
|
20
|
-
|
85
|
+
# A simple separator-based protocol. This can be used to implement
|
86
|
+
# newline-delimited communication.
|
87
|
+
class SeparatorProtocol < Protocol
|
88
|
+
public_class_method :new
|
89
|
+
|
90
|
+
attr_accessor :separator
|
91
|
+
|
92
|
+
def initialize separator = "\n"
|
93
|
+
super()
|
94
|
+
@separator = separator
|
95
|
+
end
|
96
|
+
|
97
|
+
def complete? obj
|
98
|
+
obj.index(@separator)
|
99
|
+
end
|
100
|
+
|
101
|
+
def from obj
|
102
|
+
obj.gsub(/#{Regexp.escape(@separator)}$/, "")
|
103
|
+
end
|
104
|
+
|
105
|
+
def to obj
|
106
|
+
obj + @separator
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# A linebased-protocol, which does shellwords-escaping/joining
|
111
|
+
# on the lines; messages sent are arrays of parameters.
|
112
|
+
# Note that all parameters are expected to be strings.
|
113
|
+
class ShellwordsProtocol < SeparatorProtocol
|
114
|
+
def to obj
|
115
|
+
super Shellwords.join(obj)
|
116
|
+
end
|
117
|
+
|
118
|
+
def from obj
|
119
|
+
Shellwords.shellwords(super obj)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# A sample binary protocol, which simply prefixes each message with the
|
124
|
+
# size of the data to be expected.
|
21
125
|
class SizedProtocol < Protocol
|
126
|
+
public_class_method :new
|
127
|
+
|
22
128
|
def initialize
|
129
|
+
super
|
23
130
|
@max_message_size = 1024 * 1024
|
24
131
|
end
|
25
132
|
|
26
|
-
def
|
27
|
-
sz =
|
28
|
-
|
29
|
-
data = io.read(expect)
|
133
|
+
def complete? obj
|
134
|
+
sz = obj.unpack("Q")[0]
|
135
|
+
obj.size == sz + 8 ? sz + 8 : nil
|
30
136
|
end
|
31
137
|
|
32
|
-
def
|
33
|
-
|
138
|
+
def from obj
|
139
|
+
sz, data = obj.unpack("Qa*")
|
140
|
+
data
|
141
|
+
end
|
142
|
+
|
143
|
+
def to obj
|
144
|
+
[obj.size, obj].pack("Qa*")
|
34
145
|
end
|
35
146
|
end
|
36
147
|
|
37
148
|
# A procotol that simply Marshals all data sent over
|
38
149
|
# this protocol. Served as an example, but a viable
|
39
150
|
# choice for ruby-only production code.
|
151
|
+
# Messages are arbitary objects.
|
40
152
|
class MarshalProtocol < SizedProtocol
|
153
|
+
def to obj
|
154
|
+
super Marshal.dump(obj)
|
155
|
+
end
|
156
|
+
|
157
|
+
def from obj
|
158
|
+
Marshal.load(super obj)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# A protocol which encodes objects into YAML representation.
|
163
|
+
# Messages are arbitary yaml-encodable objects.
|
164
|
+
class YAMLProtocol < Arpie::Protocol
|
41
165
|
public_class_method :new
|
42
166
|
|
43
|
-
def
|
44
|
-
|
167
|
+
def complete? obj
|
168
|
+
obj =~ /\.\.\.$/
|
45
169
|
end
|
46
170
|
|
47
|
-
def
|
48
|
-
|
171
|
+
def to obj
|
172
|
+
YAML.dump(obj) + "...\n"
|
173
|
+
end
|
174
|
+
|
175
|
+
def from obj
|
176
|
+
YAML.load(obj)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# A RPC Protocol encapsulates RPCProtocol::Call
|
181
|
+
# messages.
|
182
|
+
class RPCProtocol < Protocol
|
183
|
+
|
184
|
+
# A RPC call.
|
185
|
+
class Call < Struct.new(:ns, :meth, :argv); end
|
186
|
+
end
|
187
|
+
|
188
|
+
# A XMLRPC Protocol based on rubys xmlrpc stdlib.
|
189
|
+
# This does not encode HTTP headers; usage together with
|
190
|
+
# a real webserver is advised.
|
191
|
+
class XMLRPCProtocol < RPCProtocol
|
192
|
+
public_class_method :new
|
193
|
+
|
194
|
+
require 'xmlrpc/create'
|
195
|
+
require 'xmlrpc/parser'
|
196
|
+
require 'xmlrpc/config'
|
197
|
+
|
198
|
+
VALID_MODES = [:client, :server].freeze
|
199
|
+
|
200
|
+
attr_reader :mode
|
201
|
+
attr_accessor :writer
|
202
|
+
attr_accessor :parser
|
203
|
+
|
204
|
+
def initialize mode, writer = XMLRPC::Create, parser = XMLRPC::XMLParser::REXMLStreamParser
|
205
|
+
super()
|
206
|
+
raise ArgumentError, "Not a valid mode, expecting one of #{VALID_MODES.inspect}" unless
|
207
|
+
VALID_MODES.index(mode)
|
208
|
+
|
209
|
+
@mode = mode
|
210
|
+
@writer = writer.new
|
211
|
+
@parser = parser.new
|
212
|
+
end
|
213
|
+
|
214
|
+
def to obj
|
215
|
+
case @mode
|
216
|
+
when :client
|
217
|
+
@writer.methodCall(obj.ns + obj.meth, *obj.argv)
|
218
|
+
|
219
|
+
when :server
|
220
|
+
case obj
|
221
|
+
when Exception
|
222
|
+
# TODO: wrap XMLFault
|
223
|
+
else
|
224
|
+
@writer.methodResponse(true, obj)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def from obj
|
230
|
+
case @mode
|
231
|
+
when :client
|
232
|
+
@parser.parseMethodResponse(obj)[1]
|
233
|
+
|
234
|
+
when :server
|
235
|
+
vv = @parser.parseMethodCall(obj)
|
236
|
+
RPCProtocol::Call.new('', vv[0], vv[1])
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def complete? obj
|
241
|
+
case @mode
|
242
|
+
when :client
|
243
|
+
obj.index("</methodResponse>")
|
244
|
+
when :server
|
245
|
+
obj.index("</methodCall>")
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# This simulates a very basic HTTP XMLRPC client/server.
|
251
|
+
# It is not recommended to use this with production code.
|
252
|
+
class HTTPXMLRPCProtocol < XMLRPCProtocol
|
253
|
+
def to obj
|
254
|
+
r = super
|
255
|
+
case @mode
|
256
|
+
when :client
|
257
|
+
"GET / HTTP/1.[01]\r\nContent-Length: #{r.size}\r\n\r\n" + r
|
258
|
+
when :server
|
259
|
+
"HTTP/1.0 200 OK\r\nContent-Length: #{r.size}\r\n\r\n" + r
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def from obj
|
264
|
+
# Simply strip all HTTP headers.
|
265
|
+
header, obj = obj.split(/\r\n\r\n/, 2)
|
266
|
+
super(obj)
|
267
|
+
end
|
268
|
+
|
269
|
+
|
270
|
+
def complete? obj
|
271
|
+
# Complete if: has headers, has content-length, has data of content-length
|
272
|
+
header, body = obj.split(/\r\n\r\n/, 2)
|
273
|
+
|
274
|
+
header =~ /content-length:\s+(\d+)/i or return nil
|
275
|
+
|
276
|
+
content_length = $1.to_i
|
277
|
+
body.size == content_length ? header.size + 4 + body.size : nil
|
49
278
|
end
|
50
279
|
end
|
51
280
|
end
|
data/lib/arpie/proxy.rb
CHANGED
@@ -1,40 +1,50 @@
|
|
1
1
|
module Arpie
|
2
2
|
|
3
|
-
# The RPC call encapsulation used by ProxyEndpoint and Proxy.
|
4
|
-
class ProxyCall < Struct.new(:method, :argv); end
|
5
|
-
|
6
3
|
# A Endpoint which supports arbitary objects as handlers,
|
7
4
|
# instead of a proc.
|
8
5
|
#
|
9
6
|
# Note that this will only export public instance method
|
10
7
|
# of the class as they are defined.
|
11
|
-
class
|
12
|
-
|
8
|
+
class ProxyServer < Server
|
9
|
+
attr_accessor :interface
|
10
|
+
|
11
|
+
# Set a class handler. All instance methods will be
|
12
|
+
# callable over RPC (with a Proxy object).
|
13
|
+
# Consider yourself warned of the security implications:
|
14
|
+
# proxy.instance_eval ..
|
15
|
+
# Optional interface parameter is an array of method
|
16
|
+
# names (as symbols). If given, only those will be
|
17
|
+
# accessible for Transports.
|
18
|
+
def handle handler, interface = nil
|
13
19
|
@handler = handler
|
14
|
-
@interface =
|
20
|
+
@interface = interface
|
21
|
+
self
|
15
22
|
end
|
16
23
|
|
17
24
|
private
|
18
25
|
|
19
|
-
def _handle message
|
20
|
-
@interface.index(message.
|
21
|
-
"
|
22
|
-
|
26
|
+
def _handle endpoint, message
|
27
|
+
if !@handler.respond_to?(message.meth) || (@interface && !@interface.index(message.meth))
|
28
|
+
raise NoMethodError, "No such method: #{message.meth.inspect}"
|
29
|
+
end
|
30
|
+
|
31
|
+
ret = @handler.send(message.meth, *message.argv)
|
32
|
+
endpoint.write_message(ret)
|
23
33
|
end
|
24
34
|
end
|
25
35
|
|
26
|
-
# A Proxy is a wrapper around a
|
27
|
-
# method calls to the remote
|
28
|
-
|
36
|
+
# A Proxy is a wrapper around a Client, which transparently tunnels
|
37
|
+
# method calls to the remote ProxyServer.
|
38
|
+
# Note that the methods of Client cannot be proxied.
|
39
|
+
class ProxyClient < RPCClient
|
29
40
|
|
30
|
-
|
31
|
-
|
32
|
-
@transport = transport
|
41
|
+
def initialize protocol, namespace = ""
|
42
|
+
@protocol, @namespace = protocol, namespace
|
33
43
|
end
|
34
44
|
|
35
|
-
def method_missing
|
36
|
-
call =
|
37
|
-
ret =
|
45
|
+
def method_missing meth, *argv # :nodoc:
|
46
|
+
call = RPCProtocol::Call.new(@namespace, meth, argv)
|
47
|
+
ret = self.request(call)
|
38
48
|
case ret
|
39
49
|
when Exception
|
40
50
|
raise ret
|
data/lib/arpie/server.rb
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
module Arpie
|
2
|
+
|
3
|
+
# Endpoint wraps client IO objects. One Endpoint
|
4
|
+
# per client. This is provided as a convenience
|
5
|
+
# mechanism for protocols to store
|
6
|
+
# protocol-and-client-specific data.
|
7
|
+
class Endpoint
|
8
|
+
attr_reader :io
|
9
|
+
|
10
|
+
attr_reader :server
|
11
|
+
|
12
|
+
def initialize server, io
|
13
|
+
@io, @server = io, server
|
14
|
+
end
|
15
|
+
|
16
|
+
def read_message
|
17
|
+
@server.protocol.read_message(@io)
|
18
|
+
end
|
19
|
+
|
20
|
+
def write_message message
|
21
|
+
@server.protocol.write_message(@io, message)
|
22
|
+
end
|
23
|
+
alias_method :<<, :write_message
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
# A Server is the server-side part of a RPC setup.
|
28
|
+
# It accepts connections (via the acceptor), and handles
|
29
|
+
# incoming RPC calls on them.
|
30
|
+
#
|
31
|
+
# There will be one Thread per connection, so order of
|
32
|
+
# execution with multiple threads is not guaranteed.
|
33
|
+
class Server
|
34
|
+
attr_reader :protocol
|
35
|
+
|
36
|
+
attr_reader :endpoints
|
37
|
+
|
38
|
+
# Create a new Server with the given +Protocol+.
|
39
|
+
# You will need to define a handler, and an acceptor
|
40
|
+
# before it becomes operational.
|
41
|
+
def initialize protocol
|
42
|
+
@protocol = protocol
|
43
|
+
@endpoints = []
|
44
|
+
|
45
|
+
@on_connect = lambda {|server, endpoint| }
|
46
|
+
@on_disconnect = lambda {|server, endpoint, exception| }
|
47
|
+
@on_handler_error = lambda {|server, endpoint, message, exception|
|
48
|
+
$stderr.puts "Error in handler: #{exception.message.to_s}"
|
49
|
+
$stderr.puts exception.backtrace.join("\n")
|
50
|
+
$stderr.puts "Returning exception for this call."
|
51
|
+
Exception.new("internal error")
|
52
|
+
}
|
53
|
+
@handler = lambda {|server, endpoint, message| raise ArgumentError, "No handler defined." }
|
54
|
+
end
|
55
|
+
|
56
|
+
# Provide an acceptor; this will be run in a a loop
|
57
|
+
# to get IO objects.
|
58
|
+
#
|
59
|
+
# Example:
|
60
|
+
# listener = TCPServer.new(12345)
|
61
|
+
# my_server.accept do
|
62
|
+
# listener.accept
|
63
|
+
# end
|
64
|
+
def accept &acceptor #:yields: server
|
65
|
+
@acceptor = acceptor
|
66
|
+
Thread.new { _acceptor_thread }
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
# Set a message handler, which is a proc that will receive
|
71
|
+
# three parameters: the server, the endpoint, and the message.
|
72
|
+
#
|
73
|
+
# Example:
|
74
|
+
# my_server.handle do |server, endpoint, message|
|
75
|
+
# puts "Got a message: #{message.inspect}"
|
76
|
+
# endpoint.write_message "ok"
|
77
|
+
# end
|
78
|
+
def handle &handler #:yields: server, endpoint, message
|
79
|
+
raise ArgumentError, "No handler given; need a block or proc." unless handler
|
80
|
+
@handler = handler
|
81
|
+
self
|
82
|
+
end
|
83
|
+
|
84
|
+
# Set an error handler.
|
85
|
+
# The return value will be sent to the client.
|
86
|
+
#
|
87
|
+
# Default is to print the exception to stderr, and return
|
88
|
+
# a generic exception that does not leak information.
|
89
|
+
def on_handler_error &handler #:yields: server, endpoint, message, exception
|
90
|
+
raise ArgumentError, "No handler given; need a block or proc." unless handler
|
91
|
+
@on_handler_error = handler
|
92
|
+
self
|
93
|
+
end
|
94
|
+
|
95
|
+
# Callback that gets invoked when a new client connects.
|
96
|
+
# You can <tt>throw :kill_client</tt> here to stop this client
|
97
|
+
# from connecting. Clients stopped this way will invoke
|
98
|
+
# the on_disconnect handler normally.
|
99
|
+
def on_connect &handler #:yields: server, endpoint
|
100
|
+
raise ArgumentError, "No handler given; need a block or proc." unless handler
|
101
|
+
@on_connect = handler
|
102
|
+
self
|
103
|
+
end
|
104
|
+
|
105
|
+
# Callback that gets invoked when a client disconnects.
|
106
|
+
# The exception is the error that occured (usually a EOFError).
|
107
|
+
def on_disconnect &handler #:yields: server, endpoint, exception
|
108
|
+
raise ArgumentError, "No handler given; need a block or proc." unless handler
|
109
|
+
@on_disconnect = handler
|
110
|
+
self
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def _handle endpoint, message
|
116
|
+
@handler.call(self, endpoint, message)
|
117
|
+
end
|
118
|
+
|
119
|
+
def _acceptor_thread
|
120
|
+
loop do
|
121
|
+
client = @acceptor.call(self)
|
122
|
+
c = @protocol.endpoint_klass.new(self, client)
|
123
|
+
Thread.new { _read_thread(c) }
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def _read_thread endpoint
|
128
|
+
@endpoints << endpoint
|
129
|
+
_exception = nil
|
130
|
+
|
131
|
+
catch(:kill_client) {
|
132
|
+
@on_connect.call(self, endpoint)
|
133
|
+
|
134
|
+
loop do
|
135
|
+
message, answer = nil, nil
|
136
|
+
|
137
|
+
begin
|
138
|
+
message = endpoint.read_message
|
139
|
+
rescue IOError => e
|
140
|
+
_exception = e
|
141
|
+
break
|
142
|
+
end
|
143
|
+
|
144
|
+
begin
|
145
|
+
answer = _handle(endpoint, message)
|
146
|
+
rescue Exception => e
|
147
|
+
answer = @on_handler_error.call(self, endpoint, message, e)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
}
|
151
|
+
|
152
|
+
@on_disconnect.call(self, endpoint, _exception)
|
153
|
+
@endpoints.delete(endpoint)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
data/tools/benchmark.rb
CHANGED
@@ -16,18 +16,17 @@ include Arpie
|
|
16
16
|
|
17
17
|
server = TCPServer.new(51210)
|
18
18
|
|
19
|
-
endpoint =
|
19
|
+
endpoint = ProxyServer.new MarshalProtocol.new
|
20
20
|
endpoint.handle Wrap.new
|
21
21
|
|
22
22
|
endpoint.accept do
|
23
23
|
server.accept
|
24
24
|
end
|
25
25
|
|
26
|
-
$
|
27
|
-
$
|
26
|
+
$proxy = ProxyClient.new MarshalProtocol.new
|
27
|
+
$proxy.connect(true) do
|
28
28
|
TCPSocket.new("127.0.0.1", 51210)
|
29
29
|
end
|
30
|
-
$proxy = Proxy.new $transport
|
31
30
|
|
32
31
|
Benchmark.bm {|b|
|
33
32
|
|
@@ -54,4 +53,13 @@ Benchmark.bm {|b|
|
|
54
53
|
puts "Arpie: proxied MarshalProtocol"
|
55
54
|
b.report(" 1") { 1.times { $proxy.reverse "benchmark" } }
|
56
55
|
b.report("1000") { 1000.times { $proxy.reverse "benchmark" } }
|
56
|
+
|
57
|
+
|
58
|
+
def evented_call
|
59
|
+
$transport.request(ProxyCall.new("reverse",["benchmark"])) do end
|
60
|
+
end
|
61
|
+
puts ""
|
62
|
+
# puts "Arpie: evented messaging"
|
63
|
+
# b.report(" 1") { 1.times { evented_call } }
|
64
|
+
# b.report("1000") { 1000.times { evented_call } }
|
57
65
|
}
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'socket'
|
3
|
+
require 'arpie'
|
4
|
+
require 'benchmark'
|
5
|
+
|
6
|
+
include Arpie
|
7
|
+
|
8
|
+
# Data test size.
|
9
|
+
DATA_SIZE = 512
|
10
|
+
|
11
|
+
rpc_call = RPCProtocol::Call.new('ns.', 'meth', [1, 2, 3, 4])
|
12
|
+
$test_data = "a" * DATA_SIZE
|
13
|
+
$test_data.freeze
|
14
|
+
|
15
|
+
# Protocols to test:
|
16
|
+
PROTOCOLS = {
|
17
|
+
MarshalProtocol => $test_data,
|
18
|
+
SizedProtocol => $test_data,
|
19
|
+
ShellwordsProtocol => $test_data,
|
20
|
+
SeparatorProtocol => $test_data,
|
21
|
+
YAMLProtocol => $test_data,
|
22
|
+
# XMLRPCProtocol => [rpc_call, :server],
|
23
|
+
# HTTPXMLRPCProtocol => [rpc_call, :client],
|
24
|
+
}
|
25
|
+
|
26
|
+
ITERATIONS = 1000
|
27
|
+
|
28
|
+
$stderr.puts "Testing protocols with a data size of #{DATA_SIZE}, #{ITERATIONS} iterations"
|
29
|
+
|
30
|
+
|
31
|
+
Benchmark.bm {|b|
|
32
|
+
r, w = IO.pipe
|
33
|
+
PROTOCOLS.each {|p, (d, a)|
|
34
|
+
a ||= []
|
35
|
+
proto = p.new(*a)
|
36
|
+
r, w = IO.pipe
|
37
|
+
|
38
|
+
b.report("%-30s" % p.to_s) {
|
39
|
+
ITERATIONS.times do
|
40
|
+
proto.write_message(w, d)
|
41
|
+
proto.read_message(r)
|
42
|
+
end
|
43
|
+
}
|
44
|
+
}
|
45
|
+
}
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: arpie
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bernhard Stoeckner
|
@@ -9,11 +9,11 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-
|
12
|
+
date: 2009-02-10 00:00:00 +01:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
16
|
-
description:
|
16
|
+
description: A high-performing layered networking protocol framework. Simple to use, simple to extend.
|
17
17
|
email: elven@swordcoast.net
|
18
18
|
executables: []
|
19
19
|
|
@@ -30,11 +30,12 @@ files:
|
|
30
30
|
- spec/rcov.opts
|
31
31
|
- lib/arpie.rb
|
32
32
|
- lib/arpie
|
33
|
+
- lib/arpie/client.rb
|
33
34
|
- lib/arpie/protocol.rb
|
34
|
-
- lib/arpie/transport.rb
|
35
|
-
- lib/arpie/endpoint.rb
|
36
35
|
- lib/arpie/proxy.rb
|
36
|
+
- lib/arpie/server.rb
|
37
37
|
- tools/benchmark.rb
|
38
|
+
- tools/protocol_benchmark.rb
|
38
39
|
has_rdoc: true
|
39
40
|
homepage: http://arpie.elv.es
|
40
41
|
post_install_message:
|
@@ -43,7 +44,7 @@ rdoc_options:
|
|
43
44
|
- --line-numbers
|
44
45
|
- --inline-source
|
45
46
|
- --title
|
46
|
-
- "arpie: A high-performing layered
|
47
|
+
- "arpie: A high-performing layered networking protocol framework. Simple to use, simple to extend."
|
47
48
|
- --main
|
48
49
|
- README
|
49
50
|
- --exclude
|
@@ -68,6 +69,6 @@ rubyforge_project: arpie
|
|
68
69
|
rubygems_version: 1.3.0
|
69
70
|
signing_key:
|
70
71
|
specification_version: 2
|
71
|
-
summary:
|
72
|
+
summary: A high-performing layered networking protocol framework. Simple to use, simple to extend.
|
72
73
|
test_files: []
|
73
74
|
|
data/lib/arpie/endpoint.rb
DELETED
@@ -1,94 +0,0 @@
|
|
1
|
-
module Arpie
|
2
|
-
|
3
|
-
# A Endpoint is the server-side part of a RPC setup.
|
4
|
-
# It accepts connections (via the acceptor), and handles
|
5
|
-
# incoming RPC calls on them.
|
6
|
-
#
|
7
|
-
# There will be one Thread per connection, so order of
|
8
|
-
# execution with multiple threads is not guaranteed.
|
9
|
-
class Endpoint
|
10
|
-
|
11
|
-
# Create a new Endpoint with the given +Protocol+.
|
12
|
-
# You will need to define a handler, and an acceptor
|
13
|
-
# before the endpoint becomes operational.
|
14
|
-
def initialize protocol
|
15
|
-
@protocol = protocol
|
16
|
-
@clients = []
|
17
|
-
|
18
|
-
@handler = lambda {|endpoint, message| raise ArgumentError, "No handler defined." }
|
19
|
-
end
|
20
|
-
|
21
|
-
# Provide an acceptor; this will be run in a a loop
|
22
|
-
# to get IO objects.
|
23
|
-
#
|
24
|
-
# Example:
|
25
|
-
# listener = TCPServer.new(12345)
|
26
|
-
# my_endpoint.accept do
|
27
|
-
# listener.accept
|
28
|
-
# end
|
29
|
-
def accept &acceptor
|
30
|
-
@acceptor = acceptor
|
31
|
-
Thread.new { _acceptor_thread }
|
32
|
-
end
|
33
|
-
|
34
|
-
# Set a message handler, which is a proc that will receive
|
35
|
-
# two parameters: the endpoint, and the message.
|
36
|
-
# Its return value will be sent as the reply.
|
37
|
-
#
|
38
|
-
# Example:
|
39
|
-
# my_endpoint.handle do |endpoint, message|
|
40
|
-
# puts "Got a message: #{message.inspect}"
|
41
|
-
# "ok"
|
42
|
-
# end
|
43
|
-
def handle &handler
|
44
|
-
raise ArgumentError, "need a block" unless block_given?
|
45
|
-
@handler = handler
|
46
|
-
end
|
47
|
-
|
48
|
-
private
|
49
|
-
|
50
|
-
def _handle message
|
51
|
-
@handler.call(self, message)
|
52
|
-
end
|
53
|
-
|
54
|
-
def _acceptor_thread
|
55
|
-
loop do
|
56
|
-
client = @acceptor.call(self)
|
57
|
-
@clients << client
|
58
|
-
Thread.new { _read_thread(client) }
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
def _read_thread client
|
63
|
-
loop do
|
64
|
-
break if client.eof?
|
65
|
-
|
66
|
-
message, answer = nil, nil
|
67
|
-
begin
|
68
|
-
message = @protocol.read_message(client)
|
69
|
-
rescue => e
|
70
|
-
$stderr.puts "client went away while reading the message: #{e.to_s}"
|
71
|
-
break
|
72
|
-
end
|
73
|
-
|
74
|
-
begin
|
75
|
-
answer = _handle(message)
|
76
|
-
rescue Exception => e
|
77
|
-
$stderr.puts "Error in handler: #{e.message.to_s}"
|
78
|
-
$stderr.puts e.backtrace.join("\n")
|
79
|
-
$stderr.puts "Returning exception for this call."
|
80
|
-
answer = e
|
81
|
-
end
|
82
|
-
|
83
|
-
begin
|
84
|
-
@protocol.write_message(client, answer)
|
85
|
-
rescue => e
|
86
|
-
puts "client went away while writing the answer:: #{e.to_s}"
|
87
|
-
break
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
@clients.delete(client)
|
92
|
-
end
|
93
|
-
end
|
94
|
-
end
|
data/lib/arpie/transport.rb
DELETED
@@ -1,37 +0,0 @@
|
|
1
|
-
module Arpie
|
2
|
-
|
3
|
-
# A Transport is a connection manager, and acts as the
|
4
|
-
# glue between a user-defined medium (for example, a TCP
|
5
|
-
# socket), and a protocol.
|
6
|
-
#
|
7
|
-
# See README for examples.
|
8
|
-
class Transport
|
9
|
-
attr_reader :protocol
|
10
|
-
|
11
|
-
def initialize protocol
|
12
|
-
@protocol = protocol
|
13
|
-
@io = nil
|
14
|
-
end
|
15
|
-
|
16
|
-
# Provide a connector block, which will be called
|
17
|
-
# each time a connection is needed.
|
18
|
-
# Set +connect_immediately+ to true to connect
|
19
|
-
# immediately, instead on the first message.
|
20
|
-
def connect connect_immediately = false, &connector
|
21
|
-
@connector = connector
|
22
|
-
_connect if connect_immediately
|
23
|
-
end
|
24
|
-
|
25
|
-
# Send a message and receive a reply.
|
26
|
-
def request message
|
27
|
-
_connect
|
28
|
-
@protocol.write_message(@io, message)
|
29
|
-
@protocol.read_message(@io)
|
30
|
-
end
|
31
|
-
|
32
|
-
private
|
33
|
-
def _connect
|
34
|
-
@io ||= @connector.call(self)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|