iodine 0.4.19 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of iodine might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.travis.yml +1 -2
- data/CHANGELOG.md +22 -0
- data/LIMITS.md +19 -9
- data/README.md +92 -77
- data/SPEC-PubSub-Draft.md +113 -0
- data/SPEC-Websocket-Draft.md +127 -143
- data/bin/http-hello +0 -1
- data/bin/raw-rbhttp +1 -1
- data/bin/raw_broadcast +8 -10
- data/bin/updated api +2 -2
- data/bin/ws-broadcast +2 -4
- data/bin/ws-echo +2 -2
- data/examples/config.ru +13 -13
- data/examples/echo.ru +5 -6
- data/examples/hello.ru +2 -3
- data/examples/info.md +316 -0
- data/examples/pubsub_engine.ru +81 -0
- data/examples/redis.ru +9 -9
- data/examples/shootout.ru +45 -11
- data/ext/iodine/defer.c +194 -297
- data/ext/iodine/defer.h +61 -53
- data/ext/iodine/evio.c +0 -260
- data/ext/iodine/evio.h +50 -22
- data/ext/iodine/evio_callbacks.c +26 -0
- data/ext/iodine/evio_epoll.c +251 -0
- data/ext/iodine/evio_kqueue.c +193 -0
- data/ext/iodine/extconf.rb +1 -1
- data/ext/iodine/facil.c +1420 -542
- data/ext/iodine/facil.h +151 -64
- data/ext/iodine/fio_ary.h +418 -0
- data/ext/iodine/{base64.c → fio_base64.c} +33 -24
- data/ext/iodine/{base64.h → fio_base64.h} +6 -7
- data/ext/iodine/{fio_cli_helper.c → fio_cli.c} +77 -58
- data/ext/iodine/{fio_cli_helper.h → fio_cli.h} +9 -4
- data/ext/iodine/fio_hashmap.h +759 -0
- data/ext/iodine/fio_json_parser.h +651 -0
- data/ext/iodine/fio_llist.h +257 -0
- data/ext/iodine/fio_mem.c +672 -0
- data/ext/iodine/fio_mem.h +140 -0
- data/ext/iodine/fio_random.c +248 -0
- data/ext/iodine/{random.h → fio_random.h} +11 -14
- data/ext/iodine/{sha1.c → fio_sha1.c} +28 -24
- data/ext/iodine/{sha1.h → fio_sha1.h} +38 -16
- data/ext/iodine/{sha2.c → fio_sha2.c} +66 -49
- data/ext/iodine/{sha2.h → fio_sha2.h} +57 -26
- data/ext/iodine/{fiobj_internal.c → fio_siphash.c} +9 -90
- data/ext/iodine/fio_siphash.h +18 -0
- data/ext/iodine/fio_tmpfile.h +38 -0
- data/ext/iodine/fiobj.h +24 -7
- data/ext/iodine/fiobj4sock.h +23 -0
- data/ext/iodine/fiobj_ary.c +143 -226
- data/ext/iodine/fiobj_ary.h +17 -16
- data/ext/iodine/fiobj_data.c +1160 -0
- data/ext/iodine/fiobj_data.h +164 -0
- data/ext/iodine/fiobj_hash.c +298 -406
- data/ext/iodine/fiobj_hash.h +101 -54
- data/ext/iodine/fiobj_json.c +478 -601
- data/ext/iodine/fiobj_json.h +34 -9
- data/ext/iodine/fiobj_numbers.c +383 -51
- data/ext/iodine/fiobj_numbers.h +87 -11
- data/ext/iodine/fiobj_str.c +423 -184
- data/ext/iodine/fiobj_str.h +81 -32
- data/ext/iodine/fiobject.c +273 -522
- data/ext/iodine/fiobject.h +477 -112
- data/ext/iodine/http.c +2243 -83
- data/ext/iodine/http.h +842 -121
- data/ext/iodine/http1.c +810 -385
- data/ext/iodine/http1.h +16 -39
- data/ext/iodine/http1_parser.c +146 -74
- data/ext/iodine/http1_parser.h +15 -4
- data/ext/iodine/http_internal.c +1258 -0
- data/ext/iodine/http_internal.h +226 -0
- data/ext/iodine/http_mime_parser.h +341 -0
- data/ext/iodine/iodine.c +86 -68
- data/ext/iodine/iodine.h +26 -11
- data/ext/iodine/iodine_helpers.c +8 -7
- data/ext/iodine/iodine_http.c +487 -324
- data/ext/iodine/iodine_json.c +304 -0
- data/ext/iodine/iodine_json.h +6 -0
- data/ext/iodine/iodine_protocol.c +107 -45
- data/ext/iodine/iodine_pubsub.c +526 -225
- data/ext/iodine/iodine_pubsub.h +10 -0
- data/ext/iodine/iodine_websockets.c +268 -510
- data/ext/iodine/iodine_websockets.h +2 -4
- data/ext/iodine/pubsub.c +726 -432
- data/ext/iodine/pubsub.h +85 -103
- data/ext/iodine/rb-call.c +4 -4
- data/ext/iodine/rb-defer.c +46 -22
- data/ext/iodine/rb-fiobj2rb.h +117 -0
- data/ext/iodine/rb-rack-io.c +73 -238
- data/ext/iodine/rb-rack-io.h +2 -2
- data/ext/iodine/rb-registry.c +35 -93
- data/ext/iodine/rb-registry.h +1 -0
- data/ext/iodine/redis_engine.c +742 -304
- data/ext/iodine/redis_engine.h +42 -39
- data/ext/iodine/resp_parser.h +311 -0
- data/ext/iodine/sock.c +627 -490
- data/ext/iodine/sock.h +345 -297
- data/ext/iodine/spnlock.inc +15 -4
- data/ext/iodine/websocket_parser.h +16 -20
- data/ext/iodine/websockets.c +188 -257
- data/ext/iodine/websockets.h +24 -133
- data/lib/iodine.rb +52 -7
- data/lib/iodine/cli.rb +6 -24
- data/lib/iodine/json.rb +40 -0
- data/lib/iodine/version.rb +1 -1
- data/lib/iodine/websocket.rb +5 -3
- data/lib/rack/handler/iodine.rb +58 -13
- metadata +38 -48
- data/bin/ws-shootout +0 -107
- data/examples/broadcast.ru +0 -56
- data/ext/iodine/bscrypt-common.h +0 -116
- data/ext/iodine/bscrypt.h +0 -49
- data/ext/iodine/fio2resp.c +0 -60
- data/ext/iodine/fio2resp.h +0 -51
- data/ext/iodine/fio_dict.c +0 -446
- data/ext/iodine/fio_dict.h +0 -99
- data/ext/iodine/fio_hash_table.h +0 -370
- data/ext/iodine/fio_list.h +0 -111
- data/ext/iodine/fiobj_internal.h +0 -280
- data/ext/iodine/fiobj_primitives.c +0 -131
- data/ext/iodine/fiobj_primitives.h +0 -55
- data/ext/iodine/fiobj_sym.c +0 -135
- data/ext/iodine/fiobj_sym.h +0 -60
- data/ext/iodine/hex.c +0 -124
- data/ext/iodine/hex.h +0 -70
- data/ext/iodine/http1_request.c +0 -81
- data/ext/iodine/http1_request.h +0 -58
- data/ext/iodine/http1_response.c +0 -417
- data/ext/iodine/http1_response.h +0 -95
- data/ext/iodine/http_request.c +0 -111
- data/ext/iodine/http_request.h +0 -102
- data/ext/iodine/http_response.c +0 -1703
- data/ext/iodine/http_response.h +0 -250
- data/ext/iodine/misc.c +0 -182
- data/ext/iodine/misc.h +0 -74
- data/ext/iodine/random.c +0 -208
- data/ext/iodine/redis_connection.c +0 -278
- data/ext/iodine/redis_connection.h +0 -86
- data/ext/iodine/resp.c +0 -842
- data/ext/iodine/resp.h +0 -261
- data/ext/iodine/siphash.c +0 -154
- data/ext/iodine/siphash.h +0 -22
- data/ext/iodine/xor-crypt.c +0 -193
- data/ext/iodine/xor-crypt.h +0 -107
data/examples/echo.ru
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
# This is a Websocket echo application.
|
2
2
|
#
|
3
|
-
# Running this application from the command line is
|
3
|
+
# Running this application from the command line is easy with:
|
4
4
|
#
|
5
|
-
# iodine
|
5
|
+
# iodine echo.ru
|
6
6
|
#
|
7
7
|
# Or, in single thread and single process:
|
8
8
|
#
|
9
|
-
# iodine -t 1 -w 1
|
9
|
+
# iodine -t 1 -w 1 echo.ru
|
10
10
|
#
|
11
11
|
# Benchmark with `ab` or `wrk` (a 5 seconds benchmark with 2000 concurrent clients):
|
12
12
|
#
|
@@ -29,8 +29,8 @@ module MyHTTPRouter
|
|
29
29
|
# this is function will be called by the Rack server (iodine) for every request.
|
30
30
|
def self.call env
|
31
31
|
# check if this is an upgrade request.
|
32
|
-
if(env['upgrade
|
33
|
-
env['upgrade
|
32
|
+
if(env['rack.upgrade?'.freeze] == :websocket)
|
33
|
+
env['rack.upgrade'.freeze] = WebsocketEcho
|
34
34
|
return WS_RESPONSE
|
35
35
|
end
|
36
36
|
# simply return the RESPONSE object, no matter what request was received.
|
@@ -42,7 +42,6 @@ end
|
|
42
42
|
class WebsocketEcho
|
43
43
|
# seng a message to new clients.
|
44
44
|
def on_open
|
45
|
-
p self
|
46
45
|
write "Welcome to our echo service!"
|
47
46
|
end
|
48
47
|
# send a message, letting the client know the server is suggunt down.
|
data/examples/hello.ru
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# This is a simple Hello World Rack application
|
2
2
|
#
|
3
|
-
# Running this application from the command line is
|
3
|
+
# Running this application from the command line is easy with:
|
4
4
|
#
|
5
5
|
# iodine hello.ru
|
6
6
|
#
|
@@ -16,8 +16,7 @@
|
|
16
16
|
module HelloWorld
|
17
17
|
# This is the HTTP response object according to the Rack specification.
|
18
18
|
RESPONSE = [200, { 'Content-Type' => 'text/html',
|
19
|
-
'Content-Length' => '
|
20
|
-
['Please connect using websockets.']]
|
19
|
+
'Content-Length' => '12' }, [ 'Hello World!' ] ]
|
21
20
|
|
22
21
|
# this is function will be called by the Rack server (iodine) for every request.
|
23
22
|
def self.call env
|
data/examples/info.md
ADDED
@@ -0,0 +1,316 @@
|
|
1
|
+
# Ruby's Rack Push: Decoupling the real-time web application from the web
|
2
|
+
|
3
|
+
Something exciting is coming.
|
4
|
+
|
5
|
+
Everyone is talking about WebSockets and their older cousin EventSource / Server Sent Events (SSE). Faye and ActionCable are all the rage and real-time updates are becoming easier than ever.
|
6
|
+
|
7
|
+
But it's all a mess. It's hard to set up, it's hard to maintain. The performance is meh. In short, the existing design is expensive - it's expensive in developer hours and it's expensive in hardware costs.
|
8
|
+
|
9
|
+
However, [a new PR in the Rack repository](https://github.com/rack/rack/pull/1272) promises to change all that in the near future.
|
10
|
+
|
11
|
+
This PR is a huge step towards simplifying our code base, improving real-time performance and lowering the overall cost of real-time web applications.
|
12
|
+
|
13
|
+
In a sentence, it's an important step towards [decoupling](https://softwareengineering.stackexchange.com/a/244478/224017) the web application from the web.
|
14
|
+
|
15
|
+
Remember, Rack is the interface Ruby frameworks (such and Rails and Sinatra) and web applications use to communicate with the Ruby application servers. It's everywhere. So this is a big deal.
|
16
|
+
|
17
|
+
## The Problem in a Nutshell
|
18
|
+
|
19
|
+
The problem with the current standard approach, in a nutshell, is that each real-time application process has to run two servers in order to support real-time functionality.
|
20
|
+
|
21
|
+
The two servers might be listening on the same port, they might be hidden away in some gem, but at the end of the day, two different IO event handling units have to run side by side.
|
22
|
+
|
23
|
+
"Why?" you might ask. Well, since you asked, I'll tell you (if you didn't ask, skip to the solution).
|
24
|
+
|
25
|
+
### The story of the temporary `hijack`
|
26
|
+
|
27
|
+
This is the story of a quick temporary solution coming up on it's 5th year as the only "standard" Rack solution available.
|
28
|
+
|
29
|
+
At some point in our history, the Rack specification needed a way to support long polling and other HTTP techniques. Specifically, Rails 4.0 needed something for their "live stream" feature.
|
30
|
+
|
31
|
+
For this purpose, [the Rack team came up with the `hijack` API approach](https://github.com/rack/rack/pull/481#issue-9702395).
|
32
|
+
|
33
|
+
This approach allowed for a quick fix to a pressing need. was meant to be temporary, something quick until Rack 2.0 was released (5 years later, the Rack protocol is still at version 1.3).
|
34
|
+
|
35
|
+
The `hijack` API offers applications complete control of the socket. Just hijack the socket away from the server and voilá, instant long polling / SSE support... sort of.
|
36
|
+
|
37
|
+
That's where things started to get messy.
|
38
|
+
|
39
|
+
To handle the (now "free") socket, a lot of network logic had to be copied from the server layer to the application layer (buffering `write` calls, handling incoming data, protocol management, timeout handling, etc').
|
40
|
+
|
41
|
+
This is an obvious violation of the "**S**" in S.O.L.I.D (single responsibility), as it adds IO handling responsibilities to the application / framework.
|
42
|
+
|
43
|
+
It also violates the DRY principle, since the IO handling logic is now duplicated (once within the server and once within the application / framework).
|
44
|
+
|
45
|
+
Additionally, this approach has issues with HTTP/2 connections, since the network protocol and the application are now entangled.
|
46
|
+
|
47
|
+
### The obvious `hijack` price
|
48
|
+
|
49
|
+
The `hijack` approach has many costs, some hidden, some more obvious.
|
50
|
+
|
51
|
+
The most easily observed price is memory, performance and developer hours.
|
52
|
+
|
53
|
+
Due to code duplication and extra work, the memory consumption for `hijack` based solutions is higher and their performance is slower (more system calls, more context switches, etc').
|
54
|
+
|
55
|
+
Using `require 'faye'` will add WebSockets to your application, but it will take almost 9Mb just to load the gem (this is before any actual work was performed).
|
56
|
+
|
57
|
+
On the other hand, using the `agoo` or `iodine` HTTP servers will add both WebScokets and SSE to your application without any extra memory consumption.
|
58
|
+
|
59
|
+
To be more specific, using `iodine` will consume about 2Mb of memory, less than Puma, while providing both HTTP and real-time capabilities.
|
60
|
+
|
61
|
+
### The hidden `hijack` price
|
62
|
+
|
63
|
+
A more subtle price is higher hardware costs and a lower clients-per-machine ratio when using `hijack`.
|
64
|
+
|
65
|
+
Why?
|
66
|
+
|
67
|
+
Besides the degraded performance, the `hijack` approach allows some HTTP servers to lean on the `select` system call, (Puma used `select` last time I took a look).
|
68
|
+
|
69
|
+
This system call [breaks down at around the 1024 open file limit](http://man7.org/linux/man-pages/man2/select.2.html#BUGS), possibly limiting each process to 1024 open connections.
|
70
|
+
|
71
|
+
When a connection is hijacked, the sockets don't close as fast as the web server expects, eventually leading to breakage and possible crashes if the 1024 open file limit is exceeded.
|
72
|
+
|
73
|
+
## The Solution - Callbacks and Events
|
74
|
+
|
75
|
+
[The new proposed Rack Push PR](https://github.com/rack/rack/pull/1272) offers a wonderful effective way to implement WebSockets and SSE while allowing an application to remain totally server agnostic.
|
76
|
+
|
77
|
+
This new proposal leaves the responsibility for the network / IO handling with the server, simplifying the application's code base and decoupling it from the network protocol.
|
78
|
+
|
79
|
+
By using a callback object, the application is notified of any events. Leaving the application free to focus on the data rather than the network layer.
|
80
|
+
|
81
|
+
The callback object doesn't even need to know anything about the server running the application or the underlying protocol.
|
82
|
+
|
83
|
+
The callback object is automatically linked to the correct API using Ruby's `extend` approach, allowing the application to remain totally server agnostic.
|
84
|
+
|
85
|
+
### How it works
|
86
|
+
|
87
|
+
Every Rack server uses a Hash type object to communicate with a Rack application.
|
88
|
+
|
89
|
+
This is how Rails is built, this is how Sinatra is built and this is how every Rack application / framework is built. It's in [the current Rack specification](https://github.com/rack/rack/blob/master/SPEC).
|
90
|
+
|
91
|
+
A simple Hello world using Rack would look like this (placed in a file called `config.ru`):
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
# normal HTTP response
|
95
|
+
RESPONSE = [200, { 'Content-Type' => 'text/html',
|
96
|
+
'Content-Length' => '12' }, [ 'Hello World!' ] ]
|
97
|
+
# note the `env` variable
|
98
|
+
APP = Proc.new {|env| RESPONSE }
|
99
|
+
# The Rack DSL used to run the application
|
100
|
+
run APP
|
101
|
+
```
|
102
|
+
|
103
|
+
This new proposal introduces the `env['rack.upgrade?']` variable.
|
104
|
+
|
105
|
+
Normally, this variable is set to `nil` (or missing from the `env` Hash).
|
106
|
+
|
107
|
+
However, for WebSocket connection, the `env['rack.upgrade?']` variable is set to `:websocket` and for EventSource (SSE) connections the variable is set to `:sse`.
|
108
|
+
|
109
|
+
To set a callback object, the `env['rack.upgrade']` is introduced (notice the missing question mark).
|
110
|
+
|
111
|
+
Now the design might look like this:
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
# place in config.ru
|
115
|
+
RESPONSE = [200, { 'Content-Type' => 'text/html',
|
116
|
+
'Content-Length' => '12' }, [ 'Hello World!' ] ]
|
117
|
+
# a Callback class
|
118
|
+
class MyCallbacks
|
119
|
+
def on_open
|
120
|
+
puts "* Push connection opened."
|
121
|
+
end
|
122
|
+
def on_message data
|
123
|
+
puts "* Incoming data: #{data}"
|
124
|
+
end
|
125
|
+
def on_close
|
126
|
+
puts "* Push connection closed."
|
127
|
+
end
|
128
|
+
end
|
129
|
+
# note the `env` variable
|
130
|
+
APP = Proc.new do |env|
|
131
|
+
if(env['rack.upgrade?'])
|
132
|
+
env['rack.upgrade'] = MyCallbacks.new
|
133
|
+
[0, {}, []]
|
134
|
+
else
|
135
|
+
RESPONSE
|
136
|
+
end
|
137
|
+
end
|
138
|
+
# The Rack DSL used to run the application
|
139
|
+
run APP
|
140
|
+
```
|
141
|
+
|
142
|
+
Run this application with the Agoo or Iodine servers and let the magic sparkle.
|
143
|
+
|
144
|
+
For example, using Iodine:
|
145
|
+
|
146
|
+
```bash
|
147
|
+
# install iodine
|
148
|
+
gem install iodine
|
149
|
+
# start in single threaded mode
|
150
|
+
iodine -t 1
|
151
|
+
```
|
152
|
+
|
153
|
+
Now open the browser, visit [localhost:3000](http://localhost:3000) and open the browser console to test some Javascript.
|
154
|
+
|
155
|
+
First try an EventSource (SSE) connection (run in browser console):
|
156
|
+
|
157
|
+
```js
|
158
|
+
// An SSE example
|
159
|
+
var source = new EventSource("/");
|
160
|
+
source.onmessage = function(msg) {
|
161
|
+
console.log(msg.id);
|
162
|
+
console.log(msg.data);
|
163
|
+
};
|
164
|
+
```
|
165
|
+
|
166
|
+
Sweet! nothing happened just yet (we aren't sending notifications), but we have an open SSE connection!
|
167
|
+
|
168
|
+
What about WebSockets (run in browser console):
|
169
|
+
|
170
|
+
```js
|
171
|
+
// A WebSocket example
|
172
|
+
ws = new WebSocket("ws://localhost:3000/");
|
173
|
+
ws.onmessage = function(e) { console.log(e.data); };
|
174
|
+
ws.onclose = function(e) { console.log("closed"); };
|
175
|
+
ws.onopen = function(e) { e.target.send("Hi!"); };
|
176
|
+
|
177
|
+
```
|
178
|
+
|
179
|
+
Wow! Did you look at the Ruby console - we have working WebSockets, it's that easy.
|
180
|
+
|
181
|
+
And this same example will run perfectly using the Agoo server as well (both Agoo and Iodine already support the Rack Push proposal).
|
182
|
+
|
183
|
+
Try it:
|
184
|
+
|
185
|
+
```bash
|
186
|
+
# install the agoo server
|
187
|
+
gem install agoo
|
188
|
+
# start it up
|
189
|
+
rackup -s agoo -p 3000
|
190
|
+
```
|
191
|
+
|
192
|
+
Notice, no gems, no extra code, no huge memory consumption, just the Ruby server and raw Rack (I didn't even use a framework just yet).
|
193
|
+
|
194
|
+
### The amazing push
|
195
|
+
|
196
|
+
So far, it's so simple, it's hard to notice how powerful this is.
|
197
|
+
|
198
|
+
Consider implementing a stock ticker, or in this case, a timer:
|
199
|
+
|
200
|
+
```ruby
|
201
|
+
# place in config.ru
|
202
|
+
RESPONSE = [200, { 'Content-Type' => 'text/html',
|
203
|
+
'Content-Length' => '12' }, [ 'Hello World!' ] ]
|
204
|
+
|
205
|
+
# A live connection storage
|
206
|
+
module LiveList
|
207
|
+
@list = []
|
208
|
+
@lock = Mutex.new
|
209
|
+
def self.<<(connection)
|
210
|
+
@lock.synchronize { @list << connection }
|
211
|
+
end
|
212
|
+
def self.>>(connection)
|
213
|
+
@lock.synchronize { @list.delete connection }
|
214
|
+
end
|
215
|
+
def self.broadcast(data)
|
216
|
+
@lock.synchronize {
|
217
|
+
@list.each {|c| c.write data }
|
218
|
+
}
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
# a Callback class
|
223
|
+
class MyCallbacks
|
224
|
+
def on_open
|
225
|
+
# add connection to the "live list"
|
226
|
+
LiveList << self
|
227
|
+
end
|
228
|
+
def on_message(data)
|
229
|
+
# Just an example broadcast
|
230
|
+
LiveList.broadcast "Special Announcement: #{data}"
|
231
|
+
end
|
232
|
+
def on_close
|
233
|
+
# remove connection to the "live list"
|
234
|
+
LiveList >> self
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
# Broadcast the time very second
|
239
|
+
Thread.new do
|
240
|
+
while(true) do
|
241
|
+
sleep(1)
|
242
|
+
LiveList.broadcast "The time is: #{Time.now}"
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# The Rack application
|
247
|
+
APP = Proc.new do |env|
|
248
|
+
if(env['rack.upgrade?'])
|
249
|
+
env['rack.upgrade'] = MyCallbacks.new
|
250
|
+
[0, {}, []]
|
251
|
+
else
|
252
|
+
RESPONSE
|
253
|
+
end
|
254
|
+
end
|
255
|
+
# The Rack DSL used to run the application
|
256
|
+
run APP
|
257
|
+
```
|
258
|
+
|
259
|
+
For this next example, I will use Iodine's pub/sub extension API to demonstrate the power offered by the new `env['rack.upgrade']` approach. This avoids the LiveList object and will make scaling easier.
|
260
|
+
|
261
|
+
Here is a simple chat room, but in this case I limit the interaction to WebSocket client. Why? because I can.
|
262
|
+
|
263
|
+
```ruby
|
264
|
+
# place in config.ru
|
265
|
+
RESPONSE = [200, { 'Content-Type' => 'text/html',
|
266
|
+
'Content-Length' => '12' }, [ 'Hello World!' ] ]
|
267
|
+
# a Callback class
|
268
|
+
class MyCallbacks
|
269
|
+
def initialize env
|
270
|
+
@name = env["PATH_INFO"][1..-1]
|
271
|
+
@name = "unknown" if(@name.length == 0)
|
272
|
+
end
|
273
|
+
def on_open
|
274
|
+
subscribe :chat
|
275
|
+
publish :chat, "#{@name} joined the chat."
|
276
|
+
end
|
277
|
+
def on_message data
|
278
|
+
publish :chat, "#{@name}: #{data}"
|
279
|
+
end
|
280
|
+
def on_close
|
281
|
+
publish :chat, "#{@name} left the chat."
|
282
|
+
end
|
283
|
+
end
|
284
|
+
# note the `env` variable
|
285
|
+
APP = Proc.new do |env|
|
286
|
+
if(env['rack.upgrade?'] == :websocket)
|
287
|
+
env['rack.upgrade'] = MyCallbacks.new(env)
|
288
|
+
[0, {}, []]
|
289
|
+
else
|
290
|
+
RESPONSE
|
291
|
+
end
|
292
|
+
end
|
293
|
+
# The Rack DSL used to run the application
|
294
|
+
run APP
|
295
|
+
```
|
296
|
+
|
297
|
+
Now try (in the browser console):
|
298
|
+
|
299
|
+
```js
|
300
|
+
ws = new WebSocket("ws://localhost:3000/Mitchel");
|
301
|
+
ws.onmessage = function(e) { console.log(e.data); };
|
302
|
+
ws.onclose = function(e) { console.log("Closed"); };
|
303
|
+
ws.onopen = function(e) { e.target.send("Yo!"); };
|
304
|
+
```
|
305
|
+
|
306
|
+
### Why didn't anyone think of this sooner?
|
307
|
+
|
308
|
+
Actually, this isn't a completely new idea.
|
309
|
+
|
310
|
+
Evens as the `hijack` API itself was suggested, [an alternative approach was suggested](https://github.com/rack/rack/pull/481#issuecomment-11916942).
|
311
|
+
|
312
|
+
Another proposal was attempted [a few years ago](https://github.com/rack/rack/issues/1093).
|
313
|
+
|
314
|
+
But it seems things are finally going to change, as two high performance server, [agoo](https://github.com/ohler55/agoo) and [iodine](https://github.com/boazsegev/iodine) already support this new approach.
|
315
|
+
|
316
|
+
Things look promising.
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# This example implements a custom (noop) pub/sub engine according to the Iodine::PubSub::Engine specifications.
|
2
|
+
#
|
3
|
+
require 'uri'
|
4
|
+
require 'iodine'
|
5
|
+
|
6
|
+
# creates an example Pub/Sub Engine that merely reports any pub/sub events to the system's terminal
|
7
|
+
class PubSubReporter < Iodine::PubSub::Engine
|
8
|
+
def initialize
|
9
|
+
# make sure engine setup is complete
|
10
|
+
super
|
11
|
+
# register engine and make it into the new default
|
12
|
+
@target = Iodine::PubSub.default_engine
|
13
|
+
Iodine::PubSub.default_engine = self
|
14
|
+
Iodine::PubSub.register self
|
15
|
+
end
|
16
|
+
def subscribe to, as = nil
|
17
|
+
puts "* Subscribing to \"#{to}\" (#{as || "exact match"})"
|
18
|
+
end
|
19
|
+
def unsubscribe to, as = nil
|
20
|
+
puts "* Unsubscribing to \"#{to}\" (#{as || "exact match"})"
|
21
|
+
end
|
22
|
+
def publish to, msg
|
23
|
+
puts "* Publishing to \"#{to}\": #{msg.to_s[0..6]}..."
|
24
|
+
# we need to forward the message to the actual engine (the previous default engine),
|
25
|
+
# or it will never be received by any Pub/Sub client.
|
26
|
+
@target.publish to, msg
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
PubSubReporter.new
|
31
|
+
|
32
|
+
# A simple router - Checks for Websocket Upgrade and answers HTTP.
|
33
|
+
module MyHTTPRouter
|
34
|
+
# This is the HTTP response object according to the Rack specification.
|
35
|
+
HTTP_RESPONSE = [200, { 'Content-Type' => 'text/html',
|
36
|
+
'Content-Length' => '32' },
|
37
|
+
['Please connect using websockets.']]
|
38
|
+
|
39
|
+
WS_RESPONSE = [0, {}, []]
|
40
|
+
|
41
|
+
# this is function will be called by the Rack server (iodine) for every request.
|
42
|
+
def self.call env
|
43
|
+
# check if this is an upgrade request.
|
44
|
+
if(env['rack.upgrade?'.freeze])
|
45
|
+
puts "SSE connections will not be able te send data, just listen." if(env['rack.upgrade?'.freeze] == :sse)
|
46
|
+
env['rack.upgrade'.freeze] = PubSubClient.new(env['PATH_INFO'] && env['PATH_INFO'].length > 1 ? env['PATH_INFO'][1..-1] : "guest")
|
47
|
+
return WS_RESPONSE
|
48
|
+
end
|
49
|
+
# simply return the RESPONSE object, no matter what request was received.
|
50
|
+
HTTP_RESPONSE
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# A simple Websocket Callback Object.
|
55
|
+
class PubSubClient
|
56
|
+
def initialize name
|
57
|
+
@name = name
|
58
|
+
end
|
59
|
+
# seng a message to new clients.
|
60
|
+
def on_open
|
61
|
+
subscribe "chat"
|
62
|
+
# let everyone know we arrived
|
63
|
+
publish "chat", "#{@name} entered the chat."
|
64
|
+
end
|
65
|
+
# send a message, letting the client know the server is suggunt down.
|
66
|
+
def on_shutdown
|
67
|
+
write "Server shutting down. Goodbye."
|
68
|
+
end
|
69
|
+
# perform the echo
|
70
|
+
def on_message data
|
71
|
+
publish "chat", "#{@name}: #{data}"
|
72
|
+
end
|
73
|
+
def on_close
|
74
|
+
# let everyone know we left
|
75
|
+
publish "chat", "#{@name} left the chat."
|
76
|
+
# we don't need to unsubscribe, subscriptions are cleared automatically once the connection is closed.
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# this function call links our HelloWorld application with Rack
|
81
|
+
run MyHTTPRouter
|