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.
@@ -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
+
@@ -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,5 @@
1
+ module Sloth
2
+ module Reel
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -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
+