iodine 0.7.59 → 0.8.0

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.
Files changed (160) hide show
  1. checksums.yaml +4 -4
  2. data/.clang-format +15 -0
  3. data/.rspec +1 -0
  4. data/.yardopts +1 -3
  5. data/CHANGELOG.md +132 -12
  6. data/Gemfile +5 -7
  7. data/LICENSE.txt +1 -1
  8. data/README.md +147 -671
  9. data/Rakefile +9 -16
  10. data/examples/chatroom.nru +9 -0
  11. data/examples/client +74 -0
  12. data/examples/config-hello.nru +9 -0
  13. data/examples/config-hello.ru +11 -0
  14. data/examples/config-hijack.ru +13 -0
  15. data/examples/config-shootout.ru +43 -0
  16. data/examples/config.nru +120 -0
  17. data/examples/config.rb +37 -0
  18. data/examples/config.ru +42 -53
  19. data/examples/fake +24 -0
  20. data/examples/hello_neorack +137 -0
  21. data/examples/routes.nru +29 -0
  22. data/examples/routes.ru +42 -0
  23. data/exe/iodine +218 -258
  24. data/ext/iodine/extconf.rb +27 -100
  25. data/ext/iodine/fio-stl.h +128336 -0
  26. data/ext/iodine/fio-stl.md +26396 -0
  27. data/ext/iodine/iodine.c +213 -1393
  28. data/ext/iodine/iodine.h +250 -55
  29. data/ext/iodine/iodine_arg_helper.h +329 -0
  30. data/ext/iodine/iodine_caller.h +234 -25
  31. data/ext/iodine/iodine_cli.h +434 -0
  32. data/ext/iodine/iodine_compression.h +312 -0
  33. data/ext/iodine/iodine_connection.h +3484 -34
  34. data/ext/iodine/iodine_core.h +363 -0
  35. data/ext/iodine/iodine_crypto.h +1441 -0
  36. data/ext/iodine/iodine_defer.h +225 -4
  37. data/ext/iodine/iodine_json.h +738 -4
  38. data/ext/iodine/iodine_listener.h +263 -0
  39. data/ext/iodine/iodine_log.h +297 -0
  40. data/ext/iodine/iodine_minimap.h +543 -0
  41. data/ext/iodine/iodine_mustache.h +461 -4
  42. data/ext/iodine/iodine_pubsub_eng.h +729 -0
  43. data/ext/iodine/iodine_pubsub_history.h +547 -0
  44. data/ext/iodine/iodine_pubsub_msg.h +228 -0
  45. data/ext/iodine/iodine_redis.h +322 -0
  46. data/ext/iodine/iodine_store.h +418 -16
  47. data/ext/iodine/iodine_threads.h +279 -0
  48. data/ext/iodine/iodine_tls.h +515 -6
  49. data/ext/iodine/iodine_utils.h +923 -0
  50. data/iodine.gemspec +37 -42
  51. data/lib/iodine/benchmark.rb +1690 -0
  52. data/lib/iodine/documentation.rb +3085 -0
  53. data/lib/iodine/version.rb +17 -1
  54. data/lib/iodine.rb +54 -255
  55. data/logo.svg +36 -0
  56. data/mustache.md +55 -0
  57. data/sig/iodine.rbs +4 -0
  58. metadata +58 -184
  59. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
  60. data/.github/workflows/ruby.yml +0 -42
  61. data/.gitignore +0 -20
  62. data/LIMITS.md +0 -41
  63. data/SPEC-PubSub-Draft.md +0 -159
  64. data/SPEC-WebSocket-Draft.md +0 -239
  65. data/bin/console +0 -22
  66. data/bin/info.md +0 -353
  67. data/bin/mustache_bench.rb +0 -100
  68. data/bin/poc/Gemfile.lock +0 -23
  69. data/bin/poc/README.md +0 -37
  70. data/bin/poc/config.ru +0 -66
  71. data/bin/poc/gemfile +0 -1
  72. data/bin/poc/www/index.html +0 -57
  73. data/examples/async_task.ru +0 -92
  74. data/examples/bates/README.md +0 -3
  75. data/examples/bates/config.ru +0 -342
  76. data/examples/bates/david+bold.pdf +0 -0
  77. data/examples/bates/public/drop-pdf.png +0 -0
  78. data/examples/bates/public/index.html +0 -600
  79. data/examples/echo.ru +0 -59
  80. data/examples/etag.ru +0 -16
  81. data/examples/hello.ru +0 -29
  82. data/examples/pubsub_engine.ru +0 -81
  83. data/examples/rack3.ru +0 -12
  84. data/examples/redis.ru +0 -70
  85. data/examples/shootout.ru +0 -73
  86. data/examples/sub-protocols.ru +0 -90
  87. data/examples/tcp_client.rb +0 -66
  88. data/examples/x-sendfile.ru +0 -14
  89. data/ext/iodine/fio.c +0 -11995
  90. data/ext/iodine/fio.h +0 -6371
  91. data/ext/iodine/fio_cli.c +0 -431
  92. data/ext/iodine/fio_cli.h +0 -189
  93. data/ext/iodine/fio_json_parser.h +0 -688
  94. data/ext/iodine/fio_siphash.c +0 -157
  95. data/ext/iodine/fio_siphash.h +0 -37
  96. data/ext/iodine/fio_tls.h +0 -129
  97. data/ext/iodine/fio_tls_missing.c +0 -649
  98. data/ext/iodine/fio_tls_openssl.c +0 -1106
  99. data/ext/iodine/fio_tmpfile.h +0 -50
  100. data/ext/iodine/fiobj.h +0 -44
  101. data/ext/iodine/fiobj4fio.h +0 -21
  102. data/ext/iodine/fiobj_ary.c +0 -333
  103. data/ext/iodine/fiobj_ary.h +0 -139
  104. data/ext/iodine/fiobj_data.c +0 -1187
  105. data/ext/iodine/fiobj_data.h +0 -167
  106. data/ext/iodine/fiobj_hash.c +0 -409
  107. data/ext/iodine/fiobj_hash.h +0 -176
  108. data/ext/iodine/fiobj_json.c +0 -622
  109. data/ext/iodine/fiobj_json.h +0 -68
  110. data/ext/iodine/fiobj_mem.h +0 -71
  111. data/ext/iodine/fiobj_mustache.c +0 -317
  112. data/ext/iodine/fiobj_mustache.h +0 -62
  113. data/ext/iodine/fiobj_numbers.c +0 -344
  114. data/ext/iodine/fiobj_numbers.h +0 -127
  115. data/ext/iodine/fiobj_str.c +0 -433
  116. data/ext/iodine/fiobj_str.h +0 -172
  117. data/ext/iodine/fiobject.c +0 -620
  118. data/ext/iodine/fiobject.h +0 -655
  119. data/ext/iodine/hpack.h +0 -1923
  120. data/ext/iodine/http.c +0 -2743
  121. data/ext/iodine/http.h +0 -1002
  122. data/ext/iodine/http1.c +0 -910
  123. data/ext/iodine/http1.h +0 -29
  124. data/ext/iodine/http1_parser.h +0 -878
  125. data/ext/iodine/http_internal.c +0 -1279
  126. data/ext/iodine/http_internal.h +0 -237
  127. data/ext/iodine/http_mime_parser.h +0 -350
  128. data/ext/iodine/iodine_caller.c +0 -230
  129. data/ext/iodine/iodine_connection.c +0 -941
  130. data/ext/iodine/iodine_defer.c +0 -420
  131. data/ext/iodine/iodine_fiobj2rb.h +0 -120
  132. data/ext/iodine/iodine_helpers.c +0 -282
  133. data/ext/iodine/iodine_helpers.h +0 -12
  134. data/ext/iodine/iodine_http.c +0 -1181
  135. data/ext/iodine/iodine_http.h +0 -23
  136. data/ext/iodine/iodine_json.c +0 -302
  137. data/ext/iodine/iodine_mustache.c +0 -567
  138. data/ext/iodine/iodine_pubsub.c +0 -580
  139. data/ext/iodine/iodine_pubsub.h +0 -26
  140. data/ext/iodine/iodine_rack_io.c +0 -273
  141. data/ext/iodine/iodine_rack_io.h +0 -20
  142. data/ext/iodine/iodine_store.c +0 -146
  143. data/ext/iodine/iodine_tcp.c +0 -346
  144. data/ext/iodine/iodine_tcp.h +0 -13
  145. data/ext/iodine/iodine_tls.c +0 -261
  146. data/ext/iodine/mustache_parser.h +0 -1546
  147. data/ext/iodine/redis_engine.c +0 -957
  148. data/ext/iodine/redis_engine.h +0 -79
  149. data/ext/iodine/resp_parser.h +0 -317
  150. data/ext/iodine/websocket_parser.h +0 -506
  151. data/ext/iodine/websockets.c +0 -743
  152. data/ext/iodine/websockets.h +0 -185
  153. data/lib/iodine/connection.rb +0 -61
  154. data/lib/iodine/json.rb +0 -42
  155. data/lib/iodine/mustache.rb +0 -113
  156. data/lib/iodine/pubsub.rb +0 -55
  157. data/lib/iodine/rack_utils.rb +0 -43
  158. data/lib/iodine/tls.rb +0 -16
  159. data/lib/rack/handler/iodine.rb +0 -33
  160. data/logo.png +0 -0
