cool.io-websocket 0.1.4
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/ChangeLog +23 -0
- data/README.rdoc +144 -0
- data/Rakefile +24 -0
- data/VERSION +1 -0
- data/examples/README.md +41 -0
- data/examples/echo +18 -0
- data/examples/echo.rb +31 -0
- data/examples/public/echo.css +11 -0
- data/examples/public/echo.html +62 -0
- data/examples/public/js/FABridge.js +604 -0
- data/examples/public/js/WebSocketMain.swf +0 -0
- data/examples/public/js/jquery.min.js +154 -0
- data/examples/public/js/json2.js +482 -0
- data/examples/public/js/swfobject.js +4 -0
- data/examples/public/js/web_socket.js +371 -0
- data/examples/public/shoutchat.css +63 -0
- data/examples/public/shoutchat.html +148 -0
- data/examples/rpc +26 -0
- data/examples/rpc.rb +47 -0
- data/examples/shoutchat +18 -0
- data/examples/shoutchat.rb +79 -0
- data/examples/views/rpc.erb +67 -0
- data/lib/cool.io-websocket.rb +1 -0
- data/lib/coolio/websocket.rb +243 -0
- data/lib/coolio/websocket/spec.rb +103 -0
- metadata +121 -0
data/examples/rpc
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
fork {
|
|
4
|
+
load 'rpc.rb'
|
|
5
|
+
exit 0
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
require 'rubygems'
|
|
9
|
+
require 'sinatra'
|
|
10
|
+
require 'msgpack/rpc'
|
|
11
|
+
|
|
12
|
+
rpc_port = 18800
|
|
13
|
+
$ws = MessagePack::RPC::Client.new('127.0.0.1', rpc_port)
|
|
14
|
+
|
|
15
|
+
get '/' do
|
|
16
|
+
erb :rpc
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
post '/push' do
|
|
20
|
+
data = {'Hello'=>'World!', 'data' => params}
|
|
21
|
+
$ws.call(:push_data, data)
|
|
22
|
+
redirect '/', 303
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
set :port, 8080
|
|
26
|
+
|
data/examples/rpc.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# RPC push
|
|
2
|
+
# This program receives messages.
|
|
3
|
+
# See ./rpc file which sends messages to this program.
|
|
4
|
+
|
|
5
|
+
require 'rubygems'
|
|
6
|
+
require 'cool.io-websocket'
|
|
7
|
+
require 'msgpack/rpc'
|
|
8
|
+
require 'json'
|
|
9
|
+
|
|
10
|
+
$sockets = {}
|
|
11
|
+
|
|
12
|
+
class MyConnection < Cool.io::WebSocket
|
|
13
|
+
def on_open
|
|
14
|
+
puts "WebSocket opened from '#{peeraddr[2]}': request=#{request.inspect}"
|
|
15
|
+
$sockets[self] = self
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def on_close
|
|
19
|
+
puts "WebSocket closed"
|
|
20
|
+
$sockets.delete(self)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class RPCServer
|
|
25
|
+
def push_data(data)
|
|
26
|
+
$sockets.each_key {|sock|
|
|
27
|
+
sock.send_message(data.to_json)
|
|
28
|
+
}
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
host = '0.0.0.0'
|
|
34
|
+
port = ARGV[0] || 8081
|
|
35
|
+
|
|
36
|
+
rpc_port = 18800
|
|
37
|
+
|
|
38
|
+
loop = Cool.io::Loop.default
|
|
39
|
+
|
|
40
|
+
ws = Cool.io::WebSocketServer.new(host, port, MyConnection)
|
|
41
|
+
ws.attach(loop)
|
|
42
|
+
|
|
43
|
+
rpc = MessagePack::RPC::Server.new(loop)
|
|
44
|
+
rpc.listen('127.0.0.1', rpc_port, RPCServer.new)
|
|
45
|
+
|
|
46
|
+
loop.run
|
|
47
|
+
|
data/examples/shoutchat
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
fork {
|
|
4
|
+
load 'shoutchat.rb'
|
|
5
|
+
exit 0
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
require 'webrick'
|
|
9
|
+
|
|
10
|
+
server = WEBrick::HTTPServer.new({
|
|
11
|
+
:Port => 8080,
|
|
12
|
+
:BindAddress => '127.0.0.1',
|
|
13
|
+
:DocumentRoot => File.dirname(__FILE__)+'/public'})
|
|
14
|
+
|
|
15
|
+
trap('INT') { server.shutdown }
|
|
16
|
+
|
|
17
|
+
server.start
|
|
18
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Publisher/Subscriber-style message routing
|
|
2
|
+
|
|
3
|
+
require 'rubygems'
|
|
4
|
+
require 'cool.io-websocket'
|
|
5
|
+
require 'json'
|
|
6
|
+
|
|
7
|
+
class PubSub
|
|
8
|
+
def initialize
|
|
9
|
+
@subscriber = {}
|
|
10
|
+
@seqid = 0
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def subscribe(&block)
|
|
14
|
+
sid = @seqid += 1
|
|
15
|
+
@subscriber[sid] = block
|
|
16
|
+
return sid
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def unsubscribe(key)
|
|
20
|
+
@subscriber.delete(key)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def publish(data)
|
|
24
|
+
@subscriber.reject! {|sid,block|
|
|
25
|
+
begin
|
|
26
|
+
block.call(data)
|
|
27
|
+
false
|
|
28
|
+
rescue
|
|
29
|
+
true
|
|
30
|
+
end
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def size
|
|
35
|
+
@subscriber.size
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
$pubsub = PubSub.new
|
|
40
|
+
$record = []
|
|
41
|
+
|
|
42
|
+
class ShoutChatConnection < Cool.io::WebSocket
|
|
43
|
+
def on_open
|
|
44
|
+
@host = peeraddr[2]
|
|
45
|
+
puts "connection opened: <#{@host}>"
|
|
46
|
+
|
|
47
|
+
@sid = $pubsub.subscribe {|data|
|
|
48
|
+
send_message data
|
|
49
|
+
}
|
|
50
|
+
$pubsub.publish(["count", $pubsub.size].to_json)
|
|
51
|
+
$record.each {|data| send_message data }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def on_message(data)
|
|
55
|
+
puts "broadcasting: <#{@host}> '#{data}'"
|
|
56
|
+
|
|
57
|
+
$pubsub.publish(data)
|
|
58
|
+
$record.push(data)
|
|
59
|
+
$record.shift while $record.size > 20
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def on_close
|
|
63
|
+
puts "connection closed: <#{@host}>"
|
|
64
|
+
|
|
65
|
+
$pubsub.unsubscribe(@sid)
|
|
66
|
+
$pubsub.publish(["count", $pubsub.size].to_json)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
host = '0.0.0.0'
|
|
71
|
+
port = ARGV[0] || 8081
|
|
72
|
+
|
|
73
|
+
server = Cool.io::WebSocketServer.new(host, port, ShoutChatConnection)
|
|
74
|
+
server.attach(Cool.io::Loop.default)
|
|
75
|
+
|
|
76
|
+
puts "start on #{host}:#{port}"
|
|
77
|
+
|
|
78
|
+
Cool.io::Loop.default.run
|
|
79
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta content="text/css" http-equiv="content-style-type" />
|
|
6
|
+
<meta content="text/javascript" http-equiv="content-script-type" />
|
|
7
|
+
|
|
8
|
+
<script type="text/javascript" src='js/jquery.min.js'></script>
|
|
9
|
+
<script type="text/javascript" src='js/swfobject.js'></script>
|
|
10
|
+
<script type="text/javascript" src='js/FABridge.js'></script>
|
|
11
|
+
<script type="text/javascript" src='js/web_socket.js'></script>
|
|
12
|
+
<script type="text/javascript" src='js/json2.js'></script>
|
|
13
|
+
<link rel="stylesheet" type="text/css" href="echo.css" />
|
|
14
|
+
|
|
15
|
+
<title>Cool.io-WebSocket Demo: Echo server</title>
|
|
16
|
+
|
|
17
|
+
<script>
|
|
18
|
+
WS_URL = "ws://localhost:8081";
|
|
19
|
+
WEB_SOCKET_SWF_LOCATION = "js/WebSocketMain.swf";
|
|
20
|
+
|
|
21
|
+
var global = this;
|
|
22
|
+
|
|
23
|
+
$(document).ready(function(){
|
|
24
|
+
|
|
25
|
+
function debug(message) {
|
|
26
|
+
html = "<p><span class='time'>"+new Date()+"</span>"+message+"</p>"
|
|
27
|
+
$("#debug").append(html);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
debug("connecting to "+WS_URL+"...");
|
|
31
|
+
ws = new WebSocket(WS_URL);
|
|
32
|
+
|
|
33
|
+
ws.onopen = function() {
|
|
34
|
+
debug("connected.");
|
|
35
|
+
|
|
36
|
+
text = "client: hello";
|
|
37
|
+
ws.send(text);
|
|
38
|
+
|
|
39
|
+
debug("message sent: "+text);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
ws.onclose = function() {
|
|
43
|
+
debug("disconnected...");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
ws.onerror = function(msg) {
|
|
47
|
+
debug("failed to connect:"+msg);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
ws.onmessage = function(event) {
|
|
51
|
+
debug("message received: "+event.data);
|
|
52
|
+
$("#message").append("<p>"+event.data+"</p>");
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
</script>
|
|
56
|
+
</head>
|
|
57
|
+
<body>
|
|
58
|
+
<div>
|
|
59
|
+
<form action="/push" target="_blank" method="post">
|
|
60
|
+
<input type="text" name="text" id="text" value="text..." />
|
|
61
|
+
<input type="submit" value="submit" />
|
|
62
|
+
</form>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div id="message"></div>
|
|
66
|
+
<div id="debug"></div>
|
|
67
|
+
</body>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require File.dirname(__FILE__)+'/coolio/websocket'
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
require 'cool.io'
|
|
2
|
+
require File.dirname(__FILE__)+'/websocket/spec'
|
|
3
|
+
require 'thin_parser'
|
|
4
|
+
|
|
5
|
+
module Coolio
|
|
6
|
+
class WebSocketServer < TCPServer
|
|
7
|
+
def initialize(host, port = nil, klass = WebSocket, *args, &block)
|
|
8
|
+
super
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# WebSocket spec:
|
|
13
|
+
# http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
|
|
14
|
+
|
|
15
|
+
class WebSocket < TCPSocket
|
|
16
|
+
def on_open
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def on_message(data)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def on_error(reason)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
attr_reader :request
|
|
26
|
+
|
|
27
|
+
def send_message(data)
|
|
28
|
+
if HAVE_ENCODING
|
|
29
|
+
frame = FRAME_START + data.force_encoding('UTF-8') + FRAME_END
|
|
30
|
+
else
|
|
31
|
+
frame = FRAME_START + data + FRAME_END
|
|
32
|
+
end
|
|
33
|
+
write frame
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
if "".respond_to?(:force_encoding)
|
|
37
|
+
HAVE_ENCODING = true
|
|
38
|
+
FRAME_START = "\x00".force_encoding('UTF-8')
|
|
39
|
+
FRAME_END = "\xFF".force_encoding('UTF-8')
|
|
40
|
+
else
|
|
41
|
+
HAVE_ENCODING = false
|
|
42
|
+
FRAME_START = "\x00"
|
|
43
|
+
FRAME_END = "\xFF"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
#HTTP11_PRASER = Mongrel::HttpParser
|
|
47
|
+
HTTP11_PRASER = Thin::HttpParser
|
|
48
|
+
|
|
49
|
+
# Thin::HttpParser tries to call request['rack.input'].write(body)
|
|
50
|
+
class DummyIO
|
|
51
|
+
KEY = 'rack.input'
|
|
52
|
+
def write(data) end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def initialize(socket)
|
|
56
|
+
super
|
|
57
|
+
@state = :process_handshake
|
|
58
|
+
@data = ::IO::Buffer.new
|
|
59
|
+
@http11 = HTTP11_PRASER.new
|
|
60
|
+
@http11_nbytes = 0
|
|
61
|
+
@request = {DummyIO::KEY => DummyIO.new}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def on_readable
|
|
65
|
+
super
|
|
66
|
+
rescue
|
|
67
|
+
close
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def on_read(data)
|
|
71
|
+
@data << data
|
|
72
|
+
dispatch
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
protected
|
|
76
|
+
|
|
77
|
+
def dispatch
|
|
78
|
+
while __send__(@state)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def process_handshake
|
|
83
|
+
return false if @data.empty?
|
|
84
|
+
|
|
85
|
+
data = @data.to_str
|
|
86
|
+
|
|
87
|
+
if data == "<policy-file-request/>\0"
|
|
88
|
+
write_policy_file
|
|
89
|
+
@state = :invalid_state
|
|
90
|
+
return false
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
begin
|
|
94
|
+
@http11_nbytes = @http11.execute(@request, data, @http11_nbytes)
|
|
95
|
+
rescue
|
|
96
|
+
on_error "invalid HTTP header, parsing fails"
|
|
97
|
+
@state = :invalid_state
|
|
98
|
+
close
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
return false unless @http11.finished?
|
|
102
|
+
|
|
103
|
+
@data.read(@http11_nbytes-1)
|
|
104
|
+
remove_instance_variable(:@http11)
|
|
105
|
+
remove_instance_variable(:@http11_nbytes)
|
|
106
|
+
|
|
107
|
+
@request.delete(DummyIO::KEY)
|
|
108
|
+
|
|
109
|
+
unless @request["REQUEST_METHOD"] == "GET"
|
|
110
|
+
raise RuntimeError, "Request method must be GET"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
unless @request['HTTP_CONNECTION'] == 'Upgrade' and @request['HTTP_UPGRADE'] == 'WebSocket'
|
|
114
|
+
raise RequestError, "Connection and Upgrade headers required"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
@state = :process_frame_header
|
|
118
|
+
|
|
119
|
+
version = @request['HTTP_SEC_WEBSOCKET_KEY1'] ? 76 : 75
|
|
120
|
+
begin
|
|
121
|
+
case version
|
|
122
|
+
when 75
|
|
123
|
+
extend Spec75
|
|
124
|
+
when 76
|
|
125
|
+
extend Spec76
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
if handshake
|
|
129
|
+
on_open
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
rescue
|
|
133
|
+
on_bad_request
|
|
134
|
+
return false
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
return true
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def on_bad_request
|
|
141
|
+
write "HTTP/1.1 400 Bad request\r\n\r\n"
|
|
142
|
+
close
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def process_frame_header
|
|
146
|
+
return false if @data.empty?
|
|
147
|
+
|
|
148
|
+
@frame_type = @data.read(1).to_i
|
|
149
|
+
if (@frame_type & 0x80) == 0x80
|
|
150
|
+
@binary_length = 0
|
|
151
|
+
@state = :process_binary_frame_header
|
|
152
|
+
else
|
|
153
|
+
@state = :process_text_frame
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
return true
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def process_binary_frame_header
|
|
160
|
+
until @data.empty?
|
|
161
|
+
|
|
162
|
+
b = @data.read(1).to_i
|
|
163
|
+
b_v = b & 0x7f
|
|
164
|
+
@binary_length = (@binary_length<<7) | b_v
|
|
165
|
+
|
|
166
|
+
if (b & 0x80) == 0x80
|
|
167
|
+
if @binary_length == 0
|
|
168
|
+
# If the /frame type/ is 0xFF and the /length/ was 0
|
|
169
|
+
write "\xff\x00"
|
|
170
|
+
@state = :invalid_state
|
|
171
|
+
close
|
|
172
|
+
return false
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
@state = :process_binary_frame
|
|
176
|
+
return true
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
end
|
|
180
|
+
return false
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def process_binary_frame
|
|
184
|
+
return false if @data.size < @binary_length
|
|
185
|
+
|
|
186
|
+
# Just discard the read bytes.
|
|
187
|
+
@data.read(@binary_length)
|
|
188
|
+
|
|
189
|
+
@state = :process_frame_header
|
|
190
|
+
return true
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def process_text_frame
|
|
194
|
+
return false if @data.empty?
|
|
195
|
+
|
|
196
|
+
pos = @data.to_str.index("\xff")
|
|
197
|
+
if pos.nil?
|
|
198
|
+
return false
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
msg = @data.read(pos)
|
|
202
|
+
@data.read(1) # read 0xff byte
|
|
203
|
+
|
|
204
|
+
@state = :process_frame_header
|
|
205
|
+
|
|
206
|
+
if @frame_type != 0x00
|
|
207
|
+
# discard the data
|
|
208
|
+
return true
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
msg.force_encoding('UTF-8') if HAVE_ENCODING
|
|
212
|
+
on_message(msg)
|
|
213
|
+
|
|
214
|
+
return true
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def write_policy_file
|
|
218
|
+
write %[<cross-domain-policy><allow-access-from domain="*" to-ports="*"/></cross-domain-policy>\0]
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def ssl?
|
|
222
|
+
false
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
private
|
|
226
|
+
|
|
227
|
+
def invalid_state
|
|
228
|
+
raise RuntimeError, "invalid state"
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
class SSLWebSocket < WebSocket
|
|
233
|
+
def on_connect
|
|
234
|
+
extend SSL
|
|
235
|
+
ssl_server_start
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def ssl?
|
|
239
|
+
true
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|