faye-websocket 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of faye-websocket might be problematic. Click here for more details.
- data/CHANGELOG.txt +6 -0
- data/README.rdoc +209 -24
- data/examples/app.rb +50 -0
- data/examples/config.ru +17 -0
- data/examples/server.rb +33 -32
- data/examples/sse.html +39 -0
- data/examples/{index.html → ws.html} +12 -8
- data/lib/faye/adapters/goliath.rb +47 -0
- data/lib/faye/adapters/rainbows.rb +32 -0
- data/lib/faye/adapters/rainbows_client.rb +70 -0
- data/lib/faye/{thin_extensions.rb → adapters/thin.rb} +25 -38
- data/lib/faye/eventsource.rb +118 -0
- data/lib/faye/websocket.rb +64 -30
- data/lib/faye/websocket/adapter.rb +21 -0
- data/lib/faye/websocket/api.rb +13 -62
- data/lib/faye/websocket/api/event.rb +33 -0
- data/lib/faye/websocket/api/event_target.rb +34 -0
- data/spec/faye/websocket/client_spec.rb +16 -6
- data/spec/rainbows.conf +3 -0
- data/spec/spec_helper.rb +23 -9
- metadata +45 -23
data/CHANGELOG.txt
CHANGED
data/README.rdoc
CHANGED
@@ -4,9 +4,21 @@ This is a robust, general-purpose WebSocket implementation extracted from the
|
|
4
4
|
{Faye}[http://faye.jcoglan.com] project. It provides classes for easily building
|
5
5
|
WebSocket servers and clients in Ruby. It does not provide a server itself, but
|
6
6
|
rather makes it easy to handle WebSocket connections within an existing
|
7
|
-
{Rack}[http://rack.rubyforge.org/] application
|
8
|
-
|
9
|
-
|
7
|
+
{Rack}[http://rack.rubyforge.org/] application. It does not provide any
|
8
|
+
abstraction other than the standard
|
9
|
+
{WebSocket API}[http://dev.w3.org/html5/websockets/].
|
10
|
+
|
11
|
+
It also provides an abstraction for handling {EventSource}[http://dev.w3.org/html5/eventsource/]
|
12
|
+
connections, which are one-way connections that allow the server to push data to
|
13
|
+
the client. They are based on streaming HTTP responses and can be easier to
|
14
|
+
access via proxies than WebSockets.
|
15
|
+
|
16
|
+
Currently, the following web servers are supported, and can be accessed directly
|
17
|
+
or via HAProxy:
|
18
|
+
|
19
|
+
* {Thin}[http://code.macournoyer.com/thin/]
|
20
|
+
* {Rainbows}[http://rainbows.rubyforge.org/] using EventMachine
|
21
|
+
* {Goliath}[http://postrank-labs.github.com/goliath/]
|
10
22
|
|
11
23
|
The server-side socket can process {draft-75}[http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75],
|
12
24
|
{draft-76}[http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76],
|
@@ -16,19 +28,18 @@ supports both +text+ and +binary+ messages, and transparently handles +ping+,
|
|
16
28
|
+pong+, +close+ and fragmented messages.
|
17
29
|
|
18
30
|
|
19
|
-
==
|
31
|
+
== Handling WebSocket connections in Rack
|
20
32
|
|
21
|
-
You can handle WebSockets on the server side by listening for
|
22
|
-
|
23
|
-
the usual WebSocket methods for receiving
|
24
|
-
is how you'd implement an echo server:
|
33
|
+
You can handle WebSockets on the server side by listening for requests using the
|
34
|
+
<tt>Faye::WebSocket.websocket?</tt> method, and creating a new socket for the
|
35
|
+
request. This socket object exposes the usual WebSocket methods for receiving
|
36
|
+
and sending messages. For example this is how you'd implement an echo server:
|
25
37
|
|
38
|
+
# app.rb
|
26
39
|
require 'faye/websocket'
|
27
|
-
require 'rack'
|
28
|
-
require 'eventmachine'
|
29
40
|
|
30
|
-
|
31
|
-
if env
|
41
|
+
App = lambda do |env|
|
42
|
+
if Faye::WebSocket.websocket?(env)
|
32
43
|
ws = Faye::WebSocket.new(env)
|
33
44
|
|
34
45
|
ws.onmessage = lambda do |event|
|
@@ -40,27 +51,33 @@ is how you'd implement an echo server:
|
|
40
51
|
ws = nil
|
41
52
|
end
|
42
53
|
|
43
|
-
#
|
44
|
-
|
54
|
+
# Return async Rack response
|
55
|
+
ws.rack_response
|
45
56
|
|
46
57
|
else
|
47
58
|
# Normal HTTP request
|
48
59
|
[200, {'Content-Type' => 'text/plain'}, ['Hello']]
|
49
60
|
end
|
50
61
|
end
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
62
|
+
|
63
|
+
This is a standard Rack app, so it can be run using a <tt>config.ru</tt> file.
|
64
|
+
However, so that incoming requests can be properly prepared to process WebSocket
|
65
|
+
connections, you need to tell <tt>Faye::WebSocket</tt> which adapter to load;
|
66
|
+
this can be either +thin+, +rainbows+ or +goliath+. If one of these servers is
|
67
|
+
already loaded before <tt>faye/websocket</tt> is loaded, it will load
|
68
|
+
appropriate adapters automatically.
|
69
|
+
|
70
|
+
# config.ru
|
71
|
+
require './app'
|
72
|
+
Faye::WebSocket.load_adapter('thin')
|
73
|
+
run App
|
56
74
|
|
57
75
|
|
58
76
|
== Using the WebSocket client
|
59
77
|
|
60
78
|
The client supports both the plain-text +ws+ protocol and the encrypted +wss+
|
61
79
|
protocol, and has exactly the same interface as a socket you would use in a web
|
62
|
-
browser. On the wire it identifies itself as hybi-13
|
63
|
-
with servers speaking later versions of the protocol.
|
80
|
+
browser. On the wire it identifies itself as hybi-13.
|
64
81
|
|
65
82
|
require 'faye/websocket'
|
66
83
|
require 'eventmachine'
|
@@ -125,15 +142,183 @@ messages.
|
|
125
142
|
the other peer.
|
126
143
|
* <b><tt>close(code, reason)</tt></b> closes the connection, sending the given
|
127
144
|
status code and reason text, both of which are optional.
|
128
|
-
* <b><tt>protocol</tt></b> is a string
|
129
|
-
socket is using.
|
145
|
+
* <b><tt>protocol</tt></b> is a string (which may be empty) identifying the
|
146
|
+
subprotocol the socket is using.
|
147
|
+
|
148
|
+
|
149
|
+
== Handling EventSource connections in Rack
|
150
|
+
|
151
|
+
EventSource connections provide a very similar interface, although because they
|
152
|
+
only allow the server to send data to the client, there is no +onmessage+ API.
|
153
|
+
EventSource allows the server to push text messages to the client, where each
|
154
|
+
message has an optional event-type and ID.
|
155
|
+
|
156
|
+
# app.rb
|
157
|
+
require 'faye/websocket'
|
158
|
+
|
159
|
+
App = lambda do |env|
|
160
|
+
if Faye::EventSource.eventsource?(env)
|
161
|
+
es = Faye::EventSource.new(env)
|
162
|
+
p [:open, es.url, es.last_event_id]
|
163
|
+
|
164
|
+
# Periodically send messages
|
165
|
+
loop = EM.add_periodic_timer(1) { es.send('Hello') }
|
166
|
+
|
167
|
+
es.onclose = lambda do |event|
|
168
|
+
EM.cancel_timer(loop)
|
169
|
+
es = nil
|
170
|
+
end
|
171
|
+
|
172
|
+
# Return async Rack response
|
173
|
+
es.rack_response
|
174
|
+
|
175
|
+
else
|
176
|
+
# Normal HTTP request
|
177
|
+
[200, {'Content-Type' => 'text/plain'}, ['Hello']]
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
The +send+ method takes two optional parameters, <tt>:event</tt> and
|
182
|
+
<tt>:id</tt>. The default event-type is <tt>'message'</tt> with no ID. For
|
183
|
+
example, to send a +notification+ event with ID +99+:
|
184
|
+
|
185
|
+
es.send('Breaking News!', :event => 'notification', :id => '99')
|
186
|
+
|
187
|
+
The +EventSource+ object exposes the following properties:
|
188
|
+
|
189
|
+
* <b><tt>url</tt></b> is a string containing the URL the client used to create
|
190
|
+
the EventSource.
|
191
|
+
* <b><tt>last_event_id</tt></b> is a string containing the last event ID
|
192
|
+
received by the client. You can use this when the client reconnects after a
|
193
|
+
dropped connection to determine which messages need resending.
|
194
|
+
|
195
|
+
When you initialize an EventSource with <tt>Faye::EventSource.new</tt>, you can
|
196
|
+
pass configuration options after the +env+ parameter. Available options are:
|
197
|
+
|
198
|
+
* <b><tt>:retry</tt></b> is a number that tells the client how long (in seconds)
|
199
|
+
it should wait after a dropped connection before attempting to reconnect.
|
200
|
+
* <b><tt>:ping</tt></b> is a number that tells the server how often (in seconds)
|
201
|
+
to send 'ping' packets to the client to keep the connection open, to defeat
|
202
|
+
timeouts set by proxies. The client will ignore these messages.
|
203
|
+
|
204
|
+
For example, this creates a connection that pings every 15 seconds and is
|
205
|
+
retryable every 10 seconds if the connection is broken:
|
206
|
+
|
207
|
+
es = Faye::EventSource.new(es, :ping => 15, :retry => 10)
|
208
|
+
|
209
|
+
|
210
|
+
== Running your socket application
|
211
|
+
|
212
|
+
To use this library you must be using an EventMachine-based server; currently
|
213
|
+
Thin, Rainbows and Goliath are supported.
|
214
|
+
|
215
|
+
|
216
|
+
=== Running the app with Thin
|
217
|
+
|
218
|
+
Thin can be started via the command line if you've set up a <tt>config.ru</tt>
|
219
|
+
file for your application:
|
220
|
+
|
221
|
+
thin start -R config.ru -p 9292
|
222
|
+
|
223
|
+
Or, you can use +rackup+. In development mode, this adds middlewares that don't
|
224
|
+
work with async apps, so you must start it in production mode:
|
225
|
+
|
226
|
+
rackup config.ru -s thin -E production -p 9292
|
227
|
+
|
228
|
+
It can also be started using the <tt>Rack::Handler</tt> interface common to many
|
229
|
+
Ruby servers. It must be run using EventMachine, and you can configure Thin
|
230
|
+
further in a block passed to +run+:
|
231
|
+
|
232
|
+
require 'eventmachine'
|
233
|
+
require 'rack'
|
234
|
+
require 'thin'
|
235
|
+
require './app'
|
236
|
+
|
237
|
+
EM.run {
|
238
|
+
thin = Rack::Handler.get('thin')
|
239
|
+
|
240
|
+
thin.run(App, :Port => 9292) do |server|
|
241
|
+
# You can set options on the server here, for example to set up SSL:
|
242
|
+
server.ssl_options = {
|
243
|
+
:private_key_file => 'path/to/ssl.key',
|
244
|
+
:cert_chain_file => 'path/to/ssl.crt'
|
245
|
+
}
|
246
|
+
server.ssl = true
|
247
|
+
end
|
248
|
+
}
|
249
|
+
|
250
|
+
|
251
|
+
=== Running the app with Rainbows
|
252
|
+
|
253
|
+
<tt>Faye::WebSocket</tt> can only be run using EventMachine. To begin with,
|
254
|
+
you'll need a Rainbows config file that tells it to use EventMachine, along with
|
255
|
+
whatever Rainbows/Unicorn configuration you require.
|
256
|
+
|
257
|
+
# rainbows.conf
|
258
|
+
Rainbows! do
|
259
|
+
use :EventMachine
|
260
|
+
end
|
261
|
+
|
262
|
+
You can then run your <tt>config.ru</tt> file from the command line. Again,
|
263
|
+
<tt>Rack::Lint</tt> will complain unless you put the application in production
|
264
|
+
mode.
|
265
|
+
|
266
|
+
rainbows config.ru -c path/to/rainbows.conf -E production -p 9292
|
267
|
+
|
268
|
+
Rainbows also has a Ruby API for starting a server:
|
269
|
+
|
270
|
+
require 'rainbows'
|
271
|
+
require './app'
|
272
|
+
|
273
|
+
rackup = Unicorn::Configurator::RACKUP
|
274
|
+
rackup[:port] = 9292
|
275
|
+
rackup[:set_listener] = true
|
276
|
+
options = rackup[:options]
|
277
|
+
options[:config_file] = 'path/to/rainbows.conf'
|
278
|
+
|
279
|
+
server = Rainbows::HttpServer.new(App, options)
|
280
|
+
|
281
|
+
# This is non-blocking; use server.start.join to block
|
282
|
+
server.start
|
283
|
+
|
284
|
+
|
285
|
+
=== Running the app with Goliath
|
286
|
+
|
287
|
+
Goliath can be made to run arbitrary Rack apps by delegating to them from a
|
288
|
+
<tt>Goliath::API</tt> instance. A simple server looks like this:
|
289
|
+
|
290
|
+
require 'goliath'
|
291
|
+
require './app'
|
292
|
+
|
293
|
+
class EchoServer < Goliath::API
|
294
|
+
def response(env)
|
295
|
+
App.call(env)
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
<tt>Faye::WebSocket</tt> can also be used inline within a Goliath app:
|
300
|
+
|
301
|
+
require 'goliath'
|
302
|
+
require 'faye/websocket'
|
303
|
+
|
304
|
+
class EchoServer < Goliath::API
|
305
|
+
def response(env)
|
306
|
+
ws = Faye::WebSocket.new(env)
|
307
|
+
|
308
|
+
ws.onmessage = lambda do |event|
|
309
|
+
ws.send(event.data)
|
310
|
+
end
|
311
|
+
|
312
|
+
ws.rack_response
|
313
|
+
end
|
314
|
+
end
|
130
315
|
|
131
316
|
|
132
317
|
== License
|
133
318
|
|
134
319
|
(The MIT License)
|
135
320
|
|
136
|
-
Copyright (c) 2009-
|
321
|
+
Copyright (c) 2009-2012 James Coglan
|
137
322
|
|
138
323
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
139
324
|
this software and associated documentation files (the 'Software'), to deal in
|
data/examples/app.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require File.expand_path('../../lib/faye/websocket', __FILE__)
|
2
|
+
require 'rack'
|
3
|
+
|
4
|
+
static = Rack::File.new(File.dirname(__FILE__))
|
5
|
+
|
6
|
+
App = lambda do |env|
|
7
|
+
if Faye::WebSocket.websocket?(env)
|
8
|
+
ws = Faye::WebSocket.new(env, ['irc', 'xmpp'])
|
9
|
+
p [:open, ws.url, ws.version, ws.protocol]
|
10
|
+
|
11
|
+
ws.onmessage = lambda do |event|
|
12
|
+
ws.send(event.data)
|
13
|
+
end
|
14
|
+
|
15
|
+
ws.onclose = lambda do |event|
|
16
|
+
p [:close, event.code, event.reason]
|
17
|
+
ws = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
ws.rack_response
|
21
|
+
|
22
|
+
elsif Faye::EventSource.eventsource?(env)
|
23
|
+
es = Faye::EventSource.new(env)
|
24
|
+
time = es.last_event_id.to_i
|
25
|
+
|
26
|
+
p [:open, es.url, es.last_event_id]
|
27
|
+
|
28
|
+
loop = EM.add_periodic_timer(2) do
|
29
|
+
time += 1
|
30
|
+
es.send("Time: #{time}")
|
31
|
+
EM.add_timer(1) do
|
32
|
+
es.send('Update!!', :event => 'update', :id => time) if es
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
es.send("Welcome!\n\nThis is an EventSource server.")
|
37
|
+
|
38
|
+
es.onclose = lambda do |event|
|
39
|
+
EM.cancel_timer(loop)
|
40
|
+
p [:close, es.url]
|
41
|
+
es = nil
|
42
|
+
end
|
43
|
+
|
44
|
+
es.rack_response
|
45
|
+
|
46
|
+
else
|
47
|
+
static.call(env)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
data/examples/config.ru
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# Run using your favourite async server:
|
2
|
+
#
|
3
|
+
# thin start -R examples/config.ru -p 7000
|
4
|
+
# rainbows -c spec/rainbows.conf -E production examples/config.ru -p 7000
|
5
|
+
#
|
6
|
+
# If you run using one of these commands, the webserver is loaded before this
|
7
|
+
# file, so Faye::WebSocket can figure out which adapter to load. If instead you
|
8
|
+
# run using `rackup`, you need the `load_adapter` line below.
|
9
|
+
#
|
10
|
+
# rackup -E production -s thin examples/config.ru -p 7000
|
11
|
+
|
12
|
+
require 'rubygems'
|
13
|
+
require File.expand_path('../app', __FILE__)
|
14
|
+
# Faye::WebSocket.load_adapter('thin')
|
15
|
+
|
16
|
+
run App
|
17
|
+
|
data/examples/server.rb
CHANGED
@@ -1,43 +1,44 @@
|
|
1
1
|
require 'rubygems'
|
2
|
-
require
|
3
|
-
require 'rack'
|
4
|
-
require 'eventmachine'
|
2
|
+
require 'rack/content_length'
|
3
|
+
require 'rack/chunked'
|
5
4
|
|
6
5
|
port = ARGV[0] || 7000
|
7
6
|
secure = ARGV[1] == 'ssl'
|
7
|
+
engine = ARGV[2] || 'thin'
|
8
|
+
spec = File.expand_path('../../spec', __FILE__)
|
8
9
|
|
9
|
-
|
10
|
+
require File.expand_path('../app', __FILE__)
|
11
|
+
Faye::WebSocket.load_adapter(engine)
|
10
12
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
socket.send(event.data)
|
18
|
-
end
|
19
|
-
|
20
|
-
socket.onclose = lambda do |event|
|
21
|
-
p [:close, event.code, event.reason]
|
22
|
-
socket = nil
|
13
|
+
case engine
|
14
|
+
|
15
|
+
when 'goliath'
|
16
|
+
class WebSocketServer < Goliath::API
|
17
|
+
def response(env)
|
18
|
+
App.call(env)
|
23
19
|
end
|
24
|
-
|
25
|
-
[-1, {}, []]
|
26
|
-
else
|
27
|
-
static.call(env)
|
28
20
|
end
|
29
|
-
end
|
30
21
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
22
|
+
when 'rainbows'
|
23
|
+
rackup = Unicorn::Configurator::RACKUP
|
24
|
+
rackup[:port] = port
|
25
|
+
rackup[:set_listener] = true
|
26
|
+
options = rackup[:options]
|
27
|
+
options[:config_file] = spec + '/rainbows.conf'
|
28
|
+
Rainbows::HttpServer.new(App, options).start.join
|
29
|
+
|
30
|
+
when 'thin'
|
31
|
+
EM.run {
|
32
|
+
thin = Rack::Handler.get('thin')
|
33
|
+
thin.run(App, :Port => port) do |server|
|
34
|
+
if secure
|
35
|
+
server.ssl_options = {
|
36
|
+
:private_key_file => spec + '/server.key',
|
37
|
+
:cert_chain_file => spec + '/server.crt'
|
38
|
+
}
|
39
|
+
server.ssl = true
|
40
|
+
end
|
40
41
|
end
|
41
|
-
|
42
|
-
|
42
|
+
}
|
43
|
+
end
|
43
44
|
|
data/examples/sse.html
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
<!doctype html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
|
5
|
+
<title>EventSource test</title>
|
6
|
+
</head>
|
7
|
+
<body>
|
8
|
+
|
9
|
+
<h1>EventSource test</h1>
|
10
|
+
<ul></ul>
|
11
|
+
|
12
|
+
<script type="text/javascript">
|
13
|
+
var logger = document.getElementsByTagName('ul')[0],
|
14
|
+
socket = new EventSource('/');
|
15
|
+
|
16
|
+
var log = function(text) {
|
17
|
+
logger.innerHTML += '<li>' + text + '</li>';
|
18
|
+
};
|
19
|
+
|
20
|
+
socket.onopen = function() {
|
21
|
+
log('OPEN');
|
22
|
+
};
|
23
|
+
|
24
|
+
socket.onmessage = function(event) {
|
25
|
+
log('MESSAGE: ' + event.data);
|
26
|
+
};
|
27
|
+
|
28
|
+
socket.addEventListener('update', function(event) {
|
29
|
+
log('UPDATE(' + event.lastEventId + '): ' + event.data);
|
30
|
+
});
|
31
|
+
|
32
|
+
socket.onerror = function(event) {
|
33
|
+
log('ERROR: ' + event.message);
|
34
|
+
};
|
35
|
+
</script>
|
36
|
+
|
37
|
+
</body>
|
38
|
+
</html>
|
39
|
+
|