celluloid-io 0.8.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
![Celluloid](https://github.com/tarcieri/celluloid-io/raw/master/logo.png)
|
2
2
|
=============
|
3
|
-
[![Build Status](
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
3
|
+
[![Build Status](https://secure.travis-ci.org/tarcieri/celluloid-io.png?branch=master)](http://travis-ci.org/tarcieri/celluloid-io)
|
4
|
+
[![Dependency Status](https://gemnasium.com/tarcieri/celluloid-io.png)](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:
|