async-io 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,159 @@
1
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'socket'
22
+ require_relative 'generic'
23
+
24
+ module Async
25
+ module IO
26
+ class BasicSocket < Generic
27
+ wraps ::BasicSocket, :setsockopt, :connect_address, :local_address, :remote_address, :do_not_reverse_lookup, :do_not_reverse_lookup=, :shutdown, :getsockopt, :getsockname, :getpeername, :getpeereid
28
+
29
+ wrap_blocking_method :recv, :recv_nonblock
30
+ wrap_blocking_method :recvmsg, :recvmsg_nonblock
31
+
32
+ wrap_blocking_method :recvfrom, :recvfrom_nonblock
33
+
34
+ wrap_blocking_method :sendmsg, :sendmsg_nonblock
35
+ wrap_blocking_method :send, :sendmsg_nonblock, invert: false
36
+ end
37
+
38
+ module ServerSocket
39
+ def accept
40
+ peer, address = async_send(:accept_nonblock)
41
+
42
+ if block_given?
43
+ wrapper = Socket.new(peer, self.reactor)
44
+
45
+ begin
46
+ yield wrapper, address
47
+ ensure
48
+ wrapper.close
49
+ end
50
+ else
51
+ return Socket.new(peer, self.reactor), address
52
+ end
53
+ end
54
+
55
+ def accept_each(task: Task.current)
56
+ task.annotate "accepting connections #{self.local_address.inspect}"
57
+
58
+ while true
59
+ task.async(*self.accept) do |task, io, address|
60
+ task.annotate "incoming connection #{address}"
61
+
62
+ begin
63
+ yield io, address
64
+ ensure
65
+ io.close
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ class Socket < BasicSocket
73
+ wraps ::Socket, :bind, :ipv6only!, :listen
74
+
75
+ include ::Socket::Constants
76
+
77
+ include ServerSocket
78
+
79
+ def connect(*args)
80
+ begin
81
+ async_send(:connect_nonblock, *args)
82
+ rescue Errno::EISCONN
83
+ # We are now connected.
84
+ end
85
+ end
86
+
87
+ # Establish a connection to a given `remote_address`.
88
+ # @example
89
+ # socket = Async::IO::Socket.connect(Async::IO::Address.tcp("8.8.8.8", 53))
90
+ # @param remote_address [Addrinfo] The remote address to connect to.
91
+ # @param local_address [Addrinfo] The local address to bind to before connecting.
92
+ # @option protcol [Integer] The socket protocol to use.
93
+ def self.connect(remote_address, local_address = nil, protocol: 0, task: Task.current)
94
+ task.annotate "connecting to #{remote_address.inspect}"
95
+
96
+ socket = ::Socket.new(remote_address.afamily, remote_address.socktype, protocol)
97
+
98
+ if local_address
99
+ socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEADDR, true)
100
+ socket.bind(local_address.to_sockaddr) if local_address
101
+ end
102
+
103
+ wrapper = self.new(socket, task.reactor)
104
+ wrapper.connect(remote_address.to_sockaddr)
105
+
106
+ task.annotate "connected to #{remote_address.inspect}"
107
+
108
+ if block_given?
109
+ begin
110
+ return yield(wrapper)
111
+ ensure
112
+ wrapper.close
113
+ end
114
+ else
115
+ return wrapper
116
+ end
117
+ end
118
+
119
+ # Bind to a local address.
120
+ # @example
121
+ # socket = Async::IO::Socket.bind(Async::IO::Address.tcp("0.0.0.0", 9090))
122
+ # @param local_address [Address] The local address to bind to.
123
+ # @option protcol [Integer] The socket protocol to use.
124
+ def self.bind(local_address, protocol: 0, task: Task.current, &block)
125
+ task.annotate "binding to #{local_address.inspect}"
126
+
127
+ socket = ::Socket.new(local_address.afamily, local_address.socktype, protocol)
128
+
129
+ socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEADDR, true)
130
+ socket.bind(local_address.to_sockaddr)
131
+
132
+ wrapper = self.new(socket, task.reactor)
133
+
134
+ if block_given?
135
+ begin
136
+ return yield(wrapper, task)
137
+ ensure
138
+ wrapper.close
139
+ end
140
+ else
141
+ return wrapper
142
+ end
143
+ end
144
+
145
+ # Bind to a local address and accept connections in a loop.
146
+ def self.accept(*args, backlog: SOMAXCONN, &block)
147
+ bind(*args) do |server, task|
148
+ server.listen(backlog) if backlog
149
+
150
+ server.accept_each(task: task, &block)
151
+ end
152
+ end
153
+ end
154
+
155
+ class IPSocket < BasicSocket
156
+ wraps ::IPSocket, :addr, :peeraddr
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,37 @@
1
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'socket'
22
+
23
+ module Async
24
+ module IO
25
+ # Asynchronous TCP socket wrapper.
26
+ class TCPSocket < IPSocket
27
+ wraps ::TCPSocket
28
+ end
29
+
30
+ # Asynchronous TCP server wrappper.
31
+ class TCPServer < TCPSocket
32
+ wraps ::TCPServer, :listen
33
+
34
+ include ServerSocket
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,36 @@
1
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'socket'
22
+
23
+ module Async
24
+ module IO
25
+ # Asynchronous UDP socket wrapper.
26
+ class UDPSocket < IPSocket
27
+ wraps ::UDPSocket, :bind
28
+
29
+ # We pass `send` through directly, but in theory it might block. Internally, it uses sendto.
30
+ def_delegators :@io, :send
31
+
32
+ # This function is so fucked. Why does `UDPSocket#recvfrom` return the remote address as an array, but `Socket#recfrom` return it as an `Addrinfo`? You should prefer `recvmsg`.
33
+ wrap_blocking_method :recvfrom, :recvfrom_nonblock
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,52 @@
1
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'socket'
22
+
23
+ module Async
24
+ module IO
25
+ class UNIXSocket < BasicSocket
26
+ wraps ::UNIXSocket, :path, :addr, :peeraddr
27
+
28
+ # TODO Currently unimplemented.
29
+ # , :send_io, :recv_io
30
+ end
31
+
32
+ class UNIXServer < UNIXSocket
33
+ wraps ::UNIXServer
34
+
35
+ def accept
36
+ peer = async_send(:accept_nonblock)
37
+
38
+ if block_given?
39
+ wrapper = UNIXSocket.new(peer, self.reactor)
40
+
41
+ begin
42
+ yield wrapper
43
+ ensure
44
+ wrapper.close
45
+ end
46
+ else
47
+ return UNIXSocket.new(peer, self.reactor)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,25 @@
1
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ module Async
22
+ module IO
23
+ VERSION = "0.1.0"
24
+ end
25
+ end
@@ -0,0 +1,62 @@
1
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative '../socket'
22
+
23
+ module Async
24
+ module IO
25
+ module Wrap
26
+ module TCPServer
27
+ def self.new(*args)
28
+ return ::TCPServer.new(*args) unless Task.current?
29
+
30
+ case args.size
31
+ when 2
32
+ local_address = Async::IO::Address.tcp(*args)
33
+ when 1
34
+ local_address = Async::IO::Address.tcp("0.0.0.0", *args)
35
+ else
36
+ raise ArgumentError, "TCPServer.new([hostname], port)"
37
+ end
38
+
39
+ return Async::IO::Socket.bind(local_address)
40
+ end
41
+ end
42
+
43
+ module TCPSocket
44
+ def self.new(remote_host, remote_port, local_host=nil, local_port=nil)
45
+ return ::TCPSocket.new(remote_host, remote_port, local_host, local_port) unless Task.current?
46
+
47
+ remote_address = Async::IO::Address.tcp(remote_host, remote_port)
48
+
49
+ if local_host && local_port
50
+ local_address = Async::IO::Address.tcp(local_host, local_port)
51
+ end
52
+
53
+ return Async::IO::Socket.connect(remote_address, local_address)
54
+ end
55
+
56
+ def self.open(*args)
57
+ self.new(*args)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,45 @@
1
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'async/io/address'
22
+
23
+ RSpec.describe Async::IO::Address do
24
+ include_context Async::RSpec::Reactor
25
+
26
+ describe Async::IO::Address.new([:tcp, '0.0.0.0', 1234]) do
27
+ it "should be a tcp binding" do
28
+ expect(subject.socktype).to be == ::Socket::SOCK_STREAM
29
+ end
30
+
31
+ it "should generate valid address" do
32
+ expect(subject).to be == Addrinfo.tcp('0.0.0.0', 1234)
33
+ end
34
+ end
35
+
36
+ describe Async::IO::Address.new(TCPServer.new('0.0.0.0', 1234)) do
37
+ it "should be a tcp binding" do
38
+ expect(subject.socktype).to be == ::Socket::SOCK_STREAM
39
+ end
40
+
41
+ it "should generate valid address" do
42
+ expect(subject).to be == Addrinfo.tcp('0.0.0.0', 1234)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,72 @@
1
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'async/io'
22
+
23
+ RSpec.describe "echo client/server" do
24
+ include_context Async::RSpec::Reactor
25
+
26
+ let(:server_address) {Async::IO::Address.new([:tcp, '0.0.0.0', 9000])}
27
+
28
+ def echo_server(server_address)
29
+ Async::Reactor.run do |task|
30
+ # This is a synchronous block within the current task:
31
+ Async::IO::Socket.accept(server_address) do |client|
32
+ # This is an asynchronous block within the current reactor:
33
+ data = client.read(512)
34
+
35
+ # This produces out-of-order responses.
36
+ task.sleep(rand * 0.01)
37
+
38
+ client.write(data)
39
+ end
40
+ end
41
+ end
42
+
43
+ def echo_client(server_address, data, responses)
44
+ Async::Reactor.run do |task|
45
+ Async::IO::Socket.connect(server_address) do |peer|
46
+ result = peer.write(data)
47
+
48
+ message = peer.read(512)
49
+
50
+ responses << message
51
+ end
52
+ end
53
+ end
54
+
55
+ let(:repeats) {10}
56
+
57
+ it "should echo several messages" do
58
+ server = echo_server(server_address)
59
+ responses = []
60
+
61
+ tasks = repeats.times.collect do |i|
62
+ echo_client(server_address, "Hello World #{i}", responses)
63
+ end
64
+
65
+ # task.reactor.print_hierarchy
66
+
67
+ tasks.each(&:wait)
68
+ server.stop
69
+
70
+ expect(responses.count).to be repeats
71
+ end
72
+ end