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 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::Endpoint.new(Arpie::MarshalProtocol.new)
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::Transport.new(Arpie::MarshalProtocol.new)
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
- puts c.request "hi"
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::ProxyEndpoint.new(Arpie::MarshalProtocol.new)
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
- c = Arpie::Transport.new(Arpie::MarshalProtocol.new)
66
- c.connect do |transport|
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.1"
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 RPC framework. Simple to use, simple to extend.", \
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 = "a synchronous RPC library based on google protobuf"
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"
@@ -1,4 +1,4 @@
1
1
  require 'arpie/protocol'
2
- require 'arpie/transport'
3
- require 'arpie/endpoint'
2
+ require 'arpie/server'
3
+ require 'arpie/client'
4
4
  require 'arpie/proxy'
@@ -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
@@ -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
- # Write a message to +io+.
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 sample binary protocol, upon which others can expand.
19
- # The on the wire format is simply the data, prefixed
20
- # with data.size.
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 read_message io
27
- sz = io.read(8)
28
- expect = sz.unpack("Q")[0]
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 write_message io, message
33
- io.write([message.size, message].pack("Qa*"))
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 read_message io
44
- Marshal.load super(io)
167
+ def complete? obj
168
+ obj =~ /\.\.\.$/
45
169
  end
46
170
 
47
- def write_message io, message
48
- super io, Marshal.dump(message)
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
@@ -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 ProxyEndpoint < Endpoint
12
- def handle handler
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 = @handler.class.public_instance_methods(false)
20
+ @interface = interface
21
+ self
15
22
  end
16
23
 
17
24
  private
18
25
 
19
- def _handle message
20
- @interface.index(message.method.to_s) or raise NoMethodError,
21
- "Unknown method."
22
- @handler.send(message.method, *message.argv)
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 transport, which transparently tunnels
27
- # method calls to the remote ProxyEndpoint.
28
- class Proxy
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
- # Create a new Proxy.
31
- def initialize transport
32
- @transport = transport
41
+ def initialize protocol, namespace = ""
42
+ @protocol, @namespace = protocol, namespace
33
43
  end
34
44
 
35
- def method_missing method, *argv # :nodoc:
36
- call = ProxyCall.new(method, argv)
37
- ret = @transport.request(call)
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
@@ -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
@@ -16,18 +16,17 @@ include Arpie
16
16
 
17
17
  server = TCPServer.new(51210)
18
18
 
19
- endpoint = ProxyEndpoint.new MarshalProtocol.new
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
- $transport = Transport.new MarshalProtocol.new
27
- $transport.connect(false) do |transport|
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.1
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-01-18 00:00:00 +01:00
12
+ date: 2009-02-10 00:00:00 +01:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
16
- description: a synchronous RPC library based on google protobuf
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 RPC framework. Simple to use, simple to extend."
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: a synchronous RPC library based on google protobuf
72
+ summary: A high-performing layered networking protocol framework. Simple to use, simple to extend.
72
73
  test_files: []
73
74
 
@@ -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
@@ -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