celluloid-io 0.8.0 → 0.9.0
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/.travis.yml +5 -3
- data/CHANGES.md +7 -0
- data/README.md +114 -40
- data/celluloid-io.gemspec +3 -3
- data/examples/echo_server.rb +38 -0
- data/lib/celluloid/io.rb +5 -0
- data/lib/celluloid/io/common_methods.rb +96 -0
- data/lib/celluloid/io/mailbox.rb +14 -13
- data/lib/celluloid/io/reactor.rb +17 -10
- data/lib/celluloid/io/tcp_server.rb +40 -0
- data/lib/celluloid/io/tcp_socket.rb +34 -0
- data/lib/celluloid/io/udp_socket.rb +48 -0
- data/lib/celluloid/io/version.rb +1 -1
- data/logo.png +0 -0
- data/spec/celluloid/io/tcp_server_spec.rb +47 -0
- data/spec/celluloid/io/tcp_socket_spec.rb +91 -0
- data/spec/celluloid/io/udp_socket_spec.rb +34 -0
- data/spec/spec_helper.rb +45 -1
- metadata +26 -14
data/.travis.yml
CHANGED
data/CHANGES.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
0.9.0
|
2
|
+
-----
|
3
|
+
* TCPServer, TCPSocket, and UDPSocket classes in Celluloid::IO namespace
|
4
|
+
with both evented and blocking I/O support
|
5
|
+
* Celluloid::IO::Mailbox.new now takes a single parameter to specify an
|
6
|
+
alternative reactor (e.g. Celluloid::ZMQ::Reactor)
|
7
|
+
|
1
8
|
0.8.0
|
2
9
|
-----
|
3
10
|
* Switch to nio4r-based reactor
|
data/README.md
CHANGED
@@ -1,82 +1,156 @@
|
|
1
|
-
Celluloid
|
1
|
+

