Browse by type
Iodine is a fast concurrent web application server for real-time Ruby applications, with native support for WebSockets and Pub/Sub services - but it's also so much more.
Iodine is a Ruby wrapper for much of the facil.io C framework, leveraging the speed of C for many common web application tasks. In addition, iodine abstracts away all network concerns, so you never need to worry about the transport layer, leaving you free to concentrate on your application logic.
Iodine includes native support for:
gzip support for pre-compressed assets);stderr;Since iodine wraps much of the C facil.io framework for Ruby:
Iodine can handle thousands of concurrent connections (tested with more than 20K connections on Linux)!
Iodine is ideal for Linux/Unix based systems (i.e. macOS, Ubuntu, FreeBSD etc'), which are ideal for evented IO (while Windows and Solaris are better at IO completion events, which are very different).
Iodine is a C extension for Ruby, developed and optimized for Ruby MRI 2.3 and up... it should support the whole Ruby 2.x and 3.x MRI family, but CI tests start at Ruby 2.3.
Note: iodine does not support streaming when using Rack. It's recommended to avoid blocking the server when using body.each since the each loop will block iodine's thread until it's finished and iodine won't send any data before the loop is done.
Iodine includes a light and fast HTTP and Websocket server written in C that was written according to the Rack interface specifications and the Websocket draft extension.
With Iodine.listen service: :http it's possible to run multiple HTTP applications (but please remember not to set more than a single application on a single TCP/IP port).
Iodine also supports native process cluster Pub/Sub and a native RedisEngine to easily scale iodine's Pub/Sub horizontally.
See the GitHub Open Issues list for known issues and to report new issues.
Install iodine on any Linux / BSD / macOS system using:
gem install iodine
Using the iodine server is easy, simply add iodine as a gem to your Rails / Sinatra / Rack application's Gemfile:
gem 'iodine', '~>0.7'
Then start your application from the command-line / terminal using iodine:
bundler exec iodine
Note: iodine has known issues with the TLS/SSL support. TLS/SSL should NOT be used in production (see issues #95 and #94).
Make sure to update OpenSSL to the latest version before installing Ruby (rbenv should do this automatically).
To avoid name resolution conflicts, iodine will bind to the same OpenSSL version Ruby is bound to. To use SSL/TLS this should be OpenSSL >= 1.1.0 or LibreSSL >= 2.7.4.
Verbose installation should provide a confirmation message, such as:
$ gem install iodine -f -V
...
checking for -lcrypto... yes
checking for -lssl... yes
Detected OpenSSL library, testing for version.
Confirmed OpenSSL to be version 1.1.0 or above (OpenSSL 1.1.0j 20 Nov 2018)...
* Compiling with HAVE_OPENSSL.
...
The installation script tests for OpenSSL 1.1.0 and above. However, this testing approach sometimes provides false positives. If TLS isn't required, install with NO_SSL=1. i.e.:
NO_SSL=1 bundler exec iodine
On Rails:
Replace the puma gem with the iodine gem.
Remove the config/puma.rb file (or comment out the code).
Optionally, it's possible to add a config/initializers/iodine.rb file. For example:
```ruby
if(defined?(Iodine)) Iodine.threads = ENV.fetch("RAILS_MAX_THREADS", 5).to_i if Iodine.threads.zero? Iodine.workers = ENV.fetch("WEB_CONCURRENCY", 2).to_i if Iodine.workers.zero? Iodine::DEFAULT_SETTINGS[:port] ||= ENV.fetch("PORT") if ENV.fetch("PORT") end ```
When using native WebSockets with Rails, middle-ware is probably the best approach. A guide for this approach will, hopefully, get published in the future.
Note: command-line instructions (CLI) should be the preferred way for configuring iodine, allowing for code-less configuration updates.
To get the most out of iodine, consider the amount of CPU cores available and the concurrency level the application requires.
Iodine will calculate, when possible, a good enough default concurrency model for fast applications. See if this works for your application or customize according to the application's needs.
Command line arguments allow easy access to different options, including concurrency levels. i.e., to set up 16 threads and 4 processes:
bundler exec iodine -p $PORT -t 16 -w 4
The environment variables THREADS and WORKERS are automatically recognized when iodine is first required, allowing environment specific customization. i.e.:
export THREADS=16
export WORKERS=-1 # negative values are fractions of CPU cores.
bundler exec iodine -p $PORT
Negative values are evaluated as "CPU Cores / abs(Value)". i.e., on an 8 core CPU machine, this will produce 4 worker processes with 2 threads per worker:
bundler exec iodine -p $PORT -t 2 -w -2
Iodine includes a fast, network oriented, custom memory allocator, optimizing away some of the work usually placed on the Ruby Garbage Collector (GC).
This approach helps to minimize heap fragmentation for long running processes, by grouping many short-lived objects into a common memory space.
It is still recommended to consider jemalloc or other allocators that also help mitigate heap fragmentation issues.
Iodine supports an internal static file service that bypasses the Ruby layer and serves static files directly from "C-land".
This means that iodine won't lock Ruby's GVL when sending static files. The files will be sent directly, allowing for true native concurrency.
Since the Ruby layer is unaware of these requests, logging can be performed by turning iodine's logger on.
To use native static file service, setup the public folder's address before starting the server.
This can be done when starting the server from the command line:
bundler exec iodine -p $PORT -t 16 -w 4 -www /my/public/folder
Or using a simple Ruby script. i.e. (a my_server.rb example):
require 'iodine'
# static file service
Iodine.listen, service: :http, public: '/my/public/folder'
# for static file service, we only need a single thread and a single worker.
Iodine.threads = 1
Iodine.start
To enable logging from the command line, use the -v (verbose) option:
bundler exec iodine -p $PORT -t 16 -w 4 -www /my/public/folder -v
When a public folder is assigned (the static file server is active), iodine automatically adds support for the X-Sendfile header in any Ruby application response.
This allows Ruby to send very large files using a very small memory footprint and usually leverages the sendfile system call.
i.e. (example config.ru for iodine):
app = proc do |env|
request = Rack::Request.new(env)
if request.path_info == '/source'.freeze
[200, { 'X-Sendfile' => File.expand_path(__FILE__), 'Content-Type' => 'text/plain'}, []]
elsif request.path_info == '/file'.freeze
[200, { 'X-Header' => 'This was a Rack::Sendfile response sent as text.' }, File.open(__FILE__)]
else
[200, { 'Content-Type' => 'text/html',
'Content-Length' => request.path_info.length.to_s },
[request.path_info]]
end
end
# # optional:
# use Rack::Sendfile
run app
Benchmark localhost:3000/source to experience the X-Sendfile extension at work.
Rails does this automatically when compiling assets, which is: gzip your static files.
Iodine will automatically recognize and send the gz version if the client (browser) supports the gzip transfer-encoding.
For example, to offer a compressed version of style.css, run (in the terminal):
$ gzip -k -9 style.css
This results in both files, style.css (the original) and style.css.gz (the compressed).
When a browser that supports compressed encoding (which is most browsers) requests the file, iodine will recognize that a pre-compressed option exists and will prefer the gzip compressed version.
It's as easy as that. No extra code required.
Upgrade and SSE supportIodine's HTTP server implements the WebSocket/SSE Rack Specification Draft, supporting native WebSocket/SSE connections using Rack's env Hash.
This promotes separation of concerns, where iodine handles all the Network related logic and the application can focus on the API and data it provides.
Upgrading an HTTP connection can be performed either using iodine's native WebSocket / EventSource (SSE) support with env['rack.upgrade?'] or by implementing your own protocol directly over the TCP/IP layer - be it a WebSocket flavor or something completely different - using env['upgrade.tcp'].
Iodine treats EventSource / SSE connections as if they were a half-duplex WebSocket connection, using the exact same API and callbacks as WebSockets.
When an EventSource / SSE request is received, iodine will set the Rack Hash's upgrade property to :sse, so that: env['rack.upgrade?'] == :sse.
The rest is detailed in the WebSocket support section.
When a WebSocket connection request is received, iodine will set the Rack Hash's upgrade property to :websocket, so that: env['rack.upgrade?'] == :websocket
To "upgrade" the HTTP request to the WebSockets protocol (or SSE), simply provide iodine with a WebSocket Callback Object instance or class: env['rack.upgrade'] = MyWebsocketClass or env['rack.upgrade'] = MyWebsocketClass.new(args)
Iodine will adopt the object, providing it with network functionality (methods such as write, defer and close will become available) and invoke it's callbacks on network events.
Here is a simple chat-room example we can run in the terminal (irb) or easily paste into a config.ru file:
require 'iodine'
module WebsocketChat
def on_open client
# Pub/Sub directly to the client (or use a block to process the messages)
client.subscribe :chat
# Writing directly to the socket
client.write "You're now in the chatroom."
end
def on_message client, data
# Strings and symbol channel names are equivalent.
client.publish "chat", data
end
extend self
end
APP = Proc.new do |env|
if env['rack.upgrade?'.freeze] == :websocket
env['rack.upgrade'.freeze] = WebsocketChat
[0,{}, []] # It's possible to set cookies for the response.
elsif env['rack.upgrade?'.freeze] == :sse
puts "SSE connections can only receive data from the server, the can't write."
env['rack.upgrade'.freeze] = WebsocketChat
[0,{}, []] # It's possible to set cookies for the response.
else
[200, {"Content-Length" => "12", "Content-Type" => "text/plain"}, ["Welcome Home"] ]
end
end
# Pus/Sub can be server oriented as well as connection bound
Iodine.subscribe(:chat) {|ch, msg| puts msg if Iodine.master? }
# By default, Pub/Sub performs in process cluster mode.
Iodine.workers = 4
# # in irb:
Iodine.listen service: :http, public: "www/public", handler: APP
Iodine.start
# # or in config.ru
run APP
Iodine's core, facil.io offers a native Pub/Sub implementation that can be scaled across machine boundaries using Redis.
The default implementation covers the whole process cluster, so a single cluster doesn't need Redis
Once a single iodine process cluster isn't enough, horizontal scaling for the Pub/Sub layer is as simple as connecting iodine to Redis using the -r <url> from the command line. i.e.:
$ iodine -w -1 -t 8 -r redis://localhost
It's also possible to initialize the iodine<=>Redis link using Ruby, directly from the application's code:
``
$ claude mcp add iodine \
-- python -m otcore.mcp_server <graph>