data/Rakefile CHANGED
@@ -1,23 +1,16 @@
1
- require "bundler/gem_tasks"
2
- require "rake/extensiontask"
3
-
4
- begin
5
- require 'rspec/core/rake_task'
6
- RSpec::Core::RakeTask.new(:rspec)
7
- rescue LoadError
8
- end
1
+ # frozen_string_literal: true
9
2
 
10
- task :spec => [:compile, :rspec]
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
11
5
 
12
- task :default => [:compile, :spec]
6
+ require "rake/extensiontask"
13
7
 
14
8
  Rake::ExtensionTask.new "iodine" do |ext|
15
9
  ext.lib_dir = "lib/iodine"
16
10
  end
17
11
 
18
- # Rake::ExtensionTask.new "iodine_http" do |ext|
19
- # ext.name = 'iodine_http'
20
- # ext.lib_dir = "lib/iodine"
21
- # ext.ext_dir = 'ext/iodine'
22
- # ext.config_script = 'extconf-http.rb'
23
- # end
12
+ require "rspec/core/rake_task"
13
+ RSpec::Core::RakeTask.new(:spec) do |t|
14
+ t.pattern = "spec/**/*_spec.rb"
15
+ t.rspec_opts = "--format progress --no-color --backtrace"
16
+ end
@@ -0,0 +1,9 @@
1
+ # A simple Hello World NeoRack application
2
+ module App
3
+ def self.on_http(e)
4
+ e.status = 200
5
+ e.finish
6
+ end
7
+ end
8
+
9
+ run App
data/examples/client ADDED
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'iodine'
5
+
6
+ Iodine::Logger.level = 3 # only log warnings or worse
7
+
8
+ # A demo client class
9
+ module Client
10
+
11
+ # a helper to print response data
12
+ def self.print_response(h)
13
+ puts "#{h.version} #{h.status}"
14
+ h.headers.each {|k,v| puts("#{k}: #{v}\r") if k.is_a? String }
15
+ puts "\r"
16
+ while(s = h.gets)
17
+ puts s
18
+ end
19
+ end
20
+
21
+ # Called Once per HTTP request. Each HTTP request starts with a new HTTP handle / Event object (`h`).
22
+ def self.on_http(h)
23
+ print_response(h)
24
+ end
25
+
26
+ # Called when a long-running connection opens.
27
+ #
28
+ # This will be called for Event Source, WebSocket, TCP/IP and Raw Unix Socket connections.
29
+ def self.on_open(ws)
30
+ print_response(ws)
31
+ puts "Opening #{ws.websocket? ? "WebSocket" : "SSE" } Connection"
32
+ ws.subscribe(:stdio) if ws.websocket? # SSE connections don't send stuff
33
+ end
34
+ # Called when incoming data arrives and its behavior depends on the underlying protocol.
35
+ def self.on_message(ws, data)
36
+ puts "Got WebSocket Message: #{data}"
37
+ end
38
+
39
+ # Called a long running connection was closed (WebSocket / SSE / TCP/IP / Raw Unix Socket).
40
+ def self.on_close(ws)
41
+ puts "Closed #{ws.websocket? ? "WebSocket" : "SSE" } Connection"
42
+ end
43
+ # Called after `on_http`/`on_closed` or if a client failed to connect.
44
+ def self.on_finish(h)
45
+ Iodine.stop
46
+ end
47
+ # Called when an Event Source (SSE) event has been received (when acting as an Event Source client).
48
+ def self.on_eventsource(sse, message)
49
+ puts "id: #{message.id}"
50
+ puts "event: #{message.event}"
51
+ puts "data: #{message.data}"
52
+ end
53
+ end
54
+
55
+ # I will cheat a little, using Iodine's CLI parser...
56
+ opt = Iodine::Base::CLI.parse(false)
57
+ opt[:bind] ||= opt[0] # allow the first unnamed parameter to be the URL to connect to
58
+ raise "ERROR: this app requires a URL to connect to!" unless opt[:bind]
59
+
60
+ Iodine.threads = 0
61
+ Iodine.workers = 0
62
+
63
+ # create the service instance, the block returns a connection handler.
64
+ Iodine::Connection.new(opt[:bind], handler: Client)
65
+
66
+ # Read STDIO while running
67
+ Thread.new do
68
+ while true
69
+ Iodine.publish channel: :stdio, message: STDIN.readline
70
+ end
71
+ end
72
+
73
+ # Start the server
74
+ Iodine.start
@@ -0,0 +1,9 @@
1
+ # A simple Hello World NeoRack application
2
+ module App
3
+ HELLO_RESPONSE = "Hello World!"
4
+ def self.on_http(e)
5
+ e.finish HELLO_RESPONSE
6
+ end
7
+ end
8
+
9
+ run App
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A simple Hello World Rack application
4
+ module App
5
+ HELLO_RESPONSE = [200, {"content-length" => 12}, ["Hello World!"]]
6
+ def self.call(e)
7
+ HELLO_RESPONSE
8
+ end
9
+ end
10
+
11
+ run App
@@ -0,0 +1,13 @@
1
+ # A simple Rack application that prints out the `env` variable to the browser.
2
+ module EchoApp
3
+ def self.call(e)
4
+ io = e['rack.hijack'].call
5
+ txt = ["Hijacked!\r\n"]
6
+ e.each {|k,v| txt << "#{k}: #{v}\r\n" }
7
+ txt = txt.join()
8
+ Thread.new {sleep(0.5); io.write("HTTP/1.1 200 OK\r\nContent-Length: #{txt.bytesize}\r\n\r\n#{txt}"); io.close; puts("Sent"); }
9
+ [200, {}, []]
10
+ end
11
+ end
12
+
13
+ run EchoApp
@@ -0,0 +1,43 @@
1
+ # # Written for NeoRack with Iodine v.0.8.x
2
+ # #
3
+ # # Run using:
4
+ # iodine -t <number of threads> -w <number of processes> -p <port>
5
+ # # i.e.:
6
+ # iodine -t 8 -w 0 -p 3334
7
+
8
+ # Local process-cluster support is built into iodine's pub/sub, but cross-machine pub/sub requires a RESP3 database (such as Valkey or Redis).
9
+ class ShootoutApp
10
+ SHOOTOUT = "shootout".freeze
11
+ def on_http(e)
12
+ e.finish "This application should be used with the websocket-shootout benchmark utility."
13
+ end
14
+
15
+ def on_open(e)
16
+ e.subscribe channel: SHOOTOUT
17
+ end
18
+
19
+ def on_message e, data
20
+
21
+ if data[0] == 'b' # binary
22
+ e.publish(channel: SHOOTOUT, message: data)
23
+ data[0] = 'r'
24
+ e.write data
25
+ return
26
+ end
27
+
28
+ # cmd, payload = JSON(data).values_at('type', 'payload')
29
+ cmd, payload = Iodine::JSON.parse(data).values_at('type', 'payload')
30
+ if cmd == 'echo'
31
+ e.write({type: 'echo', payload: payload}.to_json)
32
+ else
33
+ e.publish(channel: SHOOTOUT, message: {type: 'broadcast', payload: payload})
34
+ e.write({type: "broadcastResult", payload: payload})
35
+ end
36
+
37
+ rescue
38
+ puts "Incoming message format error!"
39
+ end
40
+
41
+ end
42
+
43
+ run ShootoutApp.new
@@ -0,0 +1,120 @@
1
+ # This is a NeoRack Application with a Rack fallback for non-NeoRack servers (such as Puma).
2
+ #
3
+ # Iodine can run both Rack (old school) and NeoRack applications.
4
+ #
5
+ # In this case the `on_http` implementation will be called by iodine (and the `call` implementation will be called by any other server).
6
+ #
7
+ module EchoApp
8
+ # A selector for the on_http example to implement.
9
+ STREAM_RESPONSES = false
10
+
11
+ # for non-NeoRack servers, such as Puma
12
+ def self.call(e)
13
+ txt = []
14
+ if e['rack.upgrade?']
15
+ e['rack.upgrade'] = self
16
+ else
17
+ e.each {|k,v| txt << "#{k}: #{v}\r\n" }
18
+ end
19
+ [200, {}, txt]
20
+ end
21
+
22
+ # Choose requested response callback – a streaming response vs. `content-length`.
23
+ if(STREAM_RESPONSES)
24
+
25
+ # This approach streams the data, adding some HTTP overhead, as may be performed by longer handlers.
26
+ def self.on_http(e)
27
+ # we write without finishing and without setting the content-length header...
28
+ # servers should automatically stream the response (i.e., using chunked encoding)
29
+ e.write "path: #{e.path}\r\n"
30
+ e.write "opath: #{e.opath} -- (original)\r\n"
31
+ e.write "query: #{e.query}\r\n"
32
+ e.write "method: #{e.method}\r\n"
33
+ e.write "version: #{e.version}\r\n"
34
+ e.write "from: #{e.from} (real: #{e.peer_addr})\r\n"
35
+ e.headers.each {|k,v| e.write "#{k}: #{v}\r\n" }
36
+ # echo request body to the response
37
+ while(l = e.gets)
38
+ e.write l
39
+ end
40
+ e.finish
41
+ end
42
+
43
+ else # STREAM_RESPONSES?
44
+
45
+ # This is similar to Rack, compiling the whole response before sending it
46
+ # this should optimally result in `content-length` being set by the server
47
+ def self.on_http(e)
48
+ # First, prepare the output data
49
+ out = "path: #{e.path}\r\n" +
50
+ "opath: #{e.opath} -- (original)\r\n" +
51
+ "query: #{e.query}\r\n" +
52
+ "method: #{e.method}\r\n" +
53
+ "version: #{e.version}\r\n" +
54
+ "from: #{e.from} (#{e.peer_addr})\r\n"
55
+ e.headers.each {|k,v| out += "#{k}: #{v}\r\n" }
56
+ # append the incoming body
57
+ while(l = e.gets)
58
+ out += l
59
+ end
60
+ # finish and write the data to the client.
61
+ # efficient servers will see this is the first and only `write` and set `content-length`
62
+ e.finish out
63
+ end
64
+
65
+ end
66
+
67
+ # Called when a WebSocket / SSE connection is opened.
68
+ def self.on_open(e)
69
+ e.subscribe(:broadcast) # { |msg| puts "(#{Process.pid}:#{Thread.current}): #{msg.channel} : #{msg.message}"; e.write msg.message }
70
+ end
71
+
72
+ # Called when a WebSocket message is received...
73
+ #
74
+ # ... however, Iodine also allows SSE clients to "cheat" and push messages to this callback.
75
+ def self.on_message(e, m)
76
+ Iodine.publish(:broadcast, m)
77
+ end
78
+ end
79
+
80
+ module ResourceApp
81
+ def self.index(e); e.finish("Show Index"); end
82
+ def self.show(e); e.finish("Show Item: #{e.path[1..-1]}"); end
83
+ def self.delete(e); e.finish("Delete Item: #{e.path[1..-1]}"); end
84
+ def self.new(e); e.finish("New Item Form."); end
85
+ def self.edit(e); e.finish("Edit Item: #{e.path.split(?/)[1]}"); end
86
+ def self.create(e); e.finish("Create a new Item."); end
87
+ def self.update(e); e.finish("Update Item: #{e.path[1..-1]}"); end
88
+ end
89
+ neorack_resource ResourceApp
90
+
91
+ if defined?(Iodine)
92
+ map("hello") { map("echo", EchoApp); run {|e| [200, {}, "Hello World"] } }
93
+ map("rack") { run {|e| EchoApp.call(e) } }
94
+ map('/crud', ResourceApp)
95
+ end
96
+ run EchoApp
97
+
98
+ # Benchmark with keep-alive:
99
+ #
100
+ # ab -c 200 -t 4 -n 1000000 -k http://127.0.0.1:3000/
101
+ # wrk -c200 -d4 -t2 http://localhost:3000/
102
+ #
103
+ # Connect to chat server with WebSockets:
104
+ #
105
+ # ws = new WebSocket("ws://" + document.location.host + document.location.pathname);
106
+ # ws.onmessage = function(e) {console.log("Got message!"); console.log(e.data);};
107
+ # ws.onclose = function(e) {console.log("closed")};
108
+ # ws.onopen = function(e) {ws.send("hi");};
109
+ #
110
+ # Listen to chat messages with Server Sent Events (EventSource / SSE):
111
+ #
112
+ # const listener = new EventSource(document.location.href);
113
+ # listener.onmessage = (e) => { console.log(e); }
114
+ # listener.addEventListener("time", (e) => { console.log(e); })
115
+ # listener.addEventListener("event", (e) => { console.log(e); })
116
+ #
117
+ # To run any code / configuration on the root (master) process, call Iodine with `-C`:
118
+ #
119
+ # iodine -C config.rb
120
+ #
@@ -0,0 +1,37 @@
1
+ # This is an example configuration file
2
+ #
3
+ # The configuration file will run code before loading the application.
4
+ #
5
+ # When hot swapping, the configuration file code will remain in memory (will be forked), but the application code will reload.
6
+
7
+ module MyConfig
8
+ def self.timer
9
+ # a false return value will cancel the timer.
10
+ return false unless Iodine.master?
11
+ puts("Heart Beat")
12
+ end
13
+ end
14
+
15
+ Iodine.run_after(2000, -1, MyConfig.method(:timer))
16
+
17
+ # Pub/Sub engines are a classical use-case for a configuration file.
18
+ #
19
+ class LogPubSub < Iodine::PubSub::Engine
20
+ def subscribe ch
21
+ puts "(#{Process.pid}) #{ch} has listeners (created)"
22
+ end
23
+ def psubscribe ch
24
+ puts "(#{Process.pid}) (pattern) #{ch} has listeners (created)"
25
+ end
26
+ def unsubscribe ch
27
+ puts "(#{Process.pid}) #{ch} has no more listeners (destroyed)"
28
+ end
29
+ def punsubscribe ch
30
+ puts "(#{Process.pid}) (pattern) #{ch} has no more listeners (destroyed)"
31
+ end
32
+ def publish m
33
+ puts "(#{Process.pid}) Someone published to #{m.channel}"
34
+ Iodine.publish channel: m.channel, message: m.message, filter: m.filter, engine: Iodine::PubSub::CLUSTER
35
+ end
36
+ end
37
+ Iodine::PubSub.default = LogPubSub.new
data/examples/config.ru CHANGED
@@ -1,59 +1,48 @@
1
- # This is a WebSocket / SSE notification broadcasting application.
1
+ # A simple Rack application that prints out the `env` variable to the browser.
2
2
  #