|
2
2
|
=============
|
3
|
-
[](http://travis-ci.org/tarcieri/celluloid-io)
|
4
|
+
[](https://gemnasium.com/tarcieri/celluloid-io)
|
5
|
+
|
6
|
+
You don't have to choose between threaded and evented IO! Celluloid::IO
|
7
|
+
provides an event-driven IO system for building fast, scalable network
|
8
|
+
applications that integrates directly with the
|
9
|
+
[Celluloid actor library](https://github.com/tarcieri/celluloid), making it
|
10
|
+
easy to combine both threaded and evented concepts. Celluloid::IO is ideal for
|
11
|
+
servers which handle large numbers of mostly-idle connections, such as Websocket
|
12
|
+
servers or chat/messaging systems.
|
13
|
+
|
14
|
+
Celluloid::IO provides a different class of actor: one that's slightly slower
|
15
|
+
and heavier than standard Celluloid actors, but one which contains a
|
16
|
+
high-performance reactor just like EventMachine or Cool.io. This means
|
17
|
+
Celluloid::IO actors have the power of both Celluloid actors and evented
|
18
|
+
I/O loops. Unlike certain other evented I/O systems which limit you to a
|
19
|
+
single event loop per process, Celluloid::IO lets you make as many actors as
|
20
|
+
you want, system resources permitting.
|
21
|
+
|
22
|
+
Rather than callbacks, Celluloid::IO exposes a synchronous API built on duck
|
23
|
+
types of Ruby's own IO classes, such as TCPServer and TCPSocket. These classes
|
24
|
+
work identically to their core Ruby counterparts, but in the scope of
|
25
|
+
Celluloid::IO actors provide "evented" performance. Since they're drop-in
|
26
|
+
replacements for the standard classes, there's no need to rewrite every
|
27
|
+
library just to take advantage of Celluloid::IO's event loop and you can
|
28
|
+
freely switch between evented and blocking IO even over the lifetime of a
|
29
|
+
single connection.
|
30
|
+
|
31
|
+
Celluloid::IO uses the [nio4r gem](https://github.com/tarcieri/nio4r)
|
32
|
+
to monitor IO objects, which provides cross-platform and cross-Ruby
|
33
|
+
implementation access to high-performance system calls such as epoll
|
34
|
+
and kqueue.
|
17
35
|
|
18
36
|
Like Celluloid::IO? [Join the Google Group](http://groups.google.com/group/celluloid-ruby)
|
19
37
|
|
20
38
|
Supported Platforms
|
21
39
|
-------------------
|
22
40
|
|
23
|
-
Celluloid works on Ruby 1.9.2+, JRuby 1.6 (in 1.9 mode), and Rubinius 2.0.
|
24
|
-
or Rubinius are the preferred platforms as they support true hardware-level
|
25
|
-
parallelism when running Ruby code, whereas MRI/YARV is constrained by a global
|
26
|
-
interpreter lock (GIL).
|
41
|
+
Celluloid::IO works on Ruby 1.9.2+, JRuby 1.6 (in 1.9 mode), and Rubinius 2.0.
|
27
42
|
|
28
43
|
To use JRuby in 1.9 mode, you'll need to pass the "--1.9" command line option
|
29
44
|
to the JRuby executable, or set the "JRUBY_OPTS=--1.9" environment variable.
|
30
45
|
|
31
|
-
Celluloid works on Rubinius in either 1.8 or 1.9 mode.
|
32
|
-
|
33
46
|
Usage
|
34
47
|
-----
|
35
48
|
|
36
|
-
To use Celluloid::IO, define a normal Ruby class that includes Celluloid::IO
|
49
|
+
To use Celluloid::IO, define a normal Ruby class that includes Celluloid::IO.
|
50
|
+
The following is an example of an echo server:
|
37
51
|
|
38
52
|
```ruby
|
39
53
|
require 'celluloid/io'
|
40
54
|
|
41
|
-
class
|
55
|
+
class EchoServer
|
42
56
|
include Celluloid::IO
|
43
57
|
|
44
|
-
# Bind a TCP server to the given host and port
|
45
58
|
def initialize(host, port)
|
46
|
-
|
59
|
+
puts "*** Starting echo server on #{host}:#{port}"
|
60
|
+
|
61
|
+
# Since we included Celluloid::IO, we're actually making a
|
62
|
+
# Celluloid::IO::TCPServer here
|
63
|
+
@server = TCPServer.new(host, port)
|
47
64
|
run!
|
48
65
|
end
|
49
66
|
|
50
|
-
|
51
|
-
|
52
|
-
while true
|
53
|
-
wait_readable(@server)
|
54
|
-
on_connect @server.accept
|
55
|
-
end
|
67
|
+
def finalize
|
68
|
+
@server.close if @server
|
56
69
|
end
|
57
70
|
|
58
|
-
|
59
|
-
|
60
|
-
@server.close
|
61
|
-
super
|
71
|
+
def run
|
72
|
+
loop { handle_connection! @server.accept }
|
62
73
|
end
|
63
74
|
|
64
|
-
|
65
|
-
|
66
|
-
connection
|
75
|
+
def handle_connection(socket)
|
76
|
+
_, port, host = socket.peeraddr
|
77
|
+
puts "*** Received connection from #{host}:#{port}"
|
78
|
+
loop { socket.write socket.readpartial(4096) }
|
79
|
+
rescue EOFError
|
80
|
+
puts "*** #{host}:#{port} disconnected"
|
67
81
|
end
|
68
82
|
end
|
69
83
|
```
|
70
84
|
|
85
|
+
The very first thing including *Celluloid::IO* does is also include the
|
86
|
+
*Celluloid* module, which promotes objects of this class to concurrent Celluloid
|
87
|
+
actors each running in their own thread. Before trying to use Celluloid::IO
|
88
|
+
you may want to [familiarize yourself with Celluloid in general](https://github.com/tarcieri/celluloid/).
|
89
|
+
Celluloid actors can each be thought of as being event loops. Celluloid::IO actors
|
90
|
+
are heavier but have capabilities similar to other event loop-driven frameworks.
|
91
|
+
|
92
|
+
While this looks like a normal Ruby TCP server, there aren't any threads, so
|
93
|
+
you might expect this server can only handle one connection at a time.
|
94
|
+
However, this is all you need to do to build servers that handle as many
|
95
|
+
connections as you want, and it happens all within a single thread.
|
96
|
+
|
97
|
+
The magic in this server which allows it to handle multiple connections
|
98
|
+
comes in three forms:
|
99
|
+
|
100
|
+
* __Replacement classes:__ Celluloid::IO includes replacements for the core
|
101
|
+
TCPServer and TCPSocket classes which automatically use an evented mode
|
102
|
+
inside of Celluloid::IO actors. They're named Celluloid::IO::TCPServer and
|
103
|
+
Celluloid::IO::TCPSocket, so they're automatically available inside
|
104
|
+
your class when you include Celluloid::IO.
|
105
|
+
|
106
|
+
* __Asynchronous method calls:__ You may have noticed that while the methods
|
107
|
+
of EchoServer are named *run* and *handle_connection*, they're invoked as
|
108
|
+
*run!* and *handle_connection!*. This queues these methods to be executed
|
109
|
+
after the current method is complete. You can queue up as many methods as
|
110
|
+
you want, allowing asynchronous operation similar to the "call later" or
|
111
|
+
"next tick" feature of Twisted, EventMachine, and Node. This echo server
|
112
|
+
first kicks off a background task for accepting connections on the server
|
113
|
+
socket, then kicks off a background task for each connection.
|
114
|
+
|
115
|
+
* __Reactor + Fibers:__ Celluloid::IO is a combination of Actor and Reactor
|
116
|
+
concepts. The blocking mechanism used by the mailboxes of Celluloid::IO
|
117
|
+
actors is an [nio4r-powered reactor](https://github.com/tarcieri/celluloid-io/blob/master/lib/celluloid/io/reactor.rb).
|
118
|
+
When the current task needs to make a blocking I/O call, it first makes
|
119
|
+
a non-blocking attempt, and if the socket isn't ready the current task
|
120
|
+
is suspended until the reactor detects the operation is ready and resumes
|
121
|
+
the suspended task.
|
122
|
+
|
123
|
+
The result is an API for doing evented I/O that looks identical to doing
|
124
|
+
synchronous I/O. Adapting existing synchronous libraries to using evented I/O
|
125
|
+
is as simple as having them use one of Celluloid::IO's provided replacement
|
126
|
+
classes instead of the core Ruby TCPSocket and TCPServer classes.
|
127
|
+
|
128
|
+
Status
|
129
|
+
------
|
130
|
+
|
131
|
+
The rudiments of TCPServer and TCPSocket are in place and ready to use.
|
132
|
+
Several methods are still missing. Making new connections with
|
133
|
+
Celluloid::IO::TCPSocket.new works, however it presently does blocking DNS
|
134
|
+
resolution and connect so it can stall the reactor loop.
|
135
|
+
|
136
|
+
Basic UDPSocket support is in place. On JRuby, recvfrom makes a blocking call
|
137
|
+
as the underlying recvfrom_nonblock call is not supported by JRuby.
|
138
|
+
|
139
|
+
No UNIXSocket support yet, sorry (patches welcome!)
|
140
|
+
|
71
141
|
Contributing to Celluloid::IO
|
72
142
|
-----------------------------
|
73
143
|
|
74
|
-
* Fork
|
144
|
+
* Fork this repository on github
|
75
145
|
* Make your changes and send me a pull request
|
76
|
-
* If I like them I'll merge them
|
146
|
+
* If I like them I'll merge them
|
147
|
+
* If I've accepted a patch, feel free to ask for a commit bit!
|
77
148
|
|
78
149
|
License
|
79
150
|
-------
|
80
151
|
|
81
152
|
Copyright (c) 2011 Tony Arcieri. Distributed under the MIT License. See
|
82
153
|
LICENSE.txt for further details.
|
154
|
+
|
155
|
+
Contains code originally from the RubySpec project also under the MIT License
|
156
|
+
Copyright (c) 2008 Engine Yard, Inc. All rights reserved.
|
data/celluloid-io.gemspec
CHANGED
@@ -15,9 +15,9 @@ Gem::Specification.new do |gem|
|
|
15
15
|
gem.require_paths = ["lib"]
|
16
16
|
gem.version = Celluloid::IO::VERSION
|
17
17
|
|
18
|
-
gem.add_dependency 'celluloid', '~> 0.
|
19
|
-
gem.add_dependency 'nio4r', '>= 0.
|
18
|
+
gem.add_dependency 'celluloid', '~> 0.9.0'
|
19
|
+
gem.add_dependency 'nio4r', '>= 0.3.1'
|
20
20
|
|
21
21
|
gem.add_development_dependency 'rake'
|
22
|
-
gem.add_development_dependency 'rspec'
|
22
|
+
gem.add_development_dependency 'rspec'
|
23
23
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.push File.expand_path('../../lib', __FILE__)
|
4
|
+
require 'celluloid/io'
|
5
|
+
|
6
|
+
class EchoServer
|
7
|
+
include Celluloid::IO
|
8
|
+
|
9
|
+
def initialize(host, port)
|
10
|
+
puts "*** Starting echo server on #{host}:#{port}"
|
11
|
+
|
12
|
+
# Since we included Celluloid::IO, we're actually making a
|
13
|
+
# Celluloid::IO::TCPServer here
|
14
|
+
@server = TCPServer.new(host, port)
|
15
|
+
run!
|
16
|
+
end
|
17
|
+
|
18
|
+
def finalize
|
19
|
+
@server.close if @server
|
20
|
+
end
|
21
|
+
|
22
|
+
def run
|
23
|
+
loop { handle_connection! @server.accept }
|
24
|
+
end
|
25
|
+
|
26
|
+
def handle_connection(socket)
|
27
|
+
_, port, host = socket.peeraddr
|
28
|
+
puts "*** Received connection from #{host}:#{port}"
|
29
|
+
loop { socket.write socket.readpartial(4096) }
|
30
|
+
rescue EOFError
|
31
|
+
puts "*** #{host}:#{port} disconnected"
|
32
|
+
socket.close
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
supervisor = EchoServer.supervise("127.0.0.1", 1234)
|
37
|
+
trap("INT") { supervisor.terminate; exit }
|
38
|
+
sleep
|
data/lib/celluloid/io.rb
CHANGED
@@ -2,9 +2,14 @@ require 'celluloid/io/version'
|
|
2
2
|
|
3
3
|
require 'forwardable'
|
4
4
|
require 'celluloid'
|
5
|
+
require 'celluloid/io/common_methods'
|
5
6
|
require 'celluloid/io/mailbox'
|
6
7
|
require 'celluloid/io/reactor'
|
7
8
|
|
9
|
+
require 'celluloid/io/tcp_server'
|
10
|
+
require 'celluloid/io/tcp_socket'
|
11
|
+
require 'celluloid/io/udp_socket'
|
12
|
+
|
8
13
|
module Celluloid
|
9
14
|
# Actors with evented IO support
|
10
15
|
module IO
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Celluloid
|
2
|
+
module IO
|
3
|
+
# Common implementations of methods originall from the IO class
|
4
|
+
module CommonMethods
|
5
|
+
# Are we inside of a Celluloid::IO actor?
|
6
|
+
def evented?
|
7
|
+
actor = Thread.current[:actor]
|
8
|
+
actor && actor.mailbox.is_a?(Celluloid::IO::Mailbox)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Wait until the current object is readable
|
12
|
+
def wait_readable
|
13
|
+
if evented?
|
14
|
+
Celluloid.current_actor.wait_readable(self.to_io)
|
15
|
+
else
|
16
|
+
Kernel.select([self.to_io])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Wait until the current object is writable
|
21
|
+
def wait_writable
|
22
|
+
actor = Thread.current[:actor]
|
23
|
+
|
24
|
+
if evented?
|
25
|
+
Celluloid.current_actor.wait_writable(self.to_io)
|
26
|
+
else
|
27
|
+
Kernel.select([], [self.to_io])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def read(length, buffer = nil)
|
32
|
+
buffer ||= ''
|
33
|
+
remaining = length
|
34
|
+
|
35
|
+
until remaining.zero?
|
36
|
+
begin
|
37
|
+
str = readpartial(remaining)
|
38
|
+
rescue EOFError
|
39
|
+
return if length == remaining
|
40
|
+
return buffer
|
41
|
+
end
|
42
|
+
|
43
|
+
buffer << str
|
44
|
+
remaining -= str.length
|
45
|
+
end
|
46
|
+
|
47
|
+
buffer
|
48
|
+
end
|
49
|
+
|
50
|
+
def readpartial(length, buffer = nil)
|
51
|
+
buffer ||= ''
|
52
|
+
|
53
|
+
begin
|
54
|
+
read_nonblock(length, buffer)
|
55
|
+
rescue ::IO::WaitReadable
|
56
|
+
wait_readable
|
57
|
+
retry
|
58
|
+
end
|
59
|
+
|
60
|
+
buffer
|
61
|
+
end
|
62
|
+
|
63
|
+
def write(string)
|
64
|
+
length = string.length
|
65
|
+
total_written = 0
|
66
|
+
|
67
|
+
remaining = string
|
68
|
+
while total_written < length
|
69
|
+
begin
|
70
|
+
written = write_nonblock(remaining)
|
71
|
+
rescue ::IO::WaitWritable
|
72
|
+
wait_writable
|
73
|
+
retry
|
74
|
+
rescue EOFError
|
75
|
+
return total_written
|
76
|
+
end
|
77
|
+
|
78
|
+
total_written += written
|
79
|
+
if written < remaining.length
|
80
|
+
# Avoid mutating string itself, but we can mutate the remaining data
|
81
|
+
if remaining == string
|
82
|
+
# Copy the remaining data so as to avoid mutating string
|
83
|
+
# Note if we have a large amount of data remaining this could be slow
|
84
|
+
remaining = string[written..-1]
|
85
|
+
else
|
86
|
+
remaining.slice!(0, written)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
total_written
|
92
|
+
end
|
93
|
+
alias_method :<<, :write
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/lib/celluloid/io/mailbox.rb
CHANGED
@@ -4,33 +4,34 @@ module Celluloid
|
|
4
4
|
class Mailbox < Celluloid::Mailbox
|
5
5
|
attr_reader :reactor
|
6
6
|
|
7
|
-
def initialize
|
7
|
+
def initialize(reactor = nil)
|
8
8
|
@messages = []
|
9
|
-
@
|
10
|
-
@reactor = Reactor.new
|
9
|
+
@mutex = Mutex.new
|
10
|
+
@reactor = reactor || Reactor.new
|
11
11
|
end
|
12
12
|
|
13
13
|
# Add a message to the Mailbox
|
14
14
|
def <<(message)
|
15
|
-
@lock
|
15
|
+
@mutex.lock
|
16
|
+
begin
|
16
17
|
@messages << message
|
17
18
|
@reactor.wakeup
|
19
|
+
rescue IOError
|
20
|
+
raise MailboxError, "dead recipient"
|
21
|
+
ensure @mutex.unlock
|
18
22
|
end
|
19
23
|
nil
|
20
|
-
rescue IOError
|
21
|
-
raise MailboxError, "dead recipient"
|
22
24
|
end
|
23
25
|
|
24
26
|
# Add a high-priority system event to the Mailbox
|
25
27
|
def system_event(event)
|
26
|
-
@lock
|
28
|
+
@mutex.lock
|
29
|
+
begin
|
27
30
|
@messages.unshift event
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
# Silently fail if messages are sent to dead actors
|
33
|
-
end
|
31
|
+
@reactor.wakeup
|
32
|
+
rescue IOError
|
33
|
+
# Silently fail if messages are sent to dead actors
|
34
|
+
ensure @mutex.unlock
|
34
35
|
end
|
35
36
|
nil
|
36
37
|
end
|
data/lib/celluloid/io/reactor.rb
CHANGED
@@ -25,29 +25,36 @@ module Celluloid
|
|
25
25
|
def wait_writeable(io)
|
26
26
|
wait io, :w
|
27
27
|
end
|
28
|
-
|
28
|
+
|
29
29
|
# Wait for the given IO operation to complete
|
30
30
|
def wait(io, set)
|
31
31
|
# zomg ugly type conversion :(
|
32
|
-
unless io.is_a?(IO)
|
33
|
-
if
|
34
|
-
io = IO.try_convert(io)
|
35
|
-
elsif io.respond_to? :to_io
|
32
|
+
unless io.is_a?(::IO)
|
33
|
+
if io.respond_to? :to_io
|
36
34
|
io = io.to_io
|
37
|
-
|
35
|
+
elsif ::IO.respond_to? :try_convert
|
36
|
+
io = ::IO.try_convert(io)
|
38
37
|
end
|
38
|
+
|
39
|
+
raise TypeError, "can't convert #{io.class} into IO" unless io.is_a? IO
|
39
40
|
end
|
40
|
-
|
41
|
+
|
41
42
|
monitor = @selector.register(io, set)
|
42
43
|
monitor.value = Task.current
|
43
44
|
Task.suspend :iowait
|
44
45
|
end
|
45
|
-
|
46
|
+
|
46
47
|
# Run the reactor, waiting for events or wakeup signal
|
47
48
|
def run_once(timeout = nil)
|
48
|
-
@selector.
|
49
|
-
monitor.value
|
49
|
+
@selector.select(timeout) do |monitor|
|
50
|
+
task = monitor.value
|
50
51
|
@selector.deregister(monitor.io)
|
52
|
+
|
53
|
+
if task.running?
|
54
|
+
task.resume
|
55
|
+
else
|
56
|
+
Logger.warn("reactor attempted to resume a dead task")
|
57
|
+
end
|
51
58
|
end
|
52
59
|
end
|
53
60
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module Celluloid
|
4
|
+
module IO
|
5
|
+
# TCPServer with combined blocking and evented support
|
6
|
+
class TCPServer
|
7
|
+
extend Forwardable
|
8
|
+
def_delegators :@server, :listen, :sysaccept, :close, :closed?
|
9
|
+
|
10
|
+
def initialize(hostname, port)
|
11
|
+
@server = ::TCPServer.new(hostname, port)
|
12
|
+
end
|
13
|
+
|
14
|
+
def accept
|
15
|
+
actor = Thread.current[:actor]
|
16
|
+
|
17
|
+
if evented?
|
18
|
+
Celluloid.current_actor.wait_readable @server
|
19
|
+
accept_nonblock
|
20
|
+
else
|
21
|
+
Celluloid::IO::TCPSocket.from_ruby_socket @server.accept
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def accept_nonblock
|
26
|
+
Celluloid::IO::TCPSocket.from_ruby_socket @server.accept_nonblock
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_io
|
30
|
+
@server
|
31
|
+
end
|
32
|
+
|
33
|
+
# Are we inside a Celluloid ::IO actor?
|
34
|
+
def evented?
|
35
|
+
actor = Thread.current[:actor]
|
36
|
+
actor && actor.mailbox.is_a?(Celluloid::IO::Mailbox)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module Celluloid
|
4
|
+
module IO
|
5
|
+
# TCPSocket with combined blocking and evented support
|
6
|
+
class TCPSocket
|
7
|
+
include CommonMethods
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
def_delegators :@socket, :read_nonblock, :write_nonblock, :close, :closed?
|
11
|
+
def_delegators :@socket, :addr, :peeraddr
|
12
|
+
|
13
|
+
# Convert a Ruby TCPSocket into a Celluloid::IO::TCPSocket
|
14
|
+
def self.from_ruby_socket(ruby_socket)
|
15
|
+
# Some hax here, but whatever ;)
|
16
|
+
socket = allocate
|
17
|
+
socket.instance_variable_set(:@socket, ruby_socket)
|
18
|
+
socket
|
19
|
+
end
|
20
|
+
|
21
|
+
# Opens a TCP connection to remote_host on remote_port. If local_host
|
22
|
+
# and local_port are specified, then those parameters are used on the
|
23
|
+
# local end to establish the connection.
|
24
|
+
def initialize(remote_host, remote_port, local_host = nil, local_port = nil)
|
25
|
+
# FIXME: not using non-blocking connect
|
26
|
+
@socket = ::TCPSocket.new(remote_host, remote_port, local_host, local_port)
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_io
|
30
|
+
@socket
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Celluloid
|
2
|
+
module IO
|
3
|
+
# UDPSockets with combined blocking and evented support
|
4
|
+
class UDPSocket
|
5
|
+
extend Forwardable
|
6
|
+
def_delegators :@socket, :bind, :send, :recvfrom_nonblock, :close, :closed?
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@socket = ::UDPSocket.new
|
10
|
+
end
|
11
|
+
|
12
|
+
# Are we inside of a Celluloid::IO actor?
|
13
|
+
def evented?
|
14
|
+
actor = Thread.current[:actor]
|
15
|
+
actor && actor.mailbox.is_a?(Celluloid::IO::Mailbox)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Wait until the socket is readable
|
19
|
+
def wait_readable
|
20
|
+
if evented?
|
21
|
+
Celluloid.current_actor.wait_readable(@socket)
|
22
|
+
else
|
23
|
+
Kernel.select([@socket])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Receives up to maxlen bytes from socket. flags is zero or more of the
|
28
|
+
# MSG_ options. The first element of the results, mesg, is the data
|
29
|
+
# received. The second element, sender_addrinfo, contains
|
30
|
+
# protocol-specific address information of the sender.
|
31
|
+
def recvfrom(maxlen, flags = nil)
|
32
|
+
begin
|
33
|
+
if @socket.respond_to? :recvfrom_nonblock
|
34
|
+
@socket.recvfrom_nonblock(maxlen, flags)
|
35
|
+
else
|
36
|
+
# FIXME: hax for JRuby
|
37
|
+
@socket.recvfrom(maxlen, flags)
|
38
|
+
end
|
39
|
+
rescue ::IO::WaitReadable
|
40
|
+
wait_readable
|
41
|
+
retry
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_io; @socket; end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/celluloid/io/version.rb
CHANGED
data/logo.png
ADDED
Binary file
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Celluloid::IO::TCPServer do
|
4
|
+
describe "#accept" do
|
5
|
+
let(:payload) { 'ohai' }
|
6
|
+
|
7
|
+
context "inside Celluloid::IO" do
|
8
|
+
it "should be evented" do
|
9
|
+
with_tcp_server do |subject|
|
10
|
+
within_io_actor { subject.evented? }.should be_true
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
it "accepts a connection and returns a Celluloid::IO::TCPSocket" do
|
15
|
+
with_tcp_server do |subject|
|
16
|
+
thread = Thread.new { TCPSocket.new(example_addr, example_port) }
|
17
|
+
peer = within_io_actor { subject.accept }
|
18
|
+
peer.should be_a Celluloid::IO::TCPSocket
|
19
|
+
|
20
|
+
client = thread.value
|
21
|
+
client.write payload
|
22
|
+
peer.read(payload.size).should eq payload
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context "elsewhere in Ruby" do
|
27
|
+
it "should be blocking" do
|
28
|
+
with_tcp_server do |subject|
|
29
|
+
subject.should_not be_evented
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
it "accepts a connection and returns a Celluloid::IO::TCPSocket" do
|
34
|
+
with_tcp_server do |subject|
|
35
|
+
thread = Thread.new { TCPSocket.new(example_addr, example_port) }
|
36
|
+
peer = subject.accept
|
37
|
+
peer.should be_a Celluloid::IO::TCPSocket
|
38
|
+
|
39
|
+
client = thread.value
|
40
|
+
client.write payload
|
41
|
+
peer.read(payload.size).should eq payload
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Celluloid::IO::TCPSocket do
|
4
|
+
let(:payload) { 'ohai' }
|
5
|
+
|
6
|
+
context "inside Celluloid::IO" do
|
7
|
+
it "connects to TCP servers" do
|
8
|
+
server = ::TCPServer.new example_addr, example_port
|
9
|
+
thread = Thread.new { server.accept }
|
10
|
+
socket = within_io_actor { Celluloid::IO::TCPSocket.new example_addr, example_port }
|
11
|
+
peer = thread.value
|
12
|
+
|
13
|
+
peer << payload
|
14
|
+
within_io_actor { socket.read(payload.size) }.should eq payload
|
15
|
+
|
16
|
+
server.close
|
17
|
+
socket.close
|
18
|
+
peer.close
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should be evented" do
|
22
|
+
with_connected_sockets do |subject|
|
23
|
+
within_io_actor { subject.evented? }.should be_true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
it "reads data" do
|
28
|
+
with_connected_sockets do |subject, peer|
|
29
|
+
peer << payload
|
30
|
+
within_io_actor { subject.read(payload.size) }.should eq payload
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
it "reads partial data" do
|
35
|
+
with_connected_sockets do |subject, peer|
|
36
|
+
peer << payload * 2
|
37
|
+
within_io_actor { subject.readpartial(payload.size) }.should eq payload
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
it "writes data" do
|
42
|
+
with_connected_sockets do |subject, peer|
|
43
|
+
within_io_actor { subject << payload }
|
44
|
+
peer.read(payload.size).should eq payload
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context "elsewhere in Ruby" do
|
50
|
+
it "connects to TCP servers" do
|
51
|
+
server = ::TCPServer.new example_addr, example_port
|
52
|
+
thread = Thread.new { server.accept }
|
53
|
+
socket = Celluloid::IO::TCPSocket.new example_addr, example_port
|
54
|
+
peer = thread.value
|
55
|
+
|
56
|
+
peer << payload
|
57
|
+
socket.read(payload.size).should eq payload
|
58
|
+
|
59
|
+
server.close
|
60
|
+
socket.close
|
61
|
+
peer.close
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should be blocking" do
|
65
|
+
with_connected_sockets do |subject|
|
66
|
+
subject.should_not be_evented
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
it "reads data" do
|
71
|
+
with_connected_sockets do |subject, peer|
|
72
|
+
peer << payload
|
73
|
+
subject.read(payload.size).should eq payload
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
it "reads partial data" do
|
78
|
+
with_connected_sockets do |subject, peer|
|
79
|
+
peer << payload * 2
|
80
|
+
subject.readpartial(payload.size).should eq payload
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
it "writes data" do
|
85
|
+
with_connected_sockets do |subject, peer|
|
86
|
+
subject << payload
|
87
|
+
peer.read(payload.size).should eq payload
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Celluloid::IO::UDPSocket do
|
4
|
+
let(:payload) { 'ohai' }
|
5
|
+
subject do
|
6
|
+
Celluloid::IO::UDPSocket.new.tap do |sock|
|
7
|
+
sock.bind example_addr, example_port
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
after { subject.close }
|
12
|
+
|
13
|
+
context "inside Celluloid::IO" do
|
14
|
+
it "should be evented" do
|
15
|
+
within_io_actor { subject.evented? }.should be_true
|
16
|
+
end
|
17
|
+
|
18
|
+
it "sends and receives packets" do
|
19
|
+
subject.send payload, 0, example_addr, example_port
|
20
|
+
subject.recvfrom(payload.size).first.should == payload
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context "elsewhere in Ruby" do
|
25
|
+
it "should be blocking" do
|
26
|
+
subject.should_not be_evented
|
27
|
+
end
|
28
|
+
|
29
|
+
it "sends and receives packets" do
|
30
|
+
subject.send payload, 0, example_addr, example_port
|
31
|
+
subject.recvfrom(payload.size).first.should == payload
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,4 +1,48 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'bundler/setup'
|
3
3
|
require 'celluloid/io'
|
4
|
-
require 'celluloid/rspec'
|
4
|
+
require 'celluloid/rspec'
|
5
|
+
|
6
|
+
class ExampleActor
|
7
|
+
include Celluloid::IO
|
8
|
+
|
9
|
+
def wrap
|
10
|
+
yield
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
EXAMPLE_PORT = 10000 + rand(10000)
|
15
|
+
|
16
|
+
def example_addr; '127.0.0.1'; end
|
17
|
+
def example_port; EXAMPLE_PORT; end
|
18
|
+
|
19
|
+
def within_io_actor(&block)
|
20
|
+
actor = ExampleActor.new
|
21
|
+
actor.wrap(&block)
|
22
|
+
ensure
|
23
|
+
actor.terminate
|
24
|
+
end
|
25
|
+
|
26
|
+
def with_tcp_server
|
27
|
+
server = Celluloid::IO::TCPServer.new(example_addr, example_port)
|
28
|
+
begin
|
29
|
+
yield server
|
30
|
+
ensure
|
31
|
+
server.close
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def with_connected_sockets
|
36
|
+
with_tcp_server do |server|
|
37
|
+
# FIXME: client isn't actually a Celluloid::IO::TCPSocket yet
|
38
|
+
client = ::TCPSocket.new(example_addr, example_port)
|
39
|
+
peer = server.accept
|
40
|
+
|
41
|
+
begin
|
42
|
+
yield peer, client
|
43
|
+
ensure
|
44
|
+
client.close
|
45
|
+
peer.close
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: celluloid-io
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,33 +9,33 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-02-21 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: celluloid
|
16
|
-
requirement: &
|
16
|
+
requirement: &70287081256280 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
20
20
|
- !ruby/object:Gem::Version
|
21
|
-
version: 0.
|
21
|
+
version: 0.9.0
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70287081256280
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: nio4r
|
27
|
-
requirement: &
|
27
|
+
requirement: &70287081254940 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: 0.
|
32
|
+
version: 0.3.1
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70287081254940
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: rake
|
38
|
-
requirement: &
|
38
|
+
requirement: &70287081253700 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,18 +43,18 @@ dependencies:
|
|
43
43
|
version: '0'
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70287081253700
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: rspec
|
49
|
-
requirement: &
|
49
|
+
requirement: &70287081251980 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
|
-
- -
|
52
|
+
- - ! '>='
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
54
|
+
version: '0'
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *70287081251980
|
58
58
|
description: Evented IO for Celluloid actors
|
59
59
|
email:
|
60
60
|
- tony.arcieri@gmail.com
|
@@ -71,12 +71,21 @@ files:
|
|
71
71
|
- README.md
|
72
72
|
- Rakefile
|
73
73
|
- celluloid-io.gemspec
|
74
|
+
- examples/echo_server.rb
|
74
75
|
- lib/celluloid/io.rb
|
76
|
+
- lib/celluloid/io/common_methods.rb
|
75
77
|
- lib/celluloid/io/mailbox.rb
|
76
78
|
- lib/celluloid/io/reactor.rb
|
79
|
+
- lib/celluloid/io/tcp_server.rb
|
80
|
+
- lib/celluloid/io/tcp_socket.rb
|
81
|
+
- lib/celluloid/io/udp_socket.rb
|
77
82
|
- lib/celluloid/io/version.rb
|
83
|
+
- logo.png
|
78
84
|
- spec/celluloid/io/actor_spec.rb
|
79
85
|
- spec/celluloid/io/mailbox_spec.rb
|
86
|
+
- spec/celluloid/io/tcp_server_spec.rb
|
87
|
+
- spec/celluloid/io/tcp_socket_spec.rb
|
88
|
+
- spec/celluloid/io/udp_socket_spec.rb
|
80
89
|
- spec/spec_helper.rb
|
81
90
|
homepage: http://github.com/tarcieri/celluloid-io
|
82
91
|
licenses: []
|
@@ -106,5 +115,8 @@ summary: Celluloid::IO allows you to monitor multiple IO objects within a Cellul
|
|
106
115
|
test_files:
|
107
116
|
- spec/celluloid/io/actor_spec.rb
|
108
117
|
- spec/celluloid/io/mailbox_spec.rb
|
118
|
+
- spec/celluloid/io/tcp_server_spec.rb
|
119
|
+
- spec/celluloid/io/tcp_socket_spec.rb
|
120
|
+
- spec/celluloid/io/udp_socket_spec.rb
|
109
121
|
- spec/spec_helper.rb
|
110
122
|
has_rdoc:
|