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.
- checksums.yaml +4 -4
- data/.clang-format +15 -0
- data/.rspec +1 -0
- data/.yardopts +1 -3
- data/CHANGELOG.md +132 -12
- data/Gemfile +5 -7
- data/LICENSE.txt +1 -1
- data/README.md +147 -671
- data/Rakefile +9 -16
- data/examples/chatroom.nru +9 -0
- data/examples/client +74 -0
- data/examples/config-hello.nru +9 -0
- data/examples/config-hello.ru +11 -0
- data/examples/config-hijack.ru +13 -0
- data/examples/config-shootout.ru +43 -0
- data/examples/config.nru +120 -0
- data/examples/config.rb +37 -0
- data/examples/config.ru +42 -53
- data/examples/fake +24 -0
- data/examples/hello_neorack +137 -0
- data/examples/routes.nru +29 -0
- data/examples/routes.ru +42 -0
- data/exe/iodine +218 -258
- data/ext/iodine/extconf.rb +27 -100
- data/ext/iodine/fio-stl.h +128336 -0
- data/ext/iodine/fio-stl.md +26396 -0
- data/ext/iodine/iodine.c +213 -1393
- data/ext/iodine/iodine.h +250 -55
- data/ext/iodine/iodine_arg_helper.h +329 -0
- data/ext/iodine/iodine_caller.h +234 -25
- data/ext/iodine/iodine_cli.h +434 -0
- data/ext/iodine/iodine_compression.h +312 -0
- data/ext/iodine/iodine_connection.h +3484 -34
- data/ext/iodine/iodine_core.h +363 -0
- data/ext/iodine/iodine_crypto.h +1441 -0
- data/ext/iodine/iodine_defer.h +225 -4
- data/ext/iodine/iodine_json.h +738 -4
- data/ext/iodine/iodine_listener.h +263 -0
- data/ext/iodine/iodine_log.h +297 -0
- data/ext/iodine/iodine_minimap.h +543 -0
- data/ext/iodine/iodine_mustache.h +461 -4
- data/ext/iodine/iodine_pubsub_eng.h +729 -0
- data/ext/iodine/iodine_pubsub_history.h +547 -0
- data/ext/iodine/iodine_pubsub_msg.h +228 -0
- data/ext/iodine/iodine_redis.h +322 -0
- data/ext/iodine/iodine_store.h +418 -16
- data/ext/iodine/iodine_threads.h +279 -0
- data/ext/iodine/iodine_tls.h +515 -6
- data/ext/iodine/iodine_utils.h +923 -0
- data/iodine.gemspec +37 -42
- data/lib/iodine/benchmark.rb +1690 -0
- data/lib/iodine/documentation.rb +3085 -0
- data/lib/iodine/version.rb +17 -1
- data/lib/iodine.rb +54 -255
- data/logo.svg +36 -0
- data/mustache.md +55 -0
- data/sig/iodine.rbs +4 -0
- metadata +58 -184
- data/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
- data/.github/workflows/ruby.yml +0 -42
- data/.gitignore +0 -20
- data/LIMITS.md +0 -41
- data/SPEC-PubSub-Draft.md +0 -159
- data/SPEC-WebSocket-Draft.md +0 -239
- data/bin/console +0 -22
- data/bin/info.md +0 -353
- data/bin/mustache_bench.rb +0 -100
- data/bin/poc/Gemfile.lock +0 -23
- data/bin/poc/README.md +0 -37
- data/bin/poc/config.ru +0 -66
- data/bin/poc/gemfile +0 -1
- data/bin/poc/www/index.html +0 -57
- data/examples/async_task.ru +0 -92
- data/examples/bates/README.md +0 -3
- data/examples/bates/config.ru +0 -342
- data/examples/bates/david+bold.pdf +0 -0
- data/examples/bates/public/drop-pdf.png +0 -0
- data/examples/bates/public/index.html +0 -600
- data/examples/echo.ru +0 -59
- data/examples/etag.ru +0 -16
- data/examples/hello.ru +0 -29
- data/examples/pubsub_engine.ru +0 -81
- data/examples/rack3.ru +0 -12
- data/examples/redis.ru +0 -70
- data/examples/shootout.ru +0 -73
- data/examples/sub-protocols.ru +0 -90
- data/examples/tcp_client.rb +0 -66
- data/examples/x-sendfile.ru +0 -14
- data/ext/iodine/fio.c +0 -11995
- data/ext/iodine/fio.h +0 -6371
- data/ext/iodine/fio_cli.c +0 -431
- data/ext/iodine/fio_cli.h +0 -189
- data/ext/iodine/fio_json_parser.h +0 -688
- data/ext/iodine/fio_siphash.c +0 -157
- data/ext/iodine/fio_siphash.h +0 -37
- data/ext/iodine/fio_tls.h +0 -129
- data/ext/iodine/fio_tls_missing.c +0 -649
- data/ext/iodine/fio_tls_openssl.c +0 -1106
- data/ext/iodine/fio_tmpfile.h +0 -50
- data/ext/iodine/fiobj.h +0 -44
- data/ext/iodine/fiobj4fio.h +0 -21
- data/ext/iodine/fiobj_ary.c +0 -333
- data/ext/iodine/fiobj_ary.h +0 -139
- data/ext/iodine/fiobj_data.c +0 -1187
- data/ext/iodine/fiobj_data.h +0 -167
- data/ext/iodine/fiobj_hash.c +0 -409
- data/ext/iodine/fiobj_hash.h +0 -176
- data/ext/iodine/fiobj_json.c +0 -622
- data/ext/iodine/fiobj_json.h +0 -68
- data/ext/iodine/fiobj_mem.h +0 -71
- data/ext/iodine/fiobj_mustache.c +0 -317
- data/ext/iodine/fiobj_mustache.h +0 -62
- data/ext/iodine/fiobj_numbers.c +0 -344
- data/ext/iodine/fiobj_numbers.h +0 -127
- data/ext/iodine/fiobj_str.c +0 -433
- data/ext/iodine/fiobj_str.h +0 -172
- data/ext/iodine/fiobject.c +0 -620
- data/ext/iodine/fiobject.h +0 -655
- data/ext/iodine/hpack.h +0 -1923
- data/ext/iodine/http.c +0 -2743
- data/ext/iodine/http.h +0 -1002
- data/ext/iodine/http1.c +0 -910
- data/ext/iodine/http1.h +0 -29
- data/ext/iodine/http1_parser.h +0 -878
- data/ext/iodine/http_internal.c +0 -1279
- data/ext/iodine/http_internal.h +0 -237
- data/ext/iodine/http_mime_parser.h +0 -350
- data/ext/iodine/iodine_caller.c +0 -230
- data/ext/iodine/iodine_connection.c +0 -941
- data/ext/iodine/iodine_defer.c +0 -420
- data/ext/iodine/iodine_fiobj2rb.h +0 -120
- data/ext/iodine/iodine_helpers.c +0 -282
- data/ext/iodine/iodine_helpers.h +0 -12
- data/ext/iodine/iodine_http.c +0 -1181
- data/ext/iodine/iodine_http.h +0 -23
- data/ext/iodine/iodine_json.c +0 -302
- data/ext/iodine/iodine_mustache.c +0 -567
- data/ext/iodine/iodine_pubsub.c +0 -580
- data/ext/iodine/iodine_pubsub.h +0 -26
- data/ext/iodine/iodine_rack_io.c +0 -273
- data/ext/iodine/iodine_rack_io.h +0 -20
- data/ext/iodine/iodine_store.c +0 -146
- data/ext/iodine/iodine_tcp.c +0 -346
- data/ext/iodine/iodine_tcp.h +0 -13
- data/ext/iodine/iodine_tls.c +0 -261
- data/ext/iodine/mustache_parser.h +0 -1546
- data/ext/iodine/redis_engine.c +0 -957
- data/ext/iodine/redis_engine.h +0 -79
- data/ext/iodine/resp_parser.h +0 -317
- data/ext/iodine/websocket_parser.h +0 -506
- data/ext/iodine/websockets.c +0 -743
- data/ext/iodine/websockets.h +0 -185
- data/lib/iodine/connection.rb +0 -61
- data/lib/iodine/json.rb +0 -42
- data/lib/iodine/mustache.rb +0 -113
- data/lib/iodine/pubsub.rb +0 -55
- data/lib/iodine/rack_utils.rb +0 -43
- data/lib/iodine/tls.rb +0 -16
- data/lib/rack/handler/iodine.rb +0 -33
- data/logo.png +0 -0
data/Rakefile
CHANGED
|
@@ -1,23 +1,16 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
task default: %i[]
|
|
11
5
|
|
|
12
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
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,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
|
data/examples/config.nru
ADDED
|
@@ -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
|
+
#
|
data/examples/config.rb
ADDED
|
@@ -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
|
-
#
|
|
1
|
+
# A simple Rack application that prints out the `env` variable to the browser.
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
client.write "Server shutting down. Goodbye."
|
|
15
|
+
def self.on_open(e)
|
|
16
|
+
e.subscribe :broadcast
|
|
50
17
|
end
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
59
|
-
run
|
|
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
|
data/examples/routes.nru
ADDED
|
@@ -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]}
|