3
- # Running this application from the command line is easy with:
4
- #
5
- # iodine
6
- #
7
- # Or, in single thread and single process:
8
- #
9
- # iodine -t 1 -w 1
10
- #
11
- # Benchmark with `ab` or `wrk` (a 5 seconds benchmark with 2000 concurrent clients):
12
- #
13
- # ab -c 2000 -t 5 -n 1000000 -k http://127.0.0.1:3000/
14
- # wrk -c2000 -d5 -t12 http://localhost:3000/
15
- #
16
- # Test websocket echo using the browser. For example:
17
- # ws = new WebSocket("ws://localhost:3000"); ws.onmessage = function(e) {console.log("Got message!"); console.log(e.data);}; ws.onclose = function(e) {console.log("closed")}; ws.onopen = function(e) {ws.send("hi");};
18
-
19
-
20
- # A simple router - Checks for Websocket Upgrade and answers HTTP.
21
- module MyHTTPRouter
22
- # This is the HTTP response object according to the Rack specification.
23
- HTTP_RESPONSE = [200, { 'Content-Type' => 'text/html',
24
- 'Content-Length' => '77' },
25
- ['Please connect using WebSockets or SSE (send messages only using WebSockets).']]
26
-
27
- WS_RESPONSE = [0, {}, []].freeze
28
-
29
- # this is function will be called by the Rack server (iodine) for every request.
30
- def self.call env
31
- # check if this is an upgrade request (WebsSocket / SSE).
32
- if(env['rack.upgrade?'.freeze])
33
- env['rack.upgrade'.freeze] = BroadcastClient
34
- return WS_RESPONSE
35
- end
36
- # simply return the RESPONSE object, no matter what request was received.
37
- HTTP_RESPONSE
38
- end
39
- end
40
-
41
- # A simple Websocket Callback Object.
42
- module BroadcastClient
43
- # seng a message to new clients.
44
- def on_open client
45
- client.subscribe :broadcast
3
+ # This example also supports the (deprecated) old-style iodine Rack upgrade.
4
+ module App
5
+ def self.call(env)
6
+ txt = []
7
+ # support old-style iodine Rack upgrade for pure Rack applications.
8
+ if env['rack.upgrade?']
9
+ env['rack.upgrade'] = self
10
+ else
11
+ env.each {|k,v| txt << "#{k}: #{v}\r\n" }
12
+ end
13
+ [200, {}, txt]
46
14
  end
