rage-iodine 1.7.58
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
- data/.github/workflows/ruby.yml +42 -0
- data/.gitignore +20 -0
- data/.rspec +2 -0
- data/.yardopts +8 -0
- data/CHANGELOG.md +1098 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +21 -0
- data/LIMITS.md +41 -0
- data/README.md +782 -0
- data/Rakefile +23 -0
- data/SPEC-PubSub-Draft.md +159 -0
- data/SPEC-WebSocket-Draft.md +239 -0
- data/bin/console +22 -0
- data/bin/info.md +353 -0
- data/bin/mustache_bench.rb +100 -0
- data/bin/poc/Gemfile.lock +23 -0
- data/bin/poc/README.md +37 -0
- data/bin/poc/config.ru +66 -0
- data/bin/poc/gemfile +1 -0
- data/bin/poc/www/index.html +57 -0
- data/examples/async_task.ru +92 -0
- data/examples/bates/README.md +3 -0
- data/examples/bates/config.ru +342 -0
- data/examples/bates/david+bold.pdf +0 -0
- data/examples/bates/public/drop-pdf.png +0 -0
- data/examples/bates/public/index.html +600 -0
- data/examples/config.ru +59 -0
- data/examples/echo.ru +59 -0
- data/examples/etag.ru +16 -0
- data/examples/hello.ru +29 -0
- data/examples/pubsub_engine.ru +81 -0
- data/examples/rack3.ru +12 -0
- data/examples/redis.ru +70 -0
- data/examples/shootout.ru +73 -0
- data/examples/sub-protocols.ru +90 -0
- data/examples/tcp_client.rb +66 -0
- data/examples/x-sendfile.ru +14 -0
- data/exe/iodine +280 -0
- data/ext/iodine/extconf.rb +110 -0
- data/ext/iodine/fio.c +12096 -0
- data/ext/iodine/fio.h +6390 -0
- data/ext/iodine/fio_cli.c +431 -0
- data/ext/iodine/fio_cli.h +189 -0
- data/ext/iodine/fio_json_parser.h +687 -0
- data/ext/iodine/fio_siphash.c +157 -0
- data/ext/iodine/fio_siphash.h +37 -0
- data/ext/iodine/fio_tls.h +129 -0
- data/ext/iodine/fio_tls_missing.c +649 -0
- data/ext/iodine/fio_tls_openssl.c +1056 -0
- data/ext/iodine/fio_tmpfile.h +50 -0
- data/ext/iodine/fiobj.h +44 -0
- data/ext/iodine/fiobj4fio.h +21 -0
- data/ext/iodine/fiobj_ary.c +333 -0
- data/ext/iodine/fiobj_ary.h +139 -0
- data/ext/iodine/fiobj_data.c +1185 -0
- data/ext/iodine/fiobj_data.h +167 -0
- data/ext/iodine/fiobj_hash.c +409 -0
- data/ext/iodine/fiobj_hash.h +176 -0
- data/ext/iodine/fiobj_json.c +622 -0
- data/ext/iodine/fiobj_json.h +68 -0
- data/ext/iodine/fiobj_mem.h +71 -0
- data/ext/iodine/fiobj_mustache.c +317 -0
- data/ext/iodine/fiobj_mustache.h +62 -0
- data/ext/iodine/fiobj_numbers.c +344 -0
- data/ext/iodine/fiobj_numbers.h +127 -0
- data/ext/iodine/fiobj_str.c +433 -0
- data/ext/iodine/fiobj_str.h +172 -0
- data/ext/iodine/fiobject.c +620 -0
- data/ext/iodine/fiobject.h +654 -0
- data/ext/iodine/hpack.h +1923 -0
- data/ext/iodine/http.c +2736 -0
- data/ext/iodine/http.h +1019 -0
- data/ext/iodine/http1.c +825 -0
- data/ext/iodine/http1.h +29 -0
- data/ext/iodine/http1_parser.h +1835 -0
- data/ext/iodine/http_internal.c +1279 -0
- data/ext/iodine/http_internal.h +248 -0
- data/ext/iodine/http_mime_parser.h +350 -0
- data/ext/iodine/iodine.c +1433 -0
- data/ext/iodine/iodine.h +64 -0
- data/ext/iodine/iodine_caller.c +218 -0
- data/ext/iodine/iodine_caller.h +27 -0
- data/ext/iodine/iodine_connection.c +941 -0
- data/ext/iodine/iodine_connection.h +55 -0
- data/ext/iodine/iodine_defer.c +420 -0
- data/ext/iodine/iodine_defer.h +6 -0
- data/ext/iodine/iodine_fiobj2rb.h +120 -0
- data/ext/iodine/iodine_helpers.c +282 -0
- data/ext/iodine/iodine_helpers.h +12 -0
- data/ext/iodine/iodine_http.c +1280 -0
- data/ext/iodine/iodine_http.h +23 -0
- data/ext/iodine/iodine_json.c +302 -0
- data/ext/iodine/iodine_json.h +6 -0
- data/ext/iodine/iodine_mustache.c +567 -0
- data/ext/iodine/iodine_mustache.h +6 -0
- data/ext/iodine/iodine_pubsub.c +580 -0
- data/ext/iodine/iodine_pubsub.h +26 -0
- data/ext/iodine/iodine_rack_io.c +273 -0
- data/ext/iodine/iodine_rack_io.h +20 -0
- data/ext/iodine/iodine_store.c +142 -0
- data/ext/iodine/iodine_store.h +20 -0
- data/ext/iodine/iodine_tcp.c +346 -0
- data/ext/iodine/iodine_tcp.h +13 -0
- data/ext/iodine/iodine_tls.c +261 -0
- data/ext/iodine/iodine_tls.h +13 -0
- data/ext/iodine/mustache_parser.h +1546 -0
- data/ext/iodine/redis_engine.c +957 -0
- data/ext/iodine/redis_engine.h +79 -0
- data/ext/iodine/resp_parser.h +317 -0
- data/ext/iodine/scheduler.c +173 -0
- data/ext/iodine/scheduler.h +6 -0
- data/ext/iodine/websocket_parser.h +506 -0
- data/ext/iodine/websockets.c +752 -0
- data/ext/iodine/websockets.h +185 -0
- data/iodine.gemspec +50 -0
- data/lib/iodine/connection.rb +61 -0
- data/lib/iodine/json.rb +42 -0
- data/lib/iodine/mustache.rb +113 -0
- data/lib/iodine/pubsub.rb +55 -0
- data/lib/iodine/rack_utils.rb +43 -0
- data/lib/iodine/tls.rb +16 -0
- data/lib/iodine/version.rb +3 -0
- data/lib/iodine.rb +274 -0
- data/lib/rack/handler/iodine.rb +33 -0
- data/logo.png +0 -0
- metadata +284 -0
data/bin/info.md
ADDED
@@ -0,0 +1,353 @@
|
|
1
|
+
# Ruby's Rack Push: Decoupling the real-time web application from the web
|
2
|
+
|
3
|
+
![](https://bowild.files.wordpress.com/2018/05/6865783407_84f470ec02_o.jpg)
|
4
|
+
|
5
|
+
Something exciting is coming.
|
6
|
+
|
7
|
+
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.
|
8
|
+
|
9
|
+
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.
|
10
|
+
|
11
|
+
However, [a new PR in the Rack repository](https://github.com/rack/rack/pull/1272) promises to change all that in the near future.
|
12
|
+
|
13
|
+
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.
|
14
|
+
|
15
|
+
In a sentence, it's an important step towards [decoupling](https://softwareengineering.stackexchange.com/a/244478/224017) the web application from the web.
|
16
|
+
|
17
|
+
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.
|
18
|
+
|
19
|
+
## The Problem in a Nutshell
|
20
|
+
|
21
|
+
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.
|
22
|
+
|
23
|
+
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.
|
24
|
+
|
25
|
+
"Why?" you might ask. Well, since you asked, I'll tell you (if you didn't ask, skip to the solution).
|
26
|
+
|
27
|
+
### The story of the temporary `hijack`
|
28
|
+
|
29
|
+
This is the story of a quick temporary solution coming up on it's 5th year as the only "standard" Rack solution available.
|
30
|
+
|
31
|
+
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.
|
32
|
+
|
33
|
+
For this purpose, [the Rack team came up with the `hijack` API approach](https://github.com/rack/rack/pull/481#issue-9702395).
|
34
|
+
|
35
|
+
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).
|
36
|
+
|
37
|
+
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.
|
38
|
+
|
39
|
+
That's where things started to get messy.
|
40
|
+
|
41
|
+
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').
|
42
|
+
|
43
|
+
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.
|
44
|
+
|
45
|
+
It also violates the DRY principle, since the IO handling logic is now duplicated (once within the server and once within the application / framework).
|
46
|
+
|
47
|
+
Additionally, this approach has issues with HTTP/2 connections, since the network protocol and the application are now entangled.
|
48
|
+
|
49
|
+
### The obvious `hijack` price
|
50
|
+
|
51
|
+
The `hijack` approach has many costs, some hidden, some more obvious.
|
52
|
+
|
53
|
+
The most easily observed price is memory, performance and developer hours.
|
54
|
+
|
55
|
+
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').
|
56
|
+
|
57
|
+
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).
|
58
|
+
|
59
|
+
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.
|
60
|
+
|
61
|
+
To be more specific, using `iodine` will consume about 2Mb of memory, marginally less than Puma, while providing both HTTP and real-time capabilities.
|
62
|
+
|
63
|
+
### The hidden `hijack` price
|
64
|
+
|
65
|
+
A more subtle price is higher hardware costs and a lower clients-per-machine ratio when using `hijack`.
|
66
|
+
|
67
|
+
Why?
|
68
|
+
|
69
|
+
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).
|
70
|
+
|
71
|
+
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.
|
72
|
+
|
73
|
+
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.
|
74
|
+
|
75
|
+
## The Solution - Callbacks and Events
|
76
|
+
|
77
|
+
[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.
|
78
|
+
|
79
|
+
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.
|
80
|
+
|
81
|
+
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.
|
82
|
+
|
83
|
+
The callback object doesn't even need to know anything about the server running the application or the underlying protocol.
|
84
|
+
|
85
|
+
~~The callback object is automatically linked to the correct API using Ruby's `extend` approach, allowing the application to remain totally server agnostic.~~ **EDIT**: the PR was updated, replacing the `extend` approach with an extra `client` object.
|
86
|
+
|
87
|
+
### How it works
|
88
|
+
|
89
|
+
Every Rack server uses a Hash type object to communicate with a Rack application.
|
90
|
+
|
91
|
+
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).
|
92
|
+
|
93
|
+
A simple Hello world using Rack would look like this (placed in a file called `config.ru`):
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
# normal HTTP response
|
97
|
+
RESPONSE = [200, { 'Content-Type' => 'text/html',
|
98
|
+
'Content-Length' => '12' }, [ 'Hello World!' ] ]
|
99
|
+
# note the `env` variable
|
100
|
+
APP = Proc.new {|env| RESPONSE }
|
101
|
+
# The Rack DSL used to run the application
|
102
|
+
run APP
|
103
|
+
```
|
104
|
+
|
105
|
+
This new proposal introduces the `env['rack.upgrade?']` variable.
|
106
|
+
|
107
|
+
Normally, this variable is set to `nil` (or missing from the `env` Hash).
|
108
|
+
|
109
|
+
However, for WebSocket connection, the `env['rack.upgrade?']` variable is set to `:websocket` and for EventSource (SSE) connections the variable is set to `:sse`.
|
110
|
+
|
111
|
+
To set a callback object, the `env['rack.upgrade']` is introduced (notice the *missing* question mark).
|
112
|
+
|
113
|
+
Now the design might look like this:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
# Place in config.ru
|
117
|
+
RESPONSE = [200, { 'Content-Type' => 'text/html',
|
118
|
+
'Content-Length' => '12' }, [ 'Hello World!' ] ]
|
119
|
+
# an example Callback class
|
120
|
+
class MyCallbacks
|
121
|
+
def on_open client
|
122
|
+
puts "* Push connection opened."
|
123
|
+
end
|
124
|
+
def on_message client, data
|
125
|
+
puts "* Incoming data: #{data}"
|
126
|
+
client.write "Roger that, \"#{data}\""
|
127
|
+
end
|
128
|
+
def on_close client
|
129
|
+
puts "* Push connection closed."
|
130
|
+
end
|
131
|
+
end
|
132
|
+
# note the `env` variable
|
133
|
+
APP = Proc.new do |env|
|
134
|
+
if(env['rack.upgrade?'])
|
135
|
+
env['rack.upgrade'] = MyCallbacks.new
|
136
|
+
[200, {}, []]
|
137
|
+
else
|
138
|
+
RESPONSE
|
139
|
+
end
|
140
|
+
end
|
141
|
+
# The Rack DSL used to run the application
|
142
|
+
run APP
|
143
|
+
```
|
144
|
+
|
145
|
+
Run this application with the Agoo or Iodine servers and let the magic sparkle.
|
146
|
+
|
147
|
+
For example, using Iodine:
|
148
|
+
|
149
|
+
```bash
|
150
|
+
# install iodine, version 0.6.0 and up
|
151
|
+
gem install iodine
|
152
|
+
# start in single threaded mode
|
153
|
+
iodine -t 1
|
154
|
+
```
|
155
|
+
|
156
|
+
Now open the browser, visit [localhost:3000](http://localhost:3000) and open the browser console to test some JavaScript.
|
157
|
+
|
158
|
+
First try an EventSource (SSE) connection (run in browser console):
|
159
|
+
|
160
|
+
```js
|
161
|
+
// An SSE example
|
162
|
+
var source = new EventSource("/");
|
163
|
+
source.onmessage = function(msg) {
|
164
|
+
console.log(msg.id);
|
165
|
+
console.log(msg.data);
|
166
|
+
};
|
167
|
+
```
|
168
|
+
|
169
|
+
Sweet! nothing happened just yet (we aren't sending notifications), but we have an open SSE connection!
|
170
|
+
|
171
|
+
What about WebSockets (run in browser console):
|
172
|
+
|
173
|
+
```js
|
174
|
+
// A WebSocket example
|
175
|
+
ws = new WebSocket("ws://localhost:3000/");
|
176
|
+
ws.onmessage = function(e) { console.log(e.data); };
|
177
|
+
ws.onclose = function(e) { console.log("closed"); };
|
178
|
+
ws.onopen = function(e) { e.target.send("Hi!"); };
|
179
|
+
|
180
|
+
```
|
181
|
+
|
182
|
+
Wow! Did you look at the Ruby console - we have working WebSockets, it's that easy.
|
183
|
+
|
184
|
+
And this same example will run perfectly using the Agoo server as well (both Agoo and Iodine already support the Rack Push proposal).
|
185
|
+
|
186
|
+
Try it:
|
187
|
+
|
188
|
+
```bash
|
189
|
+
# install the agoo server, version 2.1.0 and up
|
190
|
+
gem install agoo
|
191
|
+
# start it up
|
192
|
+
rackup -s agoo -p 3000
|
193
|
+
```
|
194
|
+
|
195
|
+
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).
|
196
|
+
|
197
|
+
### The amazing push
|
198
|
+
|
199
|
+
So far, it's so simple, it's hard to notice how powerful this is.
|
200
|
+
|
201
|
+
Consider implementing a stock ticker, or in this case, a timer:
|
202
|
+
|
203
|
+
```ruby
|
204
|
+
# Place in config.ru
|
205
|
+
RESPONSE = [200, { 'Content-Type' => 'text/html',
|
206
|
+
'Content-Length' => '12' }, [ 'Hello World!' ] ]
|
207
|
+
|
208
|
+
# A global live connection storage
|
209
|
+
module LiveList
|
210
|
+
@list = []
|
211
|
+
@lock = Mutex.new
|
212
|
+
def <<(connection)
|
213
|
+
@lock.synchronize { @list << connection }
|
214
|
+
end
|
215
|
+
def >>(connection)
|
216
|
+
@lock.synchronize { @list.delete connection }
|
217
|
+
end
|
218
|
+
def any?
|
219
|
+
# remove connection to the "live list"
|
220
|
+
@lock.synchronize { @list.any? }
|
221
|
+
end
|
222
|
+
# this will send a message to all the connections that share the same process.
|
223
|
+
# (in cluster mode we get partial broadcasting only and this doesn't scale)
|
224
|
+
def broadcast(data)
|
225
|
+
# copy the list so we don't perform long operations in the critical section
|
226
|
+
tmp = nil # place tmp in this part of the scope
|
227
|
+
@lock.synchronize do
|
228
|
+
tmp = @list.dup # copy list into tmp
|
229
|
+
end
|
230
|
+
# iterate list outside of critical section
|
231
|
+
tmp.each {|c| c.write data }
|
232
|
+
end
|
233
|
+
extend self
|
234
|
+
end
|
235
|
+
|
236
|
+
# Broadcast the time very second... but...
|
237
|
+
# Threads will BREAK in cluster mode.
|
238
|
+
@thread = Thread.new do
|
239
|
+
while(LiveList.any?) do
|
240
|
+
sleep(1)
|
241
|
+
LiveList.broadcast "The time is: #{Time.now}"
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# an example static Callback module
|
246
|
+
module MyCallbacks
|
247
|
+
def on_open client
|
248
|
+
# add connection to the "live list"
|
249
|
+
LiveList << client
|
250
|
+
end
|
251
|
+
def on_message(client, data)
|
252
|
+
# Just an example broadcast
|
253
|
+
LiveList.broadcast "Special Announcement: #{data}"
|
254
|
+
end
|
255
|
+
def on_close client
|
256
|
+
# remove connection to the "live list"
|
257
|
+
LiveList >> client
|
258
|
+
end
|
259
|
+
extend self
|
260
|
+
end
|
261
|
+
|
262
|
+
# The Rack application
|
263
|
+
APP = Proc.new do |env|
|
264
|
+
if(env['rack.upgrade?'])
|
265
|
+
env['rack.upgrade'] = MyCallbacks
|
266
|
+
[200, {}, []]
|
267
|
+
else
|
268
|
+
RESPONSE
|
269
|
+
end
|
270
|
+
end
|
271
|
+
# The Rack DSL used to run the application
|
272
|
+
run APP
|
273
|
+
```
|
274
|
+
|
275
|
+
Run the iodine server in single process mode: `iodine -w 1` and the little timer is ticking.
|
276
|
+
|
277
|
+
Honestly, I don't love the code I just wrote for the previous example. It's a little long, it's slightly iffy and we can't use iodine's cluster mode.
|
278
|
+
|
279
|
+
For my next example, I'll author a chat room in 32 lines (including comments).
|
280
|
+
|
281
|
+
I will use Iodine's pub/sub extension API to avoid the LiveList module and the timer thread. I don't want a timer, so I'll skip the [`Iodine.run_every` method](https://www.rubydoc.info/github/boazsegev/iodine/master/Iodine#run_every-class_method).
|
282
|
+
|
283
|
+
Also, I'll limit the interaction to WebSocket clients. Why? to show I can.
|
284
|
+
|
285
|
+
This will better demonstrate the power offered by the new `env['rack.upgrade']` approach and it will also work in cluster mode.
|
286
|
+
|
287
|
+
Sadly, this means that the example won't run on Agoo for now.
|
288
|
+
|
289
|
+
```ruby
|
290
|
+
# Place in config.ru
|
291
|
+
RESPONSE = [200, { 'Content-Type' => 'text/html',
|
292
|
+
'Content-Length' => '12' }, [ 'Hello World!' ] ]
|
293
|
+
CHAT = "chat".freeze
|
294
|
+
# a Callback class
|
295
|
+
class MyCallbacks
|
296
|
+
def initialize env
|
297
|
+
@name = env["PATH_INFO"][1..-1]
|
298
|
+
@name = "unknown" if(@name.length == 0)
|
299
|
+
end
|
300
|
+
def on_open client
|
301
|
+
client.subscribe CHAT
|
302
|
+
client.publish CHAT, "#{@name} joined the chat."
|
303
|
+
end
|
304
|
+
def on_message client, data
|
305
|
+
client.publish CHAT, "#{@name}: #{data}"
|
306
|
+
end
|
307
|
+
def on_close client
|
308
|
+
client.publish CHAT, "#{@name} left the chat."
|
309
|
+
end
|
310
|
+
end
|
311
|
+
# The actual Rack application
|
312
|
+
APP = Proc.new do |env|
|
313
|
+
if(env['rack.upgrade?'] == :websocket)
|
314
|
+
env['rack.upgrade'] = MyCallbacks.new(env)
|
315
|
+
[200, {}, []]
|
316
|
+
else
|
317
|
+
RESPONSE
|
318
|
+
end
|
319
|
+
end
|
320
|
+
# The Rack DSL used to run the application
|
321
|
+
run APP
|
322
|
+
```
|
323
|
+
|
324
|
+
Start the application from the command line (in terminal):
|
325
|
+
|
326
|
+
```bash
|
327
|
+
iodine
|
328
|
+
```
|
329
|
+
|
330
|
+
Now try (in the browser console):
|
331
|
+
|
332
|
+
```js
|
333
|
+
ws = new WebSocket("ws://localhost:3000/Mitchel");
|
334
|
+
ws.onmessage = function(e) { console.log(e.data); };
|
335
|
+
ws.onclose = function(e) { console.log("Closed"); };
|
336
|
+
ws.onopen = function(e) { e.target.send("Yo!"); };
|
337
|
+
```
|
338
|
+
|
339
|
+
**EDIT**: Agoo 2.1.0 now implements pub/sub extensions, albeit, using slightly different semantics. I did my best so the same code would work on both servers.
|
340
|
+
|
341
|
+
### Why didn't anyone think of this sooner?
|
342
|
+
|
343
|
+
Actually, this isn't a completely new idea.
|
344
|
+
|
345
|
+
Evens as the `hijack` API itself was suggested, [an alternative approach was suggested](https://github.com/rack/rack/pull/481#issuecomment-11916942).
|
346
|
+
|
347
|
+
Another proposal was attempted [a few years ago](https://github.com/rack/rack/issues/1093).
|
348
|
+
|
349
|
+
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.
|
350
|
+
|
351
|
+
Things look promising.
|
352
|
+
|
353
|
+
**UPDATE**: code examples were updated to reflect changes in theRack specification's PR.
|
@@ -0,0 +1,100 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'benchmark/ips'
|
4
|
+
require 'mustache'
|
5
|
+
require 'iodine'
|
6
|
+
|
7
|
+
def benchmark_mustache
|
8
|
+
# benchmark code was copied, in part, from:
|
9
|
+
# https://github.com/mustache/mustache/blob/master/benchmarks/render_collection_benchmark.rb
|
10
|
+
template = """
|
11
|
+
{{#products}}
|
12
|
+
<div class='product_brick'>
|
13
|
+
<div class='container'>
|
14
|
+
<div class='element'>
|
15
|
+
<img src='images/{{image}}' class='product_miniature' />
|
16
|
+
</div>
|
17
|
+
<div class='element description'>
|
18
|
+
<a href={{url}} class='product_name block bold'>
|
19
|
+
{{external_index}}
|
20
|
+
</a>
|
21
|
+
</div>
|
22
|
+
</div>
|
23
|
+
</div>
|
24
|
+
{{/products}}
|
25
|
+
"""
|
26
|
+
|
27
|
+
IO.write "test_template.mustache", template
|
28
|
+
filename = "test_template.mustache"
|
29
|
+
|
30
|
+
data_1 = {
|
31
|
+
products: [ {
|
32
|
+
:external_index=>"This <product> should've been \"properly\" escaped.",
|
33
|
+
:url=>"/products/7",
|
34
|
+
:image=>"products/product.jpg"
|
35
|
+
} ]
|
36
|
+
}
|
37
|
+
data_1000 = {
|
38
|
+
products: []
|
39
|
+
}
|
40
|
+
|
41
|
+
1000.times do
|
42
|
+
data_1000[:products] << {
|
43
|
+
:external_index=>"product",
|
44
|
+
:url=>"/products/7",
|
45
|
+
:image=>"products/product.jpg"
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
data_1000_escaped = {
|
50
|
+
products: []
|
51
|
+
}
|
52
|
+
|
53
|
+
1000.times do
|
54
|
+
data_1000_escaped[:products] << {
|
55
|
+
:external_index=>"This <product> should've been \"properly\" escaped.",
|
56
|
+
:url=>"/products/7",
|
57
|
+
:image=>"products/product.jpg"
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
view = Mustache.new
|
62
|
+
view.template = template
|
63
|
+
view.render # Call render once so the template will be compiled
|
64
|
+
iodine_view = Iodine::Mustache.new(filename)
|
65
|
+
|
66
|
+
puts "Ruby Mustache rendering (and HTML escaping) results in:",
|
67
|
+
view.render(data_1), "",
|
68
|
+
"Notice that Iodine::Mustache rendering (and HTML escaping) results in agressive escaping:",
|
69
|
+
iodine_view.render(data_1), "", "----"
|
70
|
+
|
71
|
+
# return;
|
72
|
+
|
73
|
+
Benchmark.ips do |x|
|
74
|
+
x.report("Ruby Mustache render list of 1000") do |times|
|
75
|
+
view.render(data_1000)
|
76
|
+
end
|
77
|
+
x.report("Iodine::Mustache render list of 1000") do |times|
|
78
|
+
iodine_view.render(data_1000)
|
79
|
+
end
|
80
|
+
|
81
|
+
x.report("Ruby Mustache render list of 1000 with escaped data") do |times|
|
82
|
+
view.render(data_1000_escaped)
|
83
|
+
end
|
84
|
+
x.report("Iodine::Mustache render list of 1000 with escaped data") do |times|
|
85
|
+
iodine_view.render(data_1000_escaped)
|
86
|
+
end
|
87
|
+
|
88
|
+
x.report("Ruby Mustache - no chaching - render list of 1000") do |times|
|
89
|
+
tmp = Mustache.new
|
90
|
+
tmp.template = template
|
91
|
+
tmp.render(data_1000)
|
92
|
+
end
|
93
|
+
x.report("Iodine::Mustache - no chaching - render list of 1000") do |times|
|
94
|
+
Iodine::Mustache.render(nil, data_1000, template)
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
benchmark_mustache
|
@@ -0,0 +1,23 @@
|
|
1
|
+
GIT
|
2
|
+
remote: https://github.com/boazsegev/iodine.git
|
3
|
+
revision: 8b57a5f2008b9c35f2cf589be7ed121823a84582
|
4
|
+
specs:
|
5
|
+
iodine (0.2.0)
|
6
|
+
rack
|
7
|
+
rake-compiler
|
8
|
+
|
9
|
+
GEM
|
10
|
+
specs:
|
11
|
+
rack (2.0.1)
|
12
|
+
rake (11.2.2)
|
13
|
+
rake-compiler (0.9.5)
|
14
|
+
rake
|
15
|
+
|
16
|
+
PLATFORMS
|
17
|
+
ruby
|
18
|
+
|
19
|
+
DEPENDENCIES
|
20
|
+
iodine!
|
21
|
+
|
22
|
+
BUNDLED WITH
|
23
|
+
1.12.5
|
data/bin/poc/README.md
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# A proof of concept for Rack's `env['rack.websocket']`
|
2
|
+
|
3
|
+
This is a proof of concept for Rack based Websocket connections, showing how the Rack API can be adjusted to support server native real-time connections.
|
4
|
+
|
5
|
+
The chosen proof of concept was the ugliest chatroom I could find.
|
6
|
+
|
7
|
+
Although my hope is that Rack will adopt the concept and make `env['rack.websocket?']` and `env['rack.websocket']` part of it's standard, at the moment it's an Iodine specific feature, implemented using `env['iodine.websocket']`.
|
8
|
+
|
9
|
+
## Install
|
10
|
+
|
11
|
+
Install required gems using:
|
12
|
+
|
13
|
+
```sh
|
14
|
+
bundler install
|
15
|
+
```
|
16
|
+
|
17
|
+
## Run
|
18
|
+
|
19
|
+
Run this application single threaded:
|
20
|
+
|
21
|
+
```sh
|
22
|
+
bundler exec iodine -- -www ./www
|
23
|
+
```
|
24
|
+
|
25
|
+
Or both multi-threaded and forked (you'll notice that memory barriers for forked processes prevent websocket broadcasting from reaching websockets connected to a different process).
|
26
|
+
|
27
|
+
```sh
|
28
|
+
bundler exec iodine -- -www ./www -t 16 -w 4
|
29
|
+
```
|
30
|
+
|
31
|
+
## Further reading
|
32
|
+
|
33
|
+
* https://github.com/rack/rack/issues/1093
|
34
|
+
|
35
|
+
* https://bowild.wordpress.com/2016/07/31/the-dark-side-of-the-rack
|
36
|
+
|
37
|
+
* https://github.com/boazsegev/iodine
|
data/bin/poc/config.ru
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# The Rack Application container
|
2
|
+
module MyRackApplication
|
3
|
+
# Rack applications use the `call` callback to handle HTTP requests.
|
4
|
+
def self.call(env)
|
5
|
+
# if upgrading...
|
6
|
+
if env['HTTP_UPGRADE'.freeze] =~ /websocket/i
|
7
|
+
# We can assign a class or an instance that implements callbacks.
|
8
|
+
# We will assign an object, passing it the request information (`env`)
|
9
|
+
env['iodine.websocket'.freeze] = MyWebsocket.new(env)
|
10
|
+
# Rack responses must be a 3 item array
|
11
|
+
# [status, {http: :headers}, ["response body"]]
|
12
|
+
return [0, {}, []]
|
13
|
+
end
|
14
|
+
# a semi-regualr HTTP response
|
15
|
+
out = File.open File.expand_path('../www/index.html', __FILE__)
|
16
|
+
[200, { 'X-Sendfile' => File.expand_path('../www/index.html', __FILE__),
|
17
|
+
'Content-Length' => out.size }, out]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# The Websocket Callback Object
|
22
|
+
class MyWebsocket
|
23
|
+
# this is optional, but I wanted the object to have the nickname provided in
|
24
|
+
# the HTTP request
|
25
|
+
def initialize(env)
|
26
|
+
# we need to change the ASCI Rack encoding to UTF-8,
|
27
|
+
# otherwise everything with the nickname will be a binary "blob" in the
|
28
|
+
# Javascript layer
|
29
|
+
@nickname = env['PATH_INFO'][1..-1].force_encoding 'UTF-8'
|
30
|
+
end
|
31
|
+
|
32
|
+
# A classic websocket callback, called when the connection is opened and
|
33
|
+
# linked to this object
|
34
|
+
def on_open
|
35
|
+
puts 'We have a websocket connection'
|
36
|
+
end
|
37
|
+
|
38
|
+
# A classic websocket callback, called when the connection is closed
|
39
|
+
# (after disconnection).
|
40
|
+
def on_close
|
41
|
+
puts "Bye Bye... #{count} connections left..."
|
42
|
+
end
|
43
|
+
|
44
|
+
# A server-side niceness, called when the server if shutting down,
|
45
|
+
# to gracefully disconnect (before disconnection).
|
46
|
+
def on_shutdown
|
47
|
+
write 'The server is shutting down, goodbye.'
|
48
|
+
end
|
49
|
+
|
50
|
+
def on_message(data)
|
51
|
+
puts "got message: #{data} encoded as #{data.encoding}"
|
52
|
+
# data is a temporary string, it's buffer cleared as soon as we return.
|
53
|
+
# So we make a copy with the desired format.
|
54
|
+
tmp = "#{@nickname}: #{data}"
|
55
|
+
# The `write` method was added by the server and writes to the current
|
56
|
+
# connection
|
57
|
+
write tmp
|
58
|
+
puts "broadcasting #{tmp.bytesize} bytes with encoding #{tmp.encoding}"
|
59
|
+
# `each` was added by the server and excludes this connection
|
60
|
+
# (each except self).
|
61
|
+
each { |h| h.write tmp }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# `run` is a Rack API command, telling Rack where the `call(env)` callback is located.
|
66
|
+
run MyRackApplication
|
data/bin/poc/gemfile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
gem 'iodine', git: 'https://github.com/boazsegev/iodine.git'
|
@@ -0,0 +1,57 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
|
5
|
+
<script>
|
6
|
+
ws = NaN
|
7
|
+
handle = ''
|
8
|
+
function onsubmit(e) {
|
9
|
+
e.preventDefault();
|
10
|
+
if($('#text')[0].value == '') {return false}
|
11
|
+
if(ws && ws.readyState == 1) {
|
12
|
+
ws.send($('#text')[0].value);
|
13
|
+
$('#text')[0].value = '';
|
14
|
+
} else {
|
15
|
+
handle = $('#text')[0].value
|
16
|
+
var url = (window.location.protocol.match(/https/) ? 'wss' : 'ws') +
|
17
|
+
'://' + window.document.location.host +
|
18
|
+
'/' + $('#text')[0].value
|
19
|
+
ws = new WebSocket(url)
|
20
|
+
ws.onopen = function(e) {
|
21
|
+
output("<b>Connected :-)</b>");
|
22
|
+
$('#text')[0].value = '';
|
23
|
+
$('#text')[0].placeholder = 'your message';
|
24
|
+
}
|
25
|
+
ws.onclose = function(e) {
|
26
|
+
output("<b>Disonnected :-/</b>")
|
27
|
+
$('#text')[0].value = '';
|
28
|
+
$('#text')[0].placeholder = 'nickname';
|
29
|
+
$('#text')[0].value = handle
|
30
|
+
}
|
31
|
+
ws.onmessage = function(e) {
|
32
|
+
output(e.data);
|
33
|
+
}
|
34
|
+
}
|
35
|
+
return false;
|
36
|
+
}
|
37
|
+
function output(data) {
|
38
|
+
$('#output').append("<li>" + data + "</li>")
|
39
|
+
$('#output').animate({ scrollTop:
|
40
|
+
$('#output')[0].scrollHeight }, "slow");
|
41
|
+
}
|
42
|
+
</script>
|
43
|
+
<style>
|
44
|
+
html, body {width:100%; height: 100%; background-color: #ddd; color: #111;}
|
45
|
+
h3, form {text-align: center;}
|
46
|
+
input {background-color: #fff; color: #111; padding: 0.3em;}
|
47
|
+
</style>
|
48
|
+
</head><body>
|
49
|
+
<h3>The Ugly Chatroom POC</h3>
|
50
|
+
<form id='form'>
|
51
|
+
<input type='text' id='text' name='text' placeholder='nickname'></input>
|
52
|
+
<input type='submit' value='send'></input>
|
53
|
+
</form>
|
54
|
+
<script> $('#form')[0].onsubmit = onsubmit </script>
|
55
|
+
<ul id='output'></ul>
|
56
|
+
</body>
|
57
|
+
</html>
|