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 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