sloth-reel 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +23 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/Gemfile +5 -0
- data/README.adoc +65 -0
- data/README.ja.adoc +65 -0
- data/Rakefile +96 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/reel/rack/server.rb +127 -0
- data/lib/reel/server.rb +78 -0
- data/lib/reel/websocket.rb +132 -0
- data/lib/sloth/reel.rb +13 -0
- data/lib/sloth/reel/sinatra.rb +18 -0
- data/lib/sloth/reel/version.rb +5 -0
- data/sample/rack_server_hello_1.rb +74 -0
- data/sample/server_http_hello_1.rb +84 -0
- data/sample/server_http_ws_chat_1.rb +105 -0
- data/sample/server_http_ws_time_1.rb +94 -0
- data/sample/server_sinatra_hello_1.rb +32 -0
- data/sample/server_sinatra_hello_2.rb +36 -0
- data/sample/server_sinatra_hello_3.rb +45 -0
- data/sample/server_sinatra_hello_4.rb +44 -0
- data/sample/server_sinatra_ws_chat_1.rb +68 -0
- data/sample/server_sinatra_ws_time_1.rb +55 -0
- data/sample/server_sinatra_ws_time_2.rb +63 -0
- data/sloth-reel.gemspec +33 -0
- metadata +224 -0
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'websocket'
|
3
|
+
|
4
|
+
module Reel
|
5
|
+
class WebSocket
|
6
|
+
extend Forwardable
|
7
|
+
include ConnectionMixin
|
8
|
+
include RequestMixin
|
9
|
+
|
10
|
+
attr_reader :socket, :url
|
11
|
+
def_delegators :@socket, :addr, :peeraddr
|
12
|
+
|
13
|
+
READ_SIZE = 0x4000
|
14
|
+
@@sockets = []
|
15
|
+
@@conns = {}
|
16
|
+
@@actions = Hash.new { |h,k| h[k] = {} }
|
17
|
+
@@thread = nil
|
18
|
+
|
19
|
+
def initialize(info, connection)
|
20
|
+
@opened = false
|
21
|
+
@socket = connection.hijack_socket
|
22
|
+
@request_info = info
|
23
|
+
@url = @request_info.url
|
24
|
+
|
25
|
+
@handshake = ::WebSocket::Handshake::Server.new
|
26
|
+
@frame = ::WebSocket::Frame::Incoming::Server.new(:version => @handshake.version)
|
27
|
+
|
28
|
+
line = "#{@request_info.method} / HTTP/#{@request_info.version}\r\n"
|
29
|
+
@handshake << line
|
30
|
+
@request_info.headers.each do |k, v|
|
31
|
+
line = "#{k}: #{v}\r\n"
|
32
|
+
@handshake << line
|
33
|
+
end
|
34
|
+
@handshake << "\r\n"
|
35
|
+
|
36
|
+
if not @handshake.finished? or not @handshake.valid?
|
37
|
+
@socket.close
|
38
|
+
return
|
39
|
+
end
|
40
|
+
|
41
|
+
lines = @handshake.to_s
|
42
|
+
@socket.write lines
|
43
|
+
|
44
|
+
@@sockets.push( @socket )
|
45
|
+
@@conns[@socket] = self
|
46
|
+
|
47
|
+
@@thread ||= Thread.start do
|
48
|
+
loop do
|
49
|
+
readables, = ::IO.select( @@sockets, nil, nil, 1 )
|
50
|
+
while sock = readables&.shift
|
51
|
+
if sock.eof?
|
52
|
+
@@conns[sock]&.close
|
53
|
+
break
|
54
|
+
else
|
55
|
+
payload = sock.readpartial( READ_SIZE ) rescue nil
|
56
|
+
@@conns[sock]&.parse( payload ) if payload
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
def parse( payload )
|
65
|
+
@frame << payload
|
66
|
+
messages = []
|
67
|
+
while frame = @frame.next
|
68
|
+
if (frame.type == :close)
|
69
|
+
close
|
70
|
+
break
|
71
|
+
else
|
72
|
+
messages.push frame.to_s
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
conns = @@conns.values.map do |conn|
|
77
|
+
conn if conn.url == @url
|
78
|
+
end.compact
|
79
|
+
messages.each do |mesg|
|
80
|
+
@@actions[@url][:onmessage]&.call( mesg, self, conns ) rescue nil
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def send( mesg, type: :text )
|
85
|
+
frame = ::WebSocket::Frame::Outgoing::Server.new( :version => @handshake.version, :data => mesg, :type => type )
|
86
|
+
begin
|
87
|
+
@socket.write frame.to_s
|
88
|
+
@socket.flush
|
89
|
+
rescue
|
90
|
+
close
|
91
|
+
raise Reel::SocketError
|
92
|
+
end
|
93
|
+
end
|
94
|
+
alias write send
|
95
|
+
alias :<< send
|
96
|
+
|
97
|
+
def close
|
98
|
+
return if @socket.closed?
|
99
|
+
@@actions[@url][:onclose]&.call( self ) rescue nil
|
100
|
+
@@conns.delete( @socket )
|
101
|
+
@@sockets.delete( @socket )
|
102
|
+
@@sockets.compact!
|
103
|
+
@socket.close rescue nil
|
104
|
+
end
|
105
|
+
|
106
|
+
def closed?
|
107
|
+
@socket.closed?
|
108
|
+
end
|
109
|
+
|
110
|
+
def connections
|
111
|
+
@@conns.values
|
112
|
+
end
|
113
|
+
|
114
|
+
def on_open( &block )
|
115
|
+
@@actions[@url][:onopen] = block
|
116
|
+
if not @opened
|
117
|
+
@opened = true
|
118
|
+
block.call( self ) rescue nil
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def on_message( &block )
|
123
|
+
@@actions[@url][:onmessage] = block
|
124
|
+
end
|
125
|
+
|
126
|
+
def on_close( &block )
|
127
|
+
@@actions[@url][:onclose] = block
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
data/lib/sloth/reel.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require "sloth/reel/version"
|
2
|
+
require "celluloid/current"
|
3
|
+
require "celluloid/autostart"
|
4
|
+
require "celluloid/fsm"
|
5
|
+
require "reel/rack"
|
6
|
+
require "sinatra/base"
|
7
|
+
require "sloth/reel/sinatra"
|
8
|
+
|
9
|
+
module Sloth
|
10
|
+
module Reel
|
11
|
+
class Error < StandardError; end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "sinatra/base"
|
2
|
+
|
3
|
+
module Sloth
|
4
|
+
module Reel
|
5
|
+
module Sinatra
|
6
|
+
def websocket?
|
7
|
+
!!env['websocket']
|
8
|
+
end
|
9
|
+
|
10
|
+
def websocket
|
11
|
+
env['websocket']
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
defined?( ::Sinatra ) && Sinatra::Request.send( :include, ::Sloth::Reel::Sinatra )
|
18
|
+
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'sloth/reel'
|
2
|
+
|
3
|
+
class WebApp
|
4
|
+
def call( env )
|
5
|
+
r = nil
|
6
|
+
case env['REQUEST_METHOD']
|
7
|
+
when 'GET'
|
8
|
+
case env['PATH_INFO']
|
9
|
+
when '/'
|
10
|
+
get_index
|
11
|
+
when '/heavy'
|
12
|
+
get_heavy
|
13
|
+
end
|
14
|
+
|
15
|
+
when 'POST'
|
16
|
+
case env['PATH_INFO']
|
17
|
+
when '/'
|
18
|
+
post_index
|
19
|
+
end
|
20
|
+
|
21
|
+
else
|
22
|
+
not_found
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def not_found
|
28
|
+
[
|
29
|
+
404,
|
30
|
+
{'Content-Type' => 'text/html'},
|
31
|
+
["<html><body><h1> 404: Not found: #{env['REQUEST_METHOD']} #{env['PATH_INFO']} </h1> </body></html>"]
|
32
|
+
]
|
33
|
+
end
|
34
|
+
|
35
|
+
def get_index
|
36
|
+
[
|
37
|
+
200,
|
38
|
+
{'Content-Type' => 'text/html'},
|
39
|
+
['<html><body><h1> GET </h1><a href="/"> Hello (GET) </a> <form method="POST"><input type="submit" value="Howdy (POST)" /></form></body></html>']
|
40
|
+
]
|
41
|
+
end
|
42
|
+
|
43
|
+
def post_index
|
44
|
+
[
|
45
|
+
200,
|
46
|
+
{'Content-Type' => 'text/html'},
|
47
|
+
['<html><body><h1> POST </h1><a href="/"> Hello (GET) </a> <form method="POST"><input type="submit" value="Howdy (POST)" /></form></body></html>']
|
48
|
+
]
|
49
|
+
end
|
50
|
+
|
51
|
+
def get_heavy
|
52
|
+
sec = 10
|
53
|
+
tm1 = Time.now.to_s
|
54
|
+
Kernel.sleep sec
|
55
|
+
tm2 = Time.now.to_s
|
56
|
+
|
57
|
+
[
|
58
|
+
200,
|
59
|
+
{'Content-Type' => 'text/html'},
|
60
|
+
["<html><body><p> #{sec}: interval <br/> #{tm1}: sleep <br/> #{tm2}: wakeup </p></body></html>"]
|
61
|
+
]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
ENV['RACK_ENV'] ||= 'production'
|
66
|
+
options = {Host: "0.0.0.0", Port: 3000, max_connection: 16}
|
67
|
+
Reel::Rack::Server.new( WebApp.new, options )
|
68
|
+
|
69
|
+
Signal.trap(:INT) do
|
70
|
+
exit
|
71
|
+
end
|
72
|
+
|
73
|
+
sleep
|
74
|
+
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'sloth/reel'
|
2
|
+
|
3
|
+
class WebServer < Reel::Server::HTTP
|
4
|
+
include Celluloid::Internals::Logger
|
5
|
+
|
6
|
+
def initialize( options )
|
7
|
+
host = options[:Host] || "0.0.0.0"
|
8
|
+
port = options[:Port] || 3000
|
9
|
+
opts = {}
|
10
|
+
opts[:max_connection] = options[:max_connection] || 16
|
11
|
+
|
12
|
+
info "Reel::Server starting on #{host}:#{port}"
|
13
|
+
super( host, port, opts, &method(:on_connection) )
|
14
|
+
end
|
15
|
+
|
16
|
+
def on_connection( connection )
|
17
|
+
while request = connection.request
|
18
|
+
route_request connection, request
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def route_request( connection, request )
|
23
|
+
case request.method
|
24
|
+
when "GET"
|
25
|
+
case request.url
|
26
|
+
when "/"
|
27
|
+
get_index( connection )
|
28
|
+
when "/heavy"
|
29
|
+
get_heavy( connection )
|
30
|
+
end
|
31
|
+
|
32
|
+
when "POST"
|
33
|
+
case request.url
|
34
|
+
when "/"
|
35
|
+
post_index( connection )
|
36
|
+
end
|
37
|
+
|
38
|
+
else
|
39
|
+
not_found( connection )
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def not_found( connection )
|
45
|
+
info "404 Not Found: #{request.method} #{request.path}"
|
46
|
+
connection.respond :not_found, "Not found"
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_index( connection )
|
50
|
+
info "200 OK: GET /"
|
51
|
+
connection.respond :ok, <<-HTML
|
52
|
+
<html><body><h1> GET </h1><a href="/"> Hello (GET) </a> <form method="POST"><input type="submit" value="Howdy (POST)" /></form></body></html>
|
53
|
+
HTML
|
54
|
+
end
|
55
|
+
|
56
|
+
def post_index( connection )
|
57
|
+
info "200 OK: POST /"
|
58
|
+
connection.respond :ok, <<-HTML
|
59
|
+
<html><body><h1> POST </h1><a href="/"> Hello (GET) </a> <form method="POST"><input type="submit" value="Howdy (POST)" /></form></body></html>
|
60
|
+
HTML
|
61
|
+
end
|
62
|
+
|
63
|
+
def get_heavy( connection )
|
64
|
+
info "200 OK: POST /"
|
65
|
+
|
66
|
+
sec = 10
|
67
|
+
tm1 = Time.now.to_s
|
68
|
+
Kernel.sleep sec
|
69
|
+
tm2 = Time.now.to_s
|
70
|
+
|
71
|
+
connection.respond :ok, <<-HTML
|
72
|
+
<html><body><p> #{sec}: interval <br/> #{tm1}: sleep <br/> #{tm2}: wakeup </p></body></html>
|
73
|
+
HTML
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
options = {Host: "0.0.0.0", Port: 3000, max_connection: 16}
|
78
|
+
WebServer.new( options )
|
79
|
+
|
80
|
+
Signal.trap(:INT) do
|
81
|
+
exit
|
82
|
+
end
|
83
|
+
|
84
|
+
sleep
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'sloth/reel'
|
2
|
+
|
3
|
+
class WebServer < Reel::Server::HTTP
|
4
|
+
include Celluloid::Internals::Logger
|
5
|
+
|
6
|
+
def initialize( options )
|
7
|
+
host = options[:Host] || "0.0.0.0"
|
8
|
+
port = options[:Port] || 3000
|
9
|
+
opts = {}
|
10
|
+
opts[:max_connection] = options[:max_connection] || 16
|
11
|
+
|
12
|
+
info "Reel::Server starting on #{host}:#{port}"
|
13
|
+
super( host, port, opts, &method(:on_connection) )
|
14
|
+
end
|
15
|
+
|
16
|
+
def on_connection(connection)
|
17
|
+
while request = connection.request
|
18
|
+
if request.websocket?
|
19
|
+
connection.detach
|
20
|
+
route_websocket( request )
|
21
|
+
else
|
22
|
+
route_request( connection, request )
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def route_websocket( request )
|
28
|
+
case request.url
|
29
|
+
when "/chat"
|
30
|
+
route_chat( request )
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def route_chat(request)
|
35
|
+
if request.websocket?
|
36
|
+
ws = request.websocket
|
37
|
+
ws << Time.now.to_s
|
38
|
+
ws.on_message do |mesg, sender, conns|
|
39
|
+
conns.each do |conn|
|
40
|
+
conn << mesg
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def route_request(connection, request)
|
47
|
+
case request.url
|
48
|
+
when "/"
|
49
|
+
render_index(connection)
|
50
|
+
else
|
51
|
+
info "404 Not Found: #{request.path}"
|
52
|
+
connection.respond :not_found, "Not found"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def render_index(connection)
|
57
|
+
info "200 OK: /"
|
58
|
+
connection.respond :ok, <<-HTML
|
59
|
+
<!doctype html>
|
60
|
+
<html>
|
61
|
+
<body>
|
62
|
+
<h1>WebSocket Chat</h1>
|
63
|
+
<form id="form"> <input type="text" id="input" value=""></input> </form>
|
64
|
+
<div id="msgs"></div>
|
65
|
+
</body>
|
66
|
+
|
67
|
+
<script type="text/javascript">
|
68
|
+
window.onload = function(){
|
69
|
+
(function(){
|
70
|
+
var show = function(el){
|
71
|
+
return function(str){ el.innerHTML = str + '<br />' + el.innerHTML }
|
72
|
+
}(document.getElementById('msgs'))
|
73
|
+
|
74
|
+
ws = new WebSocket('ws://' + window.location.host + '/chat')
|
75
|
+
ws.onopen = function() { show('[opened]') }
|
76
|
+
ws.onclose = function() { show('[closed]') }
|
77
|
+
ws.onmessage = function(mesg) { show(mesg.data) }
|
78
|
+
|
79
|
+
var sender = function(fm) {
|
80
|
+
var input = document.getElementById('input')
|
81
|
+
input.onclick = function(){
|
82
|
+
input.value = ""
|
83
|
+
}
|
84
|
+
fm.onsubmit = function(){
|
85
|
+
ws.send( input.value )
|
86
|
+
input.value = ""
|
87
|
+
return false
|
88
|
+
}
|
89
|
+
}(document.getElementById('form'))
|
90
|
+
})()
|
91
|
+
}
|
92
|
+
</script>
|
93
|
+
</html>
|
94
|
+
HTML
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
options = {Host: "0.0.0.0", Port: 3000, max_connection: 16}
|
99
|
+
WebServer.new( options )
|
100
|
+
|
101
|
+
Signal.trap(:INT) do
|
102
|
+
exit
|
103
|
+
end
|
104
|
+
|
105
|
+
sleep
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'sloth/reel'
|
2
|
+
|
3
|
+
class WebServer < Reel::Server::HTTP
|
4
|
+
include Celluloid::Internals::Logger
|
5
|
+
|
6
|
+
def initialize( options )
|
7
|
+
host = options[:Host] || "0.0.0.0"
|
8
|
+
port = options[:Port] || 3000
|
9
|
+
opts = {}
|
10
|
+
opts[:max_connection] = options[:max_connection] || 16
|
11
|
+
|
12
|
+
info "Reel::Server starting on #{host}:#{port}"
|
13
|
+
super( host, port, opts, &method(:on_connection) )
|
14
|
+
end
|
15
|
+
|
16
|
+
def on_connection(connection)
|
17
|
+
while request = connection.request
|
18
|
+
if request.websocket?
|
19
|
+
connection.detach
|
20
|
+
route_websocket( request )
|
21
|
+
else
|
22
|
+
route_request( connection, request )
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def route_websocket( request )
|
28
|
+
case request.url
|
29
|
+
when "/timeinfo"
|
30
|
+
route_timeinfo( request )
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def route_timeinfo(request)
|
35
|
+
if request.websocket?
|
36
|
+
Thread.start do
|
37
|
+
begin
|
38
|
+
websocket = request.websocket
|
39
|
+
while not websocket.closed?
|
40
|
+
str = Time.now.to_s
|
41
|
+
websocket.write str
|
42
|
+
sleep 1
|
43
|
+
end
|
44
|
+
rescue Reel::SocketError
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def route_request(connection, request)
|
51
|
+
case request.url
|
52
|
+
when "/"
|
53
|
+
render_index(connection)
|
54
|
+
else
|
55
|
+
info "404 Not Found: #{request.path}"
|
56
|
+
connection.respond :not_found, "Not found"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def render_index(connection)
|
61
|
+
info "200 OK: /"
|
62
|
+
connection.respond :ok, <<-HTML
|
63
|
+
<!doctype html>
|
64
|
+
<html lang="en">
|
65
|
+
<head>
|
66
|
+
<meta charset="utf-8">
|
67
|
+
<title>Reel WebSockets Time Server Example</title>
|
68
|
+
</head>
|
69
|
+
<script>
|
70
|
+
var ws = new WebSocket('ws://' + window.location.host + '/timeinfo');
|
71
|
+
ws.onmessage = function(mesg){
|
72
|
+
document.getElementById('current-time').innerHTML = mesg.data;
|
73
|
+
}
|
74
|
+
</script>
|
75
|
+
<body>
|
76
|
+
<div id="content">
|
77
|
+
<h1>Reel WebSockets Time Server Example</h1>
|
78
|
+
<h2>The time is now: <span id="current-time">...</span></h2>
|
79
|
+
</div>
|
80
|
+
</body>
|
81
|
+
</html>
|
82
|
+
HTML
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
options = {Host: "0.0.0.0", Port: 3000, max_connection: 16}
|
87
|
+
WebServer.new( options )
|
88
|
+
|
89
|
+
Signal.trap(:INT) do
|
90
|
+
exit
|
91
|
+
end
|
92
|
+
|
93
|
+
sleep
|
94
|
+
|