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.

Files changed (146) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -2
  3. data/CHANGELOG.md +22 -0
  4. data/LIMITS.md +19 -9
  5. data/README.md +92 -77
  6. data/SPEC-PubSub-Draft.md +113 -0
  7. data/SPEC-Websocket-Draft.md +127 -143
  8. data/bin/http-hello +0 -1
  9. data/bin/raw-rbhttp +1 -1
  10. data/bin/raw_broadcast +8 -10
  11. data/bin/updated api +2 -2
  12. data/bin/ws-broadcast +2 -4
  13. data/bin/ws-echo +2 -2
  14. data/examples/config.ru +13 -13
  15. data/examples/echo.ru +5 -6
  16. data/examples/hello.ru +2 -3
  17. data/examples/info.md +316 -0
  18. data/examples/pubsub_engine.ru +81 -0
  19. data/examples/redis.ru +9 -9
  20. data/examples/shootout.ru +45 -11
  21. data/ext/iodine/defer.c +194 -297
  22. data/ext/iodine/defer.h +61 -53
  23. data/ext/iodine/evio.c +0 -260
  24. data/ext/iodine/evio.h +50 -22
  25. data/ext/iodine/evio_callbacks.c +26 -0
  26. data/ext/iodine/evio_epoll.c +251 -0
  27. data/ext/iodine/evio_kqueue.c +193 -0
  28. data/ext/iodine/extconf.rb +1 -1
  29. data/ext/iodine/facil.c +1420 -542
  30. data/ext/iodine/facil.h +151 -64
  31. data/ext/iodine/fio_ary.h +418 -0
  32. data/ext/iodine/{base64.c → fio_base64.c} +33 -24
  33. data/ext/iodine/{base64.h → fio_base64.h} +6 -7
  34. data/ext/iodine/{fio_cli_helper.c → fio_cli.c} +77 -58
  35. data/ext/iodine/{fio_cli_helper.h → fio_cli.h} +9 -4
  36. data/ext/iodine/fio_hashmap.h +759 -0
  37. data/ext/iodine/fio_json_parser.h +651 -0
  38. data/ext/iodine/fio_llist.h +257 -0
  39. data/ext/iodine/fio_mem.c +672 -0
  40. data/ext/iodine/fio_mem.h +140 -0
  41. data/ext/iodine/fio_random.c +248 -0
  42. data/ext/iodine/{random.h → fio_random.h} +11 -14
  43. data/ext/iodine/{sha1.c → fio_sha1.c} +28 -24
  44. data/ext/iodine/{sha1.h → fio_sha1.h} +38 -16
  45. data/ext/iodine/{sha2.c → fio_sha2.c} +66 -49
  46. data/ext/iodine/{sha2.h → fio_sha2.h} +57 -26
  47. data/ext/iodine/{fiobj_internal.c → fio_siphash.c} +9 -90
  48. data/ext/iodine/fio_siphash.h +18 -0
  49. data/ext/iodine/fio_tmpfile.h +38 -0
  50. data/ext/iodine/fiobj.h +24 -7
  51. data/ext/iodine/fiobj4sock.h +23 -0
  52. data/ext/iodine/fiobj_ary.c +143 -226
  53. data/ext/iodine/fiobj_ary.h +17 -16
  54. data/ext/iodine/fiobj_data.c +1160 -0
  55. data/ext/iodine/fiobj_data.h +164 -0
  56. data/ext/iodine/fiobj_hash.c +298 -406
  57. data/ext/iodine/fiobj_hash.h +101 -54
  58. data/ext/iodine/fiobj_json.c +478 -601
  59. data/ext/iodine/fiobj_json.h +34 -9
  60. data/ext/iodine/fiobj_numbers.c +383 -51
  61. data/ext/iodine/fiobj_numbers.h +87 -11
  62. data/ext/iodine/fiobj_str.c +423 -184
  63. data/ext/iodine/fiobj_str.h +81 -32
  64. data/ext/iodine/fiobject.c +273 -522
  65. data/ext/iodine/fiobject.h +477 -112
  66. data/ext/iodine/http.c +2243 -83
  67. data/ext/iodine/http.h +842 -121
  68. data/ext/iodine/http1.c +810 -385
  69. data/ext/iodine/http1.h +16 -39
  70. data/ext/iodine/http1_parser.c +146 -74
  71. data/ext/iodine/http1_parser.h +15 -4
  72. data/ext/iodine/http_internal.c +1258 -0
  73. data/ext/iodine/http_internal.h +226 -0
  74. data/ext/iodine/http_mime_parser.h +341 -0
  75. data/ext/iodine/iodine.c +86 -68
  76. data/ext/iodine/iodine.h +26 -11
  77. data/ext/iodine/iodine_helpers.c +8 -7
  78. data/ext/iodine/iodine_http.c +487 -324
  79. data/ext/iodine/iodine_json.c +304 -0
  80. data/ext/iodine/iodine_json.h +6 -0
  81. data/ext/iodine/iodine_protocol.c +107 -45
  82. data/ext/iodine/iodine_pubsub.c +526 -225
  83. data/ext/iodine/iodine_pubsub.h +10 -0
  84. data/ext/iodine/iodine_websockets.c +268 -510
  85. data/ext/iodine/iodine_websockets.h +2 -4
  86. data/ext/iodine/pubsub.c +726 -432
  87. data/ext/iodine/pubsub.h +85 -103
  88. data/ext/iodine/rb-call.c +4 -4
  89. data/ext/iodine/rb-defer.c +46 -22
  90. data/ext/iodine/rb-fiobj2rb.h +117 -0
  91. data/ext/iodine/rb-rack-io.c +73 -238
  92. data/ext/iodine/rb-rack-io.h +2 -2
  93. data/ext/iodine/rb-registry.c +35 -93
  94. data/ext/iodine/rb-registry.h +1 -0
  95. data/ext/iodine/redis_engine.c +742 -304
  96. data/ext/iodine/redis_engine.h +42 -39
  97. data/ext/iodine/resp_parser.h +311 -0
  98. data/ext/iodine/sock.c +627 -490
  99. data/ext/iodine/sock.h +345 -297
  100. data/ext/iodine/spnlock.inc +15 -4
  101. data/ext/iodine/websocket_parser.h +16 -20
  102. data/ext/iodine/websockets.c +188 -257
  103. data/ext/iodine/websockets.h +24 -133
  104. data/lib/iodine.rb +52 -7
  105. data/lib/iodine/cli.rb +6 -24
  106. data/lib/iodine/json.rb +40 -0
  107. data/lib/iodine/version.rb +1 -1
  108. data/lib/iodine/websocket.rb +5 -3
  109. data/lib/rack/handler/iodine.rb +58 -13
  110. metadata +38 -48
  111. data/bin/ws-shootout +0 -107
  112. data/examples/broadcast.ru +0 -56
  113. data/ext/iodine/bscrypt-common.h +0 -116
  114. data/ext/iodine/bscrypt.h +0 -49
  115. data/ext/iodine/fio2resp.c +0 -60
  116. data/ext/iodine/fio2resp.h +0 -51
  117. data/ext/iodine/fio_dict.c +0 -446
  118. data/ext/iodine/fio_dict.h +0 -99
  119. data/ext/iodine/fio_hash_table.h +0 -370
  120. data/ext/iodine/fio_list.h +0 -111
  121. data/ext/iodine/fiobj_internal.h +0 -280
  122. data/ext/iodine/fiobj_primitives.c +0 -131
  123. data/ext/iodine/fiobj_primitives.h +0 -55
  124. data/ext/iodine/fiobj_sym.c +0 -135
  125. data/ext/iodine/fiobj_sym.h +0 -60
  126. data/ext/iodine/hex.c +0 -124
  127. data/ext/iodine/hex.h +0 -70
  128. data/ext/iodine/http1_request.c +0 -81
  129. data/ext/iodine/http1_request.h +0 -58
  130. data/ext/iodine/http1_response.c +0 -417
  131. data/ext/iodine/http1_response.h +0 -95
  132. data/ext/iodine/http_request.c +0 -111
  133. data/ext/iodine/http_request.h +0 -102
  134. data/ext/iodine/http_response.c +0 -1703
  135. data/ext/iodine/http_response.h +0 -250
  136. data/ext/iodine/misc.c +0 -182
  137. data/ext/iodine/misc.h +0 -74
  138. data/ext/iodine/random.c +0 -208
  139. data/ext/iodine/redis_connection.c +0 -278
  140. data/ext/iodine/redis_connection.h +0 -86
  141. data/ext/iodine/resp.c +0 -842
  142. data/ext/iodine/resp.h +0 -261
  143. data/ext/iodine/siphash.c +0 -154
  144. data/ext/iodine/siphash.h +0 -22
  145. data/ext/iodine/xor-crypt.c +0 -193
  146. 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 eacy with:
3
+ # Running this application from the command line is easy with:
4
4
  #
5
- # iodine hello.ru
5
+ # iodine echo.ru
6
6
  #
7
7
  # Or, in single thread and single process:
8
8
  #
9
- # iodine -t 1 -w 1 hello.ru
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.websocket?'.freeze])
33
- env['upgrade.websocket'.freeze] = WebsocketEcho
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 eacy with:
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' => '32' },
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