arpie 0.0.1 → 0.0.2
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 +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
|