agoo 2.15.10 → 2.15.12
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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +20 -7
- data/ext/agoo/agoo.c +2 -2
- data/ext/agoo/debug.c +2 -2
- data/ext/agoo/domain.c +2 -2
- data/ext/agoo/dtime.c +1 -1
- data/ext/agoo/error_stream.c +1 -1
- data/ext/agoo/gqleval.c +1 -1
- data/ext/agoo/graphql.c +1 -1
- data/ext/agoo/http.c +2 -2
- data/ext/agoo/log.c +9 -9
- data/ext/agoo/page.c +506 -506
- data/ext/agoo/rack_logger.c +1 -1
- data/ext/agoo/rresponse.c +1 -1
- data/ext/agoo/rserver.c +31 -16
- data/lib/agoo/version.rb +1 -1
- data/misc/flymd.md +581 -0
- data/misc/glue-diagram.svg +3 -0
- data/misc/glue.md +25 -0
- data/misc/optimize.md +100 -0
- data/misc/push.md +581 -0
- data/misc/rails.md +174 -0
- data/misc/song.md +268 -0
- metadata +18 -2
data/misc/push.md
ADDED
@@ -0,0 +1,581 @@
|
|
1
|
+
# A New Rack Push
|
2
|
+
|
3
|
+
Realtime websites are essential for some domains. A web site displaying stock
|
4
|
+
prices or race progress would not be very useful if the displays were not kept
|
5
|
+
up to date. It is of course possible to use constant refreshes or polling but
|
6
|
+
that puts a heavy demand on the server and it is not real time.
|
7
|
+
|
8
|
+
Fortunately there is technology to push data to a web page. WebSocket and SSE
|
9
|
+
provide a means to push data to a web page. Coupled with Javascript pages can
|
10
|
+
be dynamic and display data as it changes.
|
11
|
+
|
12
|
+
Up until now, Ruby support for WebSockets and SSE was very cumbersome,
|
13
|
+
resulting in poor performance and higher memory bloat despite well authored
|
14
|
+
gems such as Faye and ActionCable.
|
15
|
+
|
16
|
+
Using Rack as an example it was possible to hijack a connection and manage all
|
17
|
+
the interactions using raw IO calls but that is a task that is not for the
|
18
|
+
faint at heart. It is a non trivial exercise and it requires duplication of
|
19
|
+
the network handling logic. Basically it's almost like running two servers
|
20
|
+
instead of one.
|
21
|
+
|
22
|
+
Won't it be nice to have a simple way of supporting push with Ruby? Something
|
23
|
+
that works with the server rather than against it? Something that avoids the
|
24
|
+
need to run another "server" on the same process?
|
25
|
+
|
26
|
+
There is a proposal to extend the Rack spec to include an option for WebSocket
|
27
|
+
and SSE. Currently two servers support that proposed
|
28
|
+
extension. [Agoo](https://https://github.com/ohler55/agoo) and
|
29
|
+
[Iodine](https://github.com/boazsegev/iodine) both high performance web server
|
30
|
+
gems that support push and implement the [proposed Rack
|
31
|
+
extension](https://github.com/rack/rack/pull/1272). A copy of the proposed
|
32
|
+
spec in HTML format is [here](http://www.ohler.com/agoo/rack/file.SPEC.html).
|
33
|
+
|
34
|
+
## Simple Callback Design
|
35
|
+
|
36
|
+
The Rack extension proposal uses a simple callback design. This design
|
37
|
+
replaces the need for socket hijacking, removes the hassle of socket
|
38
|
+
management and decouples the application from the network protocol layer.
|
39
|
+
|
40
|
+
All Rack handlers already have a `#call(env)` method that processes HTTP
|
41
|
+
requests. If the `env` argument to that call includes a non `nil` value for
|
42
|
+
`env['rack.upgrade?']` then the handle is expected to upgrade to a push
|
43
|
+
connection that is either WebSocket or SSE.
|
44
|
+
|
45
|
+
Acceptance of the connection upgrade is implemented by setting
|
46
|
+
`env['rack.upgrade']` to a new push handler. The push handler can implement
|
47
|
+
methods `#on_open`, `#on_close`, `#on_message`, `#on_drained`, and
|
48
|
+
`#on_shutdown`. These are all optional. A `client` object is passed as the
|
49
|
+
first argument to each method. This `client` can be used to check connection
|
50
|
+
status as well as writing messages to the connection.
|
51
|
+
|
52
|
+
The `client` has methods `#write(msg)`, `#pending`, `#open?`, and `#close`
|
53
|
+
method. The `#write(msg)` method is used to push data to web pages. The
|
54
|
+
details are handled by the server. Just call write and data appears at
|
55
|
+
browser.
|
56
|
+
|
57
|
+
## Examples
|
58
|
+
|
59
|
+
The example is a bit more than a hello world but only enough to make it
|
60
|
+
interesting. A browser is used to connect to a Rack server that runs a clock,
|
61
|
+
On each tick of the clock the time is sent to the browser. Either an SSE and a
|
62
|
+
WebSocket page can be used. That means you can connect with your mobile device
|
63
|
+
using SSE. Try it to see how easy it is.
|
64
|
+
|
65
|
+
First some web pages will be needed. Lets call them `websocket.html` and
|
66
|
+
`sse.html`. Notice how similar they look.
|
67
|
+
|
68
|
+
```html
|
69
|
+
<!-- websocket.html -->
|
70
|
+
<html>
|
71
|
+
<body>
|
72
|
+
<p id="status"> ... </p>
|
73
|
+
<p id="message"> ... waiting ... </p>
|
74
|
+
|
75
|
+
<script type="text/javascript">
|
76
|
+
var sock;
|
77
|
+
var url = "ws://" + document.URL.split('/')[2] + '/upgrade'
|
78
|
+
if (typeof MozWebSocket != "undefined") {
|
79
|
+
sock = new MozWebSocket(url);
|
80
|
+
} else {
|
81
|
+
sock = new WebSocket(url);
|
82
|
+
}
|
83
|
+
sock.onopen = function() {
|
84
|
+
document.getElementById("status").textContent = "connected";
|
85
|
+
}
|
86
|
+
sock.onmessage = function(msg) {
|
87
|
+
document.getElementById("message").textContent = msg.data;
|
88
|
+
}
|
89
|
+
</script>
|
90
|
+
</body>
|
91
|
+
</html>
|
92
|
+
```
|
93
|
+
|
94
|
+
```html
|
95
|
+
<!-- sse.html -->
|
96
|
+
<html>
|
97
|
+
<body>
|
98
|
+
<p id="status"> ... </p>
|
99
|
+
<p id="message"> ... waiting ... </p>
|
100
|
+
|
101
|
+
<script type="text/javascript">
|
102
|
+
var src = new EventSource('upgrade');
|
103
|
+
src.onopen = function() {
|
104
|
+
document.getElementById('status').textContent = 'connected';
|
105
|
+
}
|
106
|
+
src.onerror = function() {
|
107
|
+
document.getElementById('status').textContent = 'not connected';
|
108
|
+
}
|
109
|
+
function doSet(e) {
|
110
|
+
document.getElementById("message").textContent = e.data;
|
111
|
+
}
|
112
|
+
src.addEventListener('msg', doSet, false);
|
113
|
+
</script>
|
114
|
+
</body>
|
115
|
+
</html>
|
116
|
+
```
|
117
|
+
|
118
|
+
With those two files there are a couple ways to implement the Ruby side.
|
119
|
+
|
120
|
+
A pure, `rackup` example works with any of the web server gems the support the
|
121
|
+
proposed additions to the Rack spec. Currently there are two gems that support
|
122
|
+
the additions. They are [Agoo](https://github.com/ohler55/agoo) and
|
123
|
+
[Iodine](https://github.com/boazsegev/iodine). Both are high performance
|
124
|
+
servers with some features unique to both.
|
125
|
+
|
126
|
+
One example is for Agoo only to demonstrate the use of the Agoo demultiplexing
|
127
|
+
but is otherwise identical to the pure Rack example.
|
128
|
+
|
129
|
+
The third, pub-sub example is also Agoo specific but with a few minor changes
|
130
|
+
it would be compatible with Iodine as well. The pub-sub example is a minimal
|
131
|
+
example of how publish and subscribe can be used with Rack.
|
132
|
+
|
133
|
+
### Pure Rack Example
|
134
|
+
|
135
|
+
Lets start with the server agnostic example that is just Rack with the
|
136
|
+
proposed extensions to the spec. Of course the file is named `config.ru`. The
|
137
|
+
file includes more than is necessary just work but some options methods are
|
138
|
+
added to make it clear when they are called. Since it is a 'hello world'
|
139
|
+
example there is the obligatory handled for returning that result. Comments
|
140
|
+
have been stripped out of the code in favor of explaining the code separately.
|
141
|
+
|
142
|
+
First the file then an explanation.
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
# config.ru
|
146
|
+
require 'rack'
|
147
|
+
#require 'agoo'
|
148
|
+
|
149
|
+
class Clock
|
150
|
+
def initialize()
|
151
|
+
@clients = []
|
152
|
+
@mutex = Mutex.new
|
153
|
+
end
|
154
|
+
|
155
|
+
def on_open(client)
|
156
|
+
puts "--- on_open"
|
157
|
+
@mutex.synchronize {
|
158
|
+
@clients << client
|
159
|
+
}
|
160
|
+
end
|
161
|
+
|
162
|
+
def on_close(client)
|
163
|
+
puts "--- on_close"
|
164
|
+
@mutex.synchronize {
|
165
|
+
@clients.delete(client)
|
166
|
+
}
|
167
|
+
end
|
168
|
+
|
169
|
+
def on_drained(client)
|
170
|
+
puts "--- on_drained"
|
171
|
+
end
|
172
|
+
|
173
|
+
def on_message(client, data)
|
174
|
+
puts "--- on_message #{data}"
|
175
|
+
client.write("echo: #{data}")
|
176
|
+
end
|
177
|
+
|
178
|
+
def start
|
179
|
+
loop do
|
180
|
+
now = Time.now
|
181
|
+
msg = "%02d:%02d:%02d" % [now.hour, now.min, now.sec]
|
182
|
+
@mutex.synchronize {
|
183
|
+
@clients.each { |c|
|
184
|
+
puts "--- write failed" unless c.write(msg)
|
185
|
+
}
|
186
|
+
}
|
187
|
+
sleep(1)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
$clock = Clock.new
|
193
|
+
|
194
|
+
class Listen
|
195
|
+
def call(env)
|
196
|
+
path = env['SCRIPT_NAME'] + env['PATH_INFO']
|
197
|
+
case path
|
198
|
+
when '/'
|
199
|
+
return [ 200, { }, [ "hello world" ] ]
|
200
|
+
when '/websocket.html'
|
201
|
+
return [ 200, { }, [ File.read('websocket.html') ] ]
|
202
|
+
when '/sse.html'
|
203
|
+
return [ 200, { }, [ File.read('sse.html') ] ]
|
204
|
+
when '/upgrade'
|
205
|
+
unless env['rack.upgrade?'].nil?
|
206
|
+
env['rack.upgrade'] = $clock
|
207
|
+
return [ 200, { }, [ ] ]
|
208
|
+
end
|
209
|
+
end
|
210
|
+
[ 404, { }, [ ] ]
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
Thread.new { $clock.start }
|
215
|
+
run Listen.new
|
216
|
+
```
|
217
|
+
|
218
|
+
#### Running
|
219
|
+
|
220
|
+
The example is run with this command.
|
221
|
+
|
222
|
+
```
|
223
|
+
$ bundle exec rackup -r agoo -s agoo
|
224
|
+
```
|
225
|
+
|
226
|
+
The server to use must be specified to use something other than the
|
227
|
+
default. Alternatively the commented out `require` for Agoo or an alternative
|
228
|
+
can be used by uncommenting the line.
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
require 'agoo'
|
232
|
+
```
|
233
|
+
|
234
|
+
Once started open a browser and go to `http://localhost:9292/websocket.html`
|
235
|
+
or `http://localhost:9292/sse.html` and watch the page show 'connected' and
|
236
|
+
then showing the time as it changes every second.
|
237
|
+
|
238
|
+
#### Clock
|
239
|
+
|
240
|
+
The `Clock` class is the handler for upgraded calls. The `Listener` and
|
241
|
+
`Clock` could have been combined into a single class but for clarity they are
|
242
|
+
kept separate in this example.
|
243
|
+
|
244
|
+
The `Clock` object, `$clock` maintains a list of open connections. The default
|
245
|
+
configuration uses one thread but to be safe a mutex is used so that the same
|
246
|
+
example can be used with multiple threads configured. It's a good practice
|
247
|
+
when writing asynchronous callback code to assume there will be multiple threads
|
248
|
+
invoking the callbacks.
|
249
|
+
|
250
|
+
```ruby
|
251
|
+
def initialize()
|
252
|
+
@clients = []
|
253
|
+
@mutex = Mutex.new
|
254
|
+
end
|
255
|
+
```
|
256
|
+
|
257
|
+
The `#on_open` callback is called when a connection has been upgraded to a
|
258
|
+
WebSocket or SSE connection. That is the time to add the connection to the
|
259
|
+
client `client` to the `@clients` list is used to implement a broadcast write
|
260
|
+
or publish to all open connections. There are other options that will be
|
261
|
+
explored later.
|
262
|
+
|
263
|
+
```ruby
|
264
|
+
def on_open(client)
|
265
|
+
puts "--- on_open"
|
266
|
+
@mutex.synchronize {
|
267
|
+
@clients << client
|
268
|
+
}
|
269
|
+
end
|
270
|
+
```
|
271
|
+
|
272
|
+
When a connection is closed it should be removed the the `@clients` list. The
|
273
|
+
`#on_close` callback handles that. The other callbacks merely print out a
|
274
|
+
statement that they have been invoked so that it is easier to trace what is
|
275
|
+
going on.
|
276
|
+
|
277
|
+
```ruby
|
278
|
+
def on_close(client)
|
279
|
+
puts "--- on_close"
|
280
|
+
@mutex.synchronize {
|
281
|
+
@clients.delete(client)
|
282
|
+
}
|
283
|
+
end
|
284
|
+
```
|
285
|
+
|
286
|
+
Finally, the clock is going to publish the time to all connections. The
|
287
|
+
`#start` method starts the clock and then every second the time is published
|
288
|
+
by writing to each connection.
|
289
|
+
|
290
|
+
```ruby
|
291
|
+
def start
|
292
|
+
loop do
|
293
|
+
now = Time.now
|
294
|
+
msg = "%02d:%02d:%02d" % [now.hour, now.min, now.sec]
|
295
|
+
@mutex.synchronize {
|
296
|
+
@clients.each { |c|
|
297
|
+
puts "--- write failed" unless c.write(msg)
|
298
|
+
}
|
299
|
+
}
|
300
|
+
sleep(1)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
```
|
304
|
+
|
305
|
+
#### Listening
|
306
|
+
|
307
|
+
In a rackup started application, all request go to a single handler that has a
|
308
|
+
`#call(env)` method. Demultiplexing of the incoming request is done in the
|
309
|
+
`#call(env)` itself. Requests for static assets such as web pages have to be
|
310
|
+
handled.
|
311
|
+
|
312
|
+
```ruby
|
313
|
+
def call(env)
|
314
|
+
path = env['SCRIPT_NAME'] + env['PATH_INFO']
|
315
|
+
case path
|
316
|
+
when '/'
|
317
|
+
return [ 200, { }, [ "hello world" ] ]
|
318
|
+
when '/websocket.html'
|
319
|
+
return [ 200, { }, [ File.read('websocket.html') ] ]
|
320
|
+
when '/sse.html'
|
321
|
+
return [ 200, { }, [ File.read('sse.html') ] ]
|
322
|
+
when '/upgrade'
|
323
|
+
unless env['rack.upgrade?'].nil?
|
324
|
+
env['rack.upgrade'] = $clock
|
325
|
+
return [ 200, { }, [ ] ]
|
326
|
+
end
|
327
|
+
end
|
328
|
+
[ 404, { }, [ ] ]
|
329
|
+
end
|
330
|
+
```
|
331
|
+
|
332
|
+
The proposed feature is the detection of a connection that wants to be
|
333
|
+
upgraded by checking the `env['rack.upgrade?']` variable. If that variables is
|
334
|
+
`:websocket` of `sse` then a handle should be provided by setting
|
335
|
+
`env['rack.upgrade']` to the upgrade handler. A return code of less than 300
|
336
|
+
will indicate to the server that the connection can be upgraded.
|
337
|
+
|
338
|
+
```ruby
|
339
|
+
when '/upgrade'
|
340
|
+
unless env['rack.upgrade?'].nil?
|
341
|
+
env['rack.upgrade'] = $clock
|
342
|
+
return [ 200, { }, [ ] ]
|
343
|
+
end
|
344
|
+
end
|
345
|
+
```
|
346
|
+
|
347
|
+
#### Starting
|
348
|
+
|
349
|
+
The last lines start the clock is a separate thread so that it runs in the
|
350
|
+
background and then rackup starts the server with a new listener.
|
351
|
+
|
352
|
+
```ruby
|
353
|
+
Thread.new { $clock.start }
|
354
|
+
run Listen.new
|
355
|
+
```
|
356
|
+
|
357
|
+
### Demultiplex
|
358
|
+
|
359
|
+
The `push.rb` is an example of using the Agoo demultiplexing feature. Allowing
|
360
|
+
the server to handle the demultiplexing simplifies `#call(env)` method and
|
361
|
+
allows each URL path to be handled by a separate object. This approach is
|
362
|
+
common among most web server in almost every language. But that is not the
|
363
|
+
only advantage. By setting a root directory for static resources Agoo can
|
364
|
+
serve up those resources without getting Ruby involved at all. This allows
|
365
|
+
those resources to be server many times faster all without creating additional
|
366
|
+
Ruby objects. Pages with significant static resources become snappier and the
|
367
|
+
whole users experience is improved.
|
368
|
+
|
369
|
+
Note: It is possible to set up the demultiplexing using arguments to
|
370
|
+
`rackup`. As an example, to set up the handler for `/myhandler` to an instance
|
371
|
+
of the `MyHandler` class start `rackup` like this.
|
372
|
+
|
373
|
+
```
|
374
|
+
$ rackup -r agoo -s agoo -O "/myhandler=MyHandler"
|
375
|
+
```
|
376
|
+
|
377
|
+
Now the `push.rb` file.
|
378
|
+
|
379
|
+
```ruby
|
380
|
+
# push.rb
|
381
|
+
require 'agoo'
|
382
|
+
Agoo::Log.configure(dir: '',
|
383
|
+
console: true,
|
384
|
+
classic: true,
|
385
|
+
colorize: true,
|
386
|
+
states: {
|
387
|
+
INFO: true,
|
388
|
+
DEBUG: false,
|
389
|
+
connect: true,
|
390
|
+
request: true,
|
391
|
+
response: true,
|
392
|
+
eval: true,
|
393
|
+
push: true,
|
394
|
+
})
|
395
|
+
Agoo::Server.init(6464, '.', thread_count: 1)
|
396
|
+
|
397
|
+
class HelloHandler
|
398
|
+
def call(env)
|
399
|
+
[ 200, { }, [ "hello world" ] ]
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
class Clock
|
404
|
+
def initialize()
|
405
|
+
@clients = []
|
406
|
+
@mutex = Mutex.new
|
407
|
+
end
|
408
|
+
|
409
|
+
def on_open(client)
|
410
|
+
puts "--- on_open"
|
411
|
+
@mutex.synchronize {
|
412
|
+
@clients << client
|
413
|
+
}
|
414
|
+
end
|
415
|
+
|
416
|
+
def on_close(client)
|
417
|
+
puts "--- on_close"
|
418
|
+
@mutex.synchronize {
|
419
|
+
@clients.delete(client)
|
420
|
+
}
|
421
|
+
end
|
422
|
+
|
423
|
+
def on_drained(client)
|
424
|
+
puts "--- on_drained"
|
425
|
+
end
|
426
|
+
|
427
|
+
def on_message(client, data)
|
428
|
+
puts "--- on_message #{data}"
|
429
|
+
client.write("echo: #{data}")
|
430
|
+
end
|
431
|
+
|
432
|
+
def start
|
433
|
+
loop do
|
434
|
+
now = Time.now
|
435
|
+
msg = "%02d:%02d:%02d" % [now.hour, now.min, now.sec]
|
436
|
+
@mutex.synchronize {
|
437
|
+
@clients.each { |c|
|
438
|
+
puts "--- write failed" unless c.write(msg)
|
439
|
+
}
|
440
|
+
}
|
441
|
+
sleep(1)
|
442
|
+
end
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
$clock = Clock.new
|
447
|
+
|
448
|
+
class Listen
|
449
|
+
# Only used for WebSocket or SSE upgrades.
|
450
|
+
def call(env)
|
451
|
+
unless env['rack.upgrade?'].nil?
|
452
|
+
env['rack.upgrade'] = $clock
|
453
|
+
[ 200, { }, [ ] ]
|
454
|
+
else
|
455
|
+
[ 404, { }, [ ] ]
|
456
|
+
end
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
Agoo::Server.handle(:GET, "/hello", HelloHandler.new)
|
461
|
+
Agoo::Server.handle(:GET, "/upgrade", Listen.new)
|
462
|
+
|
463
|
+
Agoo::Server.start()
|
464
|
+
$clock.start
|
465
|
+
```
|
466
|
+
|
467
|
+
The `push.rb` file is similar to the `config.ru` file with the same `Clock`
|
468
|
+
class. Running is different but still simple.
|
469
|
+
|
470
|
+
```
|
471
|
+
$ ruby push.rb
|
472
|
+
```
|
473
|
+
|
474
|
+
#### Logging
|
475
|
+
|
476
|
+
Agoo allows logging to be configured. The log is an asynchronous log so the
|
477
|
+
impact of logging on performance is minimal. The log is a feature based
|
478
|
+
logger. Most of the features are turned on so that it is clear what is
|
479
|
+
happening when running the example. Switch the `true` values to `false` to
|
480
|
+
turn off logging of any of the features listed.
|
481
|
+
|
482
|
+
```ruby
|
483
|
+
Agoo::Log.configure(dir: '',
|
484
|
+
console: true,
|
485
|
+
classic: true,
|
486
|
+
colorize: true,
|
487
|
+
states: {
|
488
|
+
INFO: true,
|
489
|
+
DEBUG: false,
|
490
|
+
connect: true,
|
491
|
+
request: true,
|
492
|
+
response: true,
|
493
|
+
eval: true,
|
494
|
+
push: true,
|
495
|
+
})
|
496
|
+
```
|
497
|
+
|
498
|
+
#### Static Assets
|
499
|
+
|
500
|
+
The second argument to the Aggo server initializer sets the root directory for
|
501
|
+
static assets.
|
502
|
+
|
503
|
+
```ruby
|
504
|
+
Agoo::Server.init(6464, '.', thread_count: 1)
|
505
|
+
```
|
506
|
+
|
507
|
+
#### Listening
|
508
|
+
|
509
|
+
Listening becomes simpler just handling the upgrade.
|
510
|
+
|
511
|
+
```ruby
|
512
|
+
def call(env)
|
513
|
+
unless env['rack.upgrade?'].nil?
|
514
|
+
env['rack.upgrade'] = $clock
|
515
|
+
[ 200, { }, [ ] ]
|
516
|
+
else
|
517
|
+
[ 404, { }, [ ] ]
|
518
|
+
end
|
519
|
+
end
|
520
|
+
```
|
521
|
+
|
522
|
+
#### Demultiplexing
|
523
|
+
|
524
|
+
It is rare to have the behavior on a URL path to change after starting so why
|
525
|
+
not let the server handle the switching. The option to let the application
|
526
|
+
handle the demultiplexing in the `#call(env)` invocation but defining the
|
527
|
+
switching in one place is much easier to follow and manage especially a large
|
528
|
+
team of developers are working on a project.
|
529
|
+
|
530
|
+
```ruby
|
531
|
+
Agoo::Server.handle(:GET, "/hello", HelloHandler.new)
|
532
|
+
Agoo::Server.handle(:GET, "/upgrade", Listen.new)
|
533
|
+
```
|
534
|
+
|
535
|
+
### Simplified Pub-Sub
|
536
|
+
|
537
|
+
No reason to stop simplifying. With Agoo 2.1.0, publish and subscribe is
|
538
|
+
possible using string subjects. Instead of setting up the `@clients` array
|
539
|
+
attribute the Agoo server can take care of those details. This example is
|
540
|
+
slimmed down from the earlier example by making use of the Ruby feature that
|
541
|
+
classes are also object so the `Clock` class can act as the handler. Methods
|
542
|
+
not needed have been removed to leave just what is needed to publish time out
|
543
|
+
to all the listeners. Open a few pages with either or both the
|
544
|
+
`websocket.html` or `sse.html` files.
|
545
|
+
|
546
|
+
Note the use of `Agoo.publish` and `client.subscribe` in the `pubsub.rb`
|
547
|
+
example. Those two method are not part of the proposed Rack spec additions but
|
548
|
+
make stateless WebSocket and SSE use simple. Us this with the `websocket.html`
|
549
|
+
and `sse.html` as with the previous examples.
|
550
|
+
|
551
|
+
```ruby
|
552
|
+
# pubsub.rb
|
553
|
+
require 'agoo'
|
554
|
+
|
555
|
+
class Clock
|
556
|
+
def self.call(env)
|
557
|
+
unless env['rack.upgrade?'].nil?
|
558
|
+
env['rack.upgrade'] = Clock
|
559
|
+
[ 200, { }, [ ] ]
|
560
|
+
else
|
561
|
+
[ 404, { }, [ ] ]
|
562
|
+
end
|
563
|
+
end
|
564
|
+
|
565
|
+
def self.on_open(client)
|
566
|
+
client.subscribe('time')
|
567
|
+
end
|
568
|
+
|
569
|
+
Thread.new {
|
570
|
+
loop do
|
571
|
+
now = Time.now
|
572
|
+
Agoo.publish('time', "%02d:%02d:%02d" % [now.hour, now.min, now.sec])
|
573
|
+
sleep(1)
|
574
|
+
end
|
575
|
+
}
|
576
|
+
end
|
577
|
+
|
578
|
+
Agoo::Server.init(6464, '.', thread_count: 0)
|
579
|
+
Agoo::Server.handle(:GET, '/upgrade', Clock)
|
580
|
+
Agoo::Server.start()
|
581
|
+
```
|