47
- # send a message, letting the client know the server is suggunt down.
48
- def on_shutdown client
49
- client.write "Server shutting down. Goodbye."
15
+ def self.on_open(e)
16
+ e.subscribe :broadcast
50
17
  end
51
- # perform the echo
52
- def on_message client, data
53
- client.publish :broadcast, data
18
+ def self.on_message(e, m)
19
+ Iodine.publish :broadcast, m
54
20
  end
55
- extend self
56
21
  end
57
22
 
58
- # this function call links our HelloWorld application with Rack
59
- run MyHTTPRouter
23
+ map("/hello") { map("/echo") { run App }; run {|e| [200, {}, ["Hello World"]] } }
24
+ run App
25
+
26
+ # Benchmark with keep-alive:
27
+ #
28
+ # ab -c 200 -t 4 -n 1000000 -k http://127.0.0.1:3000/
29
+ # wrk -c200 -d4 -t2 http://localhost:3000/
30
+ #
31
+ # Connect to chat server with WebSockets:
32
+ #
33
+ # ws = new WebSocket("ws://" + document.location.host + document.location.pathname);
34
+ # ws.onmessage = function(e) {console.log("Got message!"); console.log(e.data);};
35
+ # ws.onclose = function(e) {console.log("closed")};
36
+ # ws.onopen = function(e) {ws.send("hi");};
37
+ #
38
+ # Listen to chat messages with Server Sent Events (EventSource / SSE):
39
+ #
40
+ # const listener = new EventSource(document.location.href);
41
+ # listener.onmessage = (e) => { console.log(e); }
42
+ # listener.addEventListener("time", (e) => { console.log(e); })
43
+ # listener.addEventListener("event", (e) => { console.log(e); })
44
+ #
45
+ # To run any code / configuration on the root (master) process, call Iodine with `-C`:
46
+ #
47
+ # iodine -C config.rb
48
+ #
data/examples/fake ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ require 'iodine'
4
+
5
+ # Iodine::Logger.level = 5
6
+ # Iodine.threads = 0
7
+ # Iodine.workers = 0
8
+
9
+
10
+ # Define the protocol for our service
11
+ module FakeHTTP
12
+ def on_message(client, data)
13
+ client.write "HTTP/1.1 200 OK\r\ndate: Mon, 22 Jul 2024 14:06:56 GMT\r\ncontent-length:#{data.bytesize}\r\n\r\n#{data}"
14
+ end
15
+ # We can use a singleton pattern,
16
+ # as the Protocol object doesn't contain any per-connection data.
17
+ extend self
18
+ end
19
+
20
+ # create the service instance, the block returns a connection handler.
21
+ Iodine.listen(handler: FakeHTTP, service: :tcp)
22
+
23
+ # Start the server
24
+ Iodine.start
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+
5
+ # Benchmark with keep-alive:
6
+ #
7
+ # ab -c 200 -t 4 -n 1000000 -k http://127.0.0.1:3000/
8
+ # wrk -c200 -d4 -t2 http://localhost:3000/
9
+ #
10
+ # Connect to chat server with WebSockets:
11
+ #
12
+ # ws = new WebSocket("ws://" + document.location.host + document.location.pathname);
13
+ # ws.onmessage = function(e) {console.log("Got message!"); console.log(e.data);};
14
+ # ws.onclose = function(e) {console.log("closed")};
15
+ # ws.onopen = function(e) {ws.send("hi");};
16
+ #
17
+ # Listen to chat messages with Server Sent Events (EventSource / SSE):
18
+ #
19
+ # const listener = new EventSource(document.location.href);
20
+ # listener.onmessage = (e) => { console.log(e); }
21
+ # listener.addEventListener("time", (e) => { console.log(e); })
22
+ # listener.addEventListener("event", (e) => { console.log(e); })
23
+
24
+ require 'iodine'
25
+
26
+ Server.threads = 1
27
+ Server.workers = 0
28
+
29
+ # Define the protocol for our service
30
+ #
31
+ # We can use a singleton pattern, as the Protocol object doesn't contain any per-connection data.
32
+ #
33
+ # See the [NeoRack](https://github.com/boazsegev/neorack) specification for details.
34
+ module MyNeoRackApp
35
+ # Called Once per HTTP request. Each HTTP request starts with a new HTTP handle / Event object (`h`).
36
+ def self.on_http(h)
37
+ h[:start] = Time.now
38
+ # This would be faster if we sent the whole response as a single String.
39
+ #
40
+ # However, this example demonstrates a Streaming response, so we will call `h.write` multiple times.
41
+ h.status = 200
42
+ h.write("Hello NeoRack World!\r\n#{h.method} #{h.path} #{h.version}\r\n")
43
+ h.headers.each {|k,v| h.write("#{k}: #{v}\r\n") if k.is_a? String }
44
+ h.finish
45
+ end
46
+
47
+ # Called after `on_http` / `on_closed` or if as a client Iodine failed to connect.
48
+ def self.on_finish(h); puts "#{h.method} #{h.path} - #{Time.now - h[:start]}s" end
49
+
50
+ # This is ignored by NeoRack.
51
+ #
52
+ # It's called when receiving HTTP requests using a Rack server (or if on_http is removed).
53
+ #
54
+ # [Rack](https://github.com/rack/rack) is the classical server interface and it is supported by Iodine with minimal extensions.
55
+ def self.call(env);
56
+ env[:start] = Time.now
57
+ txt = []
58
+ txt << "Hello Rack World!\r\n#{env['REQUEST_METHOD']} #{env['PATH_INFO']} #{env['HTTP_VERSION']}\r\n"
59
+ env.each {|k,v| txt << "#{k}: #{v}\r\n" if k.is_a? String }
60
+ [200, {}, txt]
61
+ end
62
+
63
+ # When `on_authenticate is present, a missing `on_authenticate_sse` or missing `on_authenticate_websocket`
64
+ # will route to this callback, allowing authentication logic to be merged in one method.
65
+ #
66
+ # **Must** return `true` for the connection to be allowed.
67
+ #
68
+ # **Any other return value will cause the connection to be refused** (and disconnected).
69
+ #
70
+ # By default, if `on_message` or `on_open` are defined, than `on_authenticate` returns `true`. Otherwise, `on_authenticate` returns `false`.
71
+ def self.on_authenticate(h)
72
+ h[:start] = Time.now
73
+ puts "Authenticating client #{h.object_id}"
74
+ true
75
+ end
76
+ # def self.on_authenticate_sse(h); end
77
+ # def self.on_authenticate_websocket(h); end
78
+
79
+ # Called when a long-running connection opens.
80
+ #
81
+ # This will be called for Event Source, WebSocket, TCP/IP and Raw Unix Socket connections.
82
+ def self.on_open(ws)
83
+ ws.subscribe(:time) if Server.extensions[:pubsub]
84
+ end
85
+ # Called when incoming data arrives and its behavior depends on the underlying protocol.
86
+ #
87
+ # For TCP/IP and Raw Unix connections, the data is a Binary encoded String with some (or all) of the data available in the incoming socket buffer.
88
+ #
89
+ # For WebSockets, the `data` element will contain a String with the WebSocket Message.
90
+ #
91
+ # If the WebSocket message is in text, the String encoding will be UTF-8.
92
+ #
93
+ # If the WebSocket message is binary, the String encoding will be Binary (ASCII).
94
+ def self.on_message(ws, data)
95
+ puts "Got WebSocket Message: #{data}"
96
+ ws.write("Echo: #{data}")
97
+ end
98
+ # Called when all calls to {Iodine::Connection#write} have been handled and the outgoing buffer is now empty.
99
+ def self.on_drained(ws)
100
+ puts "Finished sending data to #{ws.object_id}"
101
+ end
102
+ # Called when timeout has been reached for a TCP/IP / Raw Unix Socket connection.
103
+ def self.on_timeout(tcp)
104
+ puts "Timeout detected for #{tcp.object_id}"
105
+ end
106
+ # Called when the worker that manages this connection (or the root process, in non-cluster mode) starts shutting down.
107
+ def self.on_shutdown(ws)
108
+ ws.write "Server shutting down. Goodbye."
109
+ end
110
+ # Called a long running connection was closed (WebSocket / SSE / TCP/IP / Raw Unix Socket).
111
+ def self.on_close(ws)
112
+ puts "Closed #{ws.object_id}"
113
+ end
114
+ # Called when an Event Source (SSE) event has been received (when acting as an Event Source client).
115
+ def self.on_eventsource(sse, message)
116
+ puts "Normally only an SSE client would receive sse messages...\r\n#{message}"
117
+ end
118
+ # Called an Event Source (SSE) client send a re-connection request with the ID of the last message received.
119
+ def self.on_eventsource_reconnect(sse, last_message_id)
120
+ puts "Reconnecting SSE client #{sse.object_id}, should send everything after message ID #{last_message_id}"
121
+ end
122
+ end
123
+
124
+ # create the service instance, the block returns a connection handler.
125
+ Server.listen(handler: MyNeoRackApp)
126
+
127
+ # create the time pub/sub service
128
+ if Server.extensions[:pubsub]
129
+ Iodine.run_after(1000, -1) do
130
+ Server.publish(channel: :time, message: Iodine::Utils.time2str(Time.now))
131
+ end
132
+ end
133
+
134
+
135
+
136
+ # Start the server
137
+ Server.start
@@ -0,0 +1,29 @@
1
+ # This is a multi-route example using NeoRack vs Rack `map`
2
+
3
+ class RouteApp
4
+ def initialize(name)
5
+ @response = [200, {}, "Hello #{name}"]
6
+ end
7
+ def call(e)
8
+ @response
9
+ end
10
+ end
11
+
12
+ ROUTE_COUNT = 100
13
+ BSET_CASE_FOR_TRI = true
14
+
15
+ if BSET_CASE_FOR_TRI # best case
16
+ ROUTE_COUNT.times {|i| url = "/try_this_route-#{i}" ; map(url) { run RouteApp.new(url) } }
17
+ puts "\t test with:"
18
+ puts "\t wrk -c200 -d4 -t2 http://localhost:3000/try_this_route-0"
19
+ puts "\t wrk -c200 -d4 -t2 http://localhost:3000/try_this_route-#{ROUTE_COUNT / 2}"
20
+ puts "\t wrk -c200 -d4 -t2 http://localhost:3000/try_this_route-#{ROUTE_COUNT - 1}"
21
+ else
22
+ ROUTE_COUNT.times {|i| url = "/#{i}-try_this_route" ; map(url) { run RouteApp.new(url) } }
23
+ puts "\t test with:"
24
+ puts "\t wrk -c200 -d4 -t2 http://localhost:3000/0-try_this_route"
25
+ puts "\t wrk -c200 -d4 -t2 http://localhost:3000/#{ROUTE_COUNT / 2}-try_this_route"
26
+ puts "\t wrk -c200 -d4 -t2 http://localhost:3000/#{ROUTE_COUNT - 1}-try_this_route"
27
+ end
28
+
29
+ run {[404, {}, nil]}