isomorfeus-iodine 0.8.1 → 0.8.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,782 +1,782 @@
1
- # iodine - Why Settle for a fast HTTP / WebSocket Server with native Pub/Sub?
2
-
3
- [![Gem](https://img.shields.io/gem/dt/iodine.svg)](https://rubygems.org/gems/iodine)
4
- [![Build Status](https://travis-ci.org/boazsegev/iodine.svg?branch=master)](https://travis-ci.org/boazsegev/iodine)
5
- [![Gem Version](https://badge.fury.io/rb/iodine.svg)](https://badge.fury.io/rb/iodine)
6
- [![Inline docs](http://inch-ci.org/github/boazsegev/iodine.svg?branch=master)](http://www.rubydoc.info/github/boazsegev/iodine/master/frames)
7
- [![GitHub](https://img.shields.io/badge/GitHub-Open%20Source-blue.svg)](https://github.com/boazsegev/iodine)
8
-
9
- [![Logo](https://github.com/boazsegev/iodine/raw/master/logo.png)](https://github.com/boazsegev/iodine)
10
-
11
- 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.
12
-
13
- Iodine is a Ruby wrapper for many of the [facil.io](https://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, free to concentrate on your application logic.
14
-
15
- Iodine includes native support for:
16
-
17
- * HTTP, WebSockets and EventSource (SSE) Services (server);
18
- * WebSocket connections (server / client);
19
- * Pub/Sub (with optional Redis Pub/Sub scaling);
20
- * Fast(!) builtin Mustache template engine.
21
- * Static file service (with automatic `gzip` support for pre-compressed assets);
22
- * Optimized Logging to `stderr`.
23
- * Asynchronous event scheduling and timers;
24
- * HTTP/1.1 keep-alive and pipelining;
25
- * Heap Fragmentation Protection.
26
- * TLS 1.2 and above (Requires OpenSSL >= 1.1.0);
27
- * TCP/IP server and client connectivity;
28
- * Unix Socket server and client connectivity;
29
- * Hot Restart (using the USR1 signal and without hot deployment);
30
- * Custom protocol authoring;
31
- * [Sequel](https://github.com/jeremyevans/sequel) and ActiveRecord forking protection.
32
- * and more!
33
-
34
- Since iodine wraps much of the [C facil.io framework](https://github.com/boazsegev/facil.io) to Ruby:
35
-
36
- * Iodine can handle **thousands of concurrent connections** (tested with more then 20K connections on Linux)!
37
-
38
- * 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).
39
-
40
- Iodine is a C extension for Ruby, developed and optimized for Ruby MRI 2.2.2 and up... it should support the whole Ruby 2.0 MRI family, but CI tests start at Ruby 2.2.2.
41
-
42
- **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 the iodine's thread until it's finished and iodine won't send any data before the loop is done.
43
-
44
- ## Iodine - a fast & powerful HTTP + WebSockets server with native Pub/Sub
45
-
46
- Iodine includes a light and fast HTTP and Websocket server written in C that was written according to the [Rack interface specifications](http://www.rubydoc.info/github/rack/rack/master/file/SPEC) and the [Websocket draft extension](./SPEC-Websocket-Draft.md).
47
-
48
- With `Iodine.listen service: :http` it's possible to run multiple HTTP applications (please remember not to set more than a single application on a single TCP/IP port).
49
-
50
- Iodine also supports native process cluster Pub/Sub and a native RedisEngine to easily scale iodine's Pub/Sub horizontally.
51
-
52
- ### Known Issues and Reporting Issues
53
-
54
- See the [GitHub Open Issues](https://github.com/boazsegev/iodine/issues) list for known issues and to report new issues.
55
-
56
- ### Installing and Running Iodine
57
-
58
- Install iodine on any Linux / BSD / macOS system using:
59
-
60
- ```bash
61
- gem install iodine
62
- ```
63
-
64
- Using the iodine server is easy, simply add iodine as a gem to your Rails / Sinatra / Rack application's `Gemfile`:
65
-
66
- ```ruby
67
- gem 'iodine', '~>0.7'
68
- ```
69
-
70
- Then start your application from the command-line / terminal using iodine:
71
-
72
- ```bash
73
- bundler exec iodine
74
- ```
75
-
76
- #### Installing with SSL/TLS
77
-
78
- **Note**: iodine has known issues with the TLS/SSL support. TLS/SSL should **NOT** be used in production (see issues #95 and #94).
79
-
80
- Make sure to update OpenSSL to the latest version **before installing Ruby** (`rbenv` should do this automatically).
81
-
82
- 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.
83
-
84
- Verbose installation should provide a confirmation message, such as:
85
-
86
- ```bash
87
- $ gem install iodine -f -V
88
- ...
89
- checking for -lcrypto... yes
90
- checking for -lssl... yes
91
- Detected OpenSSL library, testing for version.
92
- Confirmed OpenSSL to be version 1.1.0 or above (OpenSSL 1.1.0j 20 Nov 2018)...
93
- * Compiling with HAVE_OPENSSL.
94
- ...
95
- ```
96
-
97
- 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.:
98
-
99
- ```bash
100
- NO_SSL=1 bundler exec iodine
101
- ```
102
-
103
- ### Running with Rails
104
-
105
- On Rails:
106
-
107
- 1. Replace the `puma` gem with the `iodine` gem.
108
-
109
- 1. Remove the `config/puma.rb` file (or comment out the code).
110
-
111
- 1. Optionally, it's possible to add a `config/initializers/iodine.rb` file. For example:
112
-
113
- ```ruby
114
- # Iodine setup - use conditional setup to allow command-line arguments to override these:
115
- if(defined?(Iodine))
116
- Iodine.threads = ENV.fetch("RAILS_MAX_THREADS", 5).to_i if Iodine.threads.zero?
117
- Iodine.workers = ENV.fetch("WEB_CONCURRENCY", 2).to_i if Iodine.workers.zero?
118
- Iodine::DEFAULT_SETTINGS[:port] ||= ENV.fetch("PORT") if ENV.fetch("PORT")
119
- end
120
- ```
121
-
122
- 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.
123
-
124
- **Note**: command-line instructions (CLI) should be the preferred way for configuring iodine, allowing for code-less configuration updates.
125
-
126
- ### Optimizing Iodine's Concurrency
127
-
128
- To get the most out of iodine, consider the amount of CPU cores available and the concurrency level the application requires.
129
-
130
- 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.
131
-
132
- Command line arguments allow easy access to different options, including concurrency levels. i.e., to set up 16 threads and 4 processes:
133
-
134
- ```bash
135
- bundler exec iodine -p $PORT -t 16 -w 4
136
- ```
137
-
138
- The environment variables `THREADS` and `WORKERS` are automatically recognized when iodine is first required, allowing environment specific customization. i.e.:
139
-
140
- ```bash
141
- export THREADS=16
142
- export WORKERS=-1 # negative values are fractions of CPU cores.
143
- bundler exec iodine -p $PORT
144
- ```
145
-
146
- 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:
147
-
148
- ```bash
149
- bundler exec iodine -p $PORT -t 2 -w -2
150
- ```
151
-
152
- ### Heap Fragmentation Protection
153
-
154
- Iodine includes a fast, network oriented, custom memory allocator, optimizing away some of the work usually placed on the Ruby Garbage Collector (GC).
155
-
156
- This approach helps to minimize heap fragmentation for long running processes, by grouping many short-lived objects into a common memory space.
157
-
158
- It is still recommended to consider [jemalloc](http://jemalloc.net) or other allocators that also help mitigate heap fragmentation issues.
159
-
160
- ### Static file serving support
161
-
162
- Iodine supports an internal static file service that bypasses the Ruby layer and serves static files directly from "C-land".
163
-
164
- 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.
165
-
166
- Since the Ruby layer is unaware of these requests, logging can be performed by turning iodine's logger on.
167
-
168
- To use native static file service, setup the public folder's address **before** starting the server.
169
-
170
- This can be done when starting the server from the command line:
171
-
172
- ```bash
173
- bundler exec iodine -p $PORT -t 16 -w 4 -www /my/public/folder
174
- ```
175
-
176
- Or using a simple Ruby script. i.e. (a `my_server.rb` example):
177
-
178
- ```ruby
179
- require 'iodine'
180
- # static file service
181
- Iodine.listen, service: :http, public: '/my/public/folder'
182
- # for static file service, we only need a single thread and a single worker.
183
- Iodine.threads = 1
184
- Iodine.start
185
- ```
186
-
187
- To enable logging from the command line, use the `-v` (verbose) option:
188
-
189
- ```bash
190
- bundler exec iodine -p $PORT -t 16 -w 4 -www /my/public/folder -v
191
- ```
192
-
193
- #### X-Sendfile
194
-
195
- 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.
196
-
197
- This allows Ruby to send very large files using a very small memory footprint and usually leverages the `sendfile` system call.
198
-
199
- i.e. (example `config.ru` for iodine):
200
-
201
- ```ruby
202
- app = proc do |env|
203
- request = Rack::Request.new(env)
204
- if request.path_info == '/source'.freeze
205
- [200, { 'X-Sendfile' => File.expand_path(__FILE__), 'Content-Type' => 'text/plain'}, []]
206
- elsif request.path_info == '/file'.freeze
207
- [200, { 'X-Header' => 'This was a Rack::Sendfile response sent as text.' }, File.open(__FILE__)]
208
- else
209
- [200, { 'Content-Type' => 'text/html',
210
- 'Content-Length' => request.path_info.length.to_s },
211
- [request.path_info]]
212
- end
213
- end
214
- # # optional:
215
- # use Rack::Sendfile
216
- run app
217
- ```
218
-
219
- Benchmark [localhost:3000/source](http://localhost:3000/source) to experience the `X-Sendfile` extension at work.
220
-
221
- #### Pre-Compressed assets / files
222
-
223
- Rails does this automatically when compiling assets, which is: `gzip` your static files.
224
-
225
- Iodine will automatically recognize and send the `gz` version if the client (browser) supports the `gzip` transfer-encoding.
226
-
227
- For example, to offer a compressed version of `style.css`, run (in the terminal):
228
-
229
- ```bash
230
- $ gzip -k -9 style.css
231
- ```
232
-
233
- This results in both files, `style.css` (the original) and `style.css.gz` (the compressed).
234
-
235
- 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.
236
-
237
- It's as easy as that. No extra code required.
238
-
239
- ### Special HTTP `Upgrade` and SSE support
240
-
241
- Iodine's HTTP server implements the [WebSocket/SSE Rack Specification Draft](SPEC-Websocket-Draft.md), supporting native WebSocket/SSE connections using Rack's `env` Hash.
242
-
243
- 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.
244
-
245
- 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']`.
246
-
247
- #### EventSource / SSE
248
-
249
- Iodine treats EventSource / SSE connections as if they were a half-duplex WebSocket connection, using the exact same API and callbacks as WebSockets.
250
-
251
- When an EventSource / SSE request is received, iodine will set the Rack Hash's upgrade property to `:sse`, so that: `env['rack.upgrade?'] == :sse`.
252
-
253
- The rest is detailed in the WebSocket support section.
254
-
255
- #### WebSockets
256
-
257
- When a WebSocket connection request is received, iodine will set the Rack Hash's upgrade property to `:websocket`, so that: `env['rack.upgrade?'] == :websocket`
258
-
259
- 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)`
260
-
261
- 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.
262
-
263
- Here is a simple chat-room example we can run in the terminal (`irb`) or easily paste into a `config.ru` file:
264
-
265
- ```ruby
266
- require 'iodine'
267
- module WebsocketChat
268
- def on_open client
269
- # Pub/Sub directly to the client (or use a block to process the messages)
270
- client.subscribe :chat
271
- # Writing directly to the socket
272
- client.write "You're now in the chatroom."
273
- end
274
- def on_message client, data
275
- # Strings and symbol channel names are equivalent.
276
- client.publish "chat", data
277
- end
278
- extend self
279
- end
280
- APP = Proc.new do |env|
281
- if env['rack.upgrade?'.freeze] == :websocket
282
- env['rack.upgrade'.freeze] = WebsocketChat
283
- [0,{}, []] # It's possible to set cookies for the response.
284
- elsif env['rack.upgrade?'.freeze] == :sse
285
- puts "SSE connections can only receive data from the server, the can't write."
286
- env['rack.upgrade'.freeze] = WebsocketChat
287
- [0,{}, []] # It's possible to set cookies for the response.
288
- else
289
- [200, {"Content-Length" => "12", "Content-Type" => "text/plain"}, ["Welcome Home"] ]
290
- end
291
- end
292
- # Pus/Sub can be server oriented as well as connection bound
293
- Iodine.subscribe(:chat) {|ch, msg| puts msg if Iodine.master? }
294
- # By default, Pub/Sub performs in process cluster mode.
295
- Iodine.workers = 4
296
- # # in irb:
297
- Iodine.listen service: :http, public: "www/public", handler: APP
298
- Iodine.start
299
- # # or in config.ru
300
- run APP
301
- ```
302
-
303
- ### Native Pub/Sub with *optional* Redis scaling
304
-
305
- Iodine's core, `facil.io` offers a native Pub/Sub implementation that can be scaled across machine boundaries using Redis.
306
-
307
- The default implementation covers the whole process cluster, so a single cluster doesn't need Redis
308
-
309
- 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.:
310
-
311
- ```bash
312
- $ iodine -w -1 -t 8 -r redis://localhost
313
- ```
314
-
315
- It's also possible to initialize the iodine<=>Redis link using Ruby, directly from the application's code:
316
-
317
- ```ruby
318
- # initialize the Redis engine for each iodine process.
319
- if ENV["REDIS_URL"]
320
- Iodine::PubSub.default = Iodine::PubSub::Redis.new(ENV["REDIS_URL"])
321
- else
322
- puts "* No Redis, it's okay, pub/sub will still run on the whole process cluster."
323
- end
324
- # ... the rest of the application remains unchanged.
325
- ```
326
-
327
- Iodine's Redis client can also be used for asynchronous Redis command execution. i.e.:
328
-
329
- ```ruby
330
- if(Iodine::PubSub.default.is_a? Iodine::PubSub::Redis)
331
- # Ask Redis about all it's client connections and print out the reply.
332
- Iodine::PubSub.default.cmd("CLIENT LIST") { |reply| puts reply }
333
- end
334
- ```
335
-
336
- **Pub/Sub Details and Limitations:**
337
-
338
- * Iodine's Redis client does *not* support multiple databases. This is both because [database scoping is ignored by Redis during pub/sub](https://redis.io/topics/pubsub#database-amp-scoping) and because [Redis Cluster doesn't support multiple databases](https://redis.io/topics/cluster-spec). This indicated that multiple database support just isn't worth the extra effort and performance hit.
339
-
340
- * The iodine Redis client will use two Redis connections for the whole process cluster (a single publishing connection and a single subscription connection), minimizing the Redis load and network bandwidth.
341
-
342
- * Connections will be automatically re-established if timeouts or errors occur.
343
-
344
- ### Hot Restart
345
-
346
- Iodine will "hot-restart" the application by shutting down and re-spawning the worker processes.
347
-
348
- This will clear away any memory fragmentation concerns and other issues that might plague a long running worker process or ruby application.
349
-
350
- To hot-restart iodine, send the `SIGUSR1` signal to the root process.
351
-
352
- The following code will hot-restart iodine every 4 hours when iodine is running in cluster mode:
353
-
354
- ```ruby
355
- Iodine.run_every(4 * 60 * 60 * 1000) do
356
- Process.kill("SIGUSR1", Process.pid) unless Iodine.worker?
357
- end
358
- ```
359
-
360
- Since the master / root process doesn't handle any requests (it only handles pub/sub and house-keeping), it's memory map and process data shouldn't be as affected and the new worker processes should be healthier and more performant.
361
-
362
- **Note**: This will **not** re-load the application (any changes to the Ruby code require an actual restart).
363
-
364
- ### Optimized HTTP logging
365
-
366
- By default, iodine is pretty quiet. Some messages are logged to `stderr`, but not many.
367
-
368
- However, HTTP requests can be logged using iodine's optimized logger to `stderr`. Iodine will optimize the log output by caching the output time string which updates every second rather than every request.
369
-
370
- This can be performed by setting the `-v` flag during startup, i.e.:
371
-
372
- ```bash
373
- bundler exec iodine -p $PORT -t 16 -w 4 -v -www /my/public/folder
374
- ```
375
-
376
- The log output can be redirected to a file:
377
-
378
- ```bash
379
- bundler exec iodine -p $PORT -v 2>my_log.log
380
- ```
381
-
382
- The log output can also be redirected to a `stdout`:
383
-
384
- ```bash
385
- bundler exec iodine -p $PORT -v 2>&1
386
- ```
387
-
388
- ### Built-in support for Sequel and ActiveRecord
389
-
390
- It's a well known fact that [Database connections require special attention when using `fork`-ing servers (multi-process servers)](https://devcenter.heroku.com/articles/concurrency-and-database-connections#multi-process-servers) such as Puma, Passenger (Pro) and iodine.
391
-
392
- However, it's also true that [these issues go unnoticed by many developers](https://stackoverflow.com/a/45570999/4025095), since application developers are (rightfully) focused on the application rather than the infrastructure.
393
-
394
- With iodine, there's no need to worry.
395
-
396
- Iodine provides built-in `fork` handling for both ActiveRecord and [Sequel](https://github.com/jeremyevans/sequel), in order to protect against these possible errors.
397
-
398
- ### Client Support
399
-
400
- Iodine supports raw (TCP/IP and Unix Sockets) client connections as well as WebSocket connections.
401
-
402
- This can be utilized for communicating across micro services or taking advantage of persistent connection APIs such as ActionCable APIs, socket.io APIs etc'.
403
-
404
- Here is an example WebSocket client that will connect to the [WebSocket.org echo test service](https://www.websocket.org/echo.html) and send a number of pre-programmed messages.
405
-
406
- ```ruby
407
- require 'iodine'
408
-
409
- # The client class
410
- class EchoClient
411
-
412
- def on_open(connection)
413
- @messages = [ "Hello World!",
414
- "I'm alive and sending messages",
415
- "I also receive messages",
416
- "now that we all know this...",
417
- "I can stop.",
418
- "Goodbye." ]
419
- send_one_message(connection)
420
- end
421
-
422
- def on_message(connection, message)
423
- puts "Received: #{message}"
424
- send_one_message(connection)
425
- end
426
-
427
- def on_close(connection)
428
- # in this example, we stop iodine once the client is closed
429
- puts "* Client closed."
430
- Iodine.stop
431
- end
432
-
433
- # We use this method to pop messages from the queue and send them
434
- #
435
- # When the queue is empty, we disconnect the client.
436
- def send_one_message(connection)
437
- msg = @messages.shift
438
- if(msg)
439
- connection.write msg
440
- else
441
- connection.close
442
- end
443
- end
444
- end
445
-
446
- Iodine.threads = 1
447
- Iodine.connect url: "wss://echo.websocket.org", handler: EchoClient.new, ping: 40
448
- Iodine.start
449
- ```
450
-
451
- ### TLS >= 1.2 support
452
-
453
- > Requires OpenSSL >= `1.1.0`. On Heroku, requires `heroku-18`.
454
-
455
- Iodine supports secure connections fore TLS version 1.2 **and up** (depending on the OpenSSL version).
456
-
457
- A self signed certificate is available using the `-tls` flag from the command-line.
458
-
459
- PEM encoded certificates (which is probably the most common format) can be loaded from the command-line (`-tls-cert` and `-tls-key`) or dynamically (using `Iodine::TLS`).
460
-
461
- The TLS API is simplified but powerful, supporting the ALPN extension and peer verification (which client connections really should leverage).
462
-
463
- When enabling peer verification for server connections (using `Iodine::TLS#trust`), clients will be required to submit a trusted certificate in order to connect to the server.
464
-
465
- ### TCP/IP (raw) sockets
466
-
467
- Upgrading to a custom protocol (i.e., in order to implement your own WebSocket protocol with special extensions) is available when neither WebSockets nor SSE connection upgrades were requested. In the following (terminal) example, we'll use an echo server without direct socket echo:
468
-
469
- ```ruby
470
- require 'iodine'
471
- class MyProtocol
472
- def on_message client, data
473
- # regular socket echo - NOT websockets
474
- client.write data
475
- end
476
- end
477
- APP = Proc.new do |env|
478
- if env["HTTP_UPGRADE".freeze] =~ /echo/i.freeze
479
- env['upgrade.tcp'.freeze] = MyProtocol.new
480
- # an HTTP response will be sent before changing protocols.
481
- [101, { "Upgrade" => "echo" }, []]
482
- else
483
- [200, {"Content-Length" => "12", "Content-Type" => "text/plain"}, ["Welcome Home"] ]
484
- end
485
- end
486
- # # in irb:
487
- Iodine.listen service: :http, public: "www/public", handler: APP
488
- Iodine.threads = 1
489
- Iodine.start
490
- # # or in config.ru
491
- run APP
492
- ```
493
-
494
- ### How does it compare to other servers?
495
-
496
- In my tests, pitching Iodine against Puma, Iodine was anywhere between x1.5 and more than x10 faster than Puma (depending on use-case and settings).
497
-
498
- Such a big difference is suspect and I recommend that you test it yourself - even better if you test performance using your own application and a number of possible different settings (how many threads per CPU core? how many worker processes? middleware vs. server request logging, etc').
499
-
500
- I recommend benchmarking the performance for yourself using `wrk` or `ab`:
501
-
502
- ```bash
503
- $ wrk -c200 -d4 -t2 http://localhost:3000/
504
- # or
505
- $ ab -n 100000 -c 200 -k http://127.0.0.1:3000/
506
- ```
507
-
508
- The best application to use for benchmarking is your actual application. Or, you could create a simple `config.ru` file with a __hello world__ app:
509
-
510
- ```ruby
511
- App = Proc.new do |env|
512
- [200,
513
- { "Content-Type" => "text/html".freeze,
514
- "Content-Length" => "16".freeze },
515
- ['Hello from Rack!'.freeze] ]
516
- end
517
-
518
- run App
519
- ```
520
-
521
- Then start comparing servers. Here are the settings I used to compare iodine and Puma (4 processes, 4 threads):
522
-
523
- ```bash
524
- $ RACK_ENV=production iodine -p 3000 -t 4 -w 4
525
- # vs.
526
- $ RACK_ENV=production puma -p 3000 -t 4 -w 4
527
- # Review the `iodine -?` help for more command line options.
528
- ```
529
-
530
- It's recommended that the servers (Iodine/Puma) and the client (`wrk`/`ab`) run on separate machines.
531
-
532
- It is worth noting that iodine can also speed up logging by replacing the logging middleware with `iodine -v`. This approach uses less memory and improves performance at the expense of fuzzy timing and some string caching.
533
-
534
- On my machine, testing with the logging functionality enabled, iodine was more then 10 times faster than puma (60.9K req/sec vs. 5.3K req/sec)
535
-
536
- ### A few notes
537
-
538
- Iodine's upgrade / callback design has a number of benefits, some of them related to better IO handling, resource optimization (no need for two IO polling systems), etc. This also allows us to use middleware without interfering with connection upgrades and provides backwards compatibility.
539
-
540
- Iodine's HTTP server imposes a few restrictions for performance and security reasons, such as limiting each header line to 8Kb. These restrictions shouldn't be an issue and are similar to limitations imposed by Apache or Nginx.
541
-
542
- If you still want to use Rack's `hijack` API, iodine will support you - but be aware that you will need to implement your own reactor and thread pool for any sockets you hijack, as well as a socket buffer for non-blocking `write` operations (why do that when you can write a protocol object and have the main reactor manage the socket?).
543
-
544
- ## Installation
545
-
546
- To install iodine, simply install the the `iodine` gem:
547
-
548
- ```bash
549
- $ gem install iodine
550
- ```
551
-
552
- Iodine is written in C and allows some compile-time customizations, such as:
553
-
554
- * `FIO_FORCE_MALLOC` - avoids iodine's custom memory allocator and use `malloc` instead (mostly used when debugging iodine or when using a different memory allocator).
555
-
556
- * `FIO_MAX_SOCK_CAPACITY` - limits iodine's maximum client capacity. Defaults to 131,072 clients.
557
-
558
- * `FIO_USE_RISKY_HASH` - replaces SipHash with RiskyHash for iodine's internal hash maps.
559
-
560
- Since iodine hash maps have internal protection against collisions and hash flooding attacks, it's possible for iodine to leverage RiskyHash, which is faster than SipHash.
561
-
562
- By default, SipHash will be used. This is a community related choice, since the community seems to believe a hash function should protect the hash map rather than it being enough for a hash map implementation to be attack resistance.
563
-
564
- * `HTTP_MAX_HEADER_COUNT` - limits the number of headers the HTTP server will accept before disconnecting a client (security). Defaults to 128 headers (permissive).
565
-
566
- * `HTTP_MAX_HEADER_LENGTH` - limits the number of bytes allowed for a single header (pre-allocated memory per connection + security). Defaults to 8Kb per header line (normal).
567
-
568
- * `HTTP_BUSY_UNLESS_HAS_FDS` - requires at least X number of free file descriptors (for new database connections, etc') before accepting a new HTTP client.
569
-
570
- * `FIO_ENGINE_POLL` - prefer the `poll` system call over `epoll` or `kqueue` (not recommended).
571
-
572
- * `FIO_LOG_LENGTH_LIMIT` - sets the limit on iodine's logging messages (uses stack memory, so limits must be reasonable. Defaults to 2048.
573
-
574
- * `FIO_TLS_PRINT_SECRET` - if true, the OpenSSL master key will be printed as debug message level log. Use only for testing (with WireShark etc'), never in production! Default: false.
575
-
576
- These options can be used, for example, like so:
577
-
578
- ```bash
579
- gem install iodine -- \
580
- --with-cflags=\"-DHTTP_MAX_HEADER_LENGTH=48000 -DFIO_FORCE_MALLOC=1 -DHTTP_MAX_HEADER_COUNT=64\"
581
- ```
582
-
583
- More possible compile time options can be found in the [facil.io documentation](http://facil.io).
584
-
585
- ## Evented oriented design with extra safety
586
-
587
- Iodine is an evented server, similar in its architecture to `nginx` and `puma`. It's different than the simple "thread-per-client" design that is often taught when we begin to learn about network programming.
588
-
589
- By leveraging `epoll` (on Linux) and `kqueue` (on BSD), iodine can listen to multiple network events on multiple sockets using a single thread.
590
-
591
- All these events go into a task queue, together with the application events and any user generated tasks, such as ones scheduled by [`Iodine.run`](http://www.rubydoc.info/github/boazsegev/iodine/Iodine#run-class_method).
592
-
593
- In pseudo-code, this might look like this
594
-
595
- ```ruby
596
- QUEUE = Queue.new
597
-
598
- def server_cycle
599
- if(QUEUE.empty?)
600
- QUEUE << get_next_32_socket_events # these events schedule the proper user code to run
601
- end
602
- QUEUE << server_cycle
603
- end
604
-
605
- def run_server
606
- while ((event = QUEUE.pop))
607
- event.shift.call(*event)
608
- end
609
- end
610
- ```
611
-
612
- In pure Ruby (without using C extensions or Java), it's possible to do the same by using `select`... and although `select` has some issues, it could work well for lighter loads.
613
-
614
- The server events are fairly fast and fragmented (longer code is fragmented across multiple events), so one thread is enough to run the server including it's static file service and everything...
615
-
616
- ...but single threaded mode should probably be avoided.
617
-
618
-
619
- It's very common that the application's code will run slower and require external resources (i.e., databases, a custom pub/sub service, etc'). This slow code could "starve" the server, which is patiently waiting to run it's short tasks on the same thread.
620
-
621
- The thread pool is there to help slow user code.
622
-
623
- The slower your application code, the more threads you will need to keep the server running in a responsive manner (note that responsiveness and speed aren't always the same).
624
-
625
- To make a thread pool easier and safer to use, iodine makes sure that no connection task / callback is called concurrently for the same connection.
626
-
627
- For example, a is a WebSocket connection is already busy in it's `on_message` callback, no other messages will be forwarded to the callback until the current callback returns.
628
-
629
- ## Free, as in freedom (BYO beer)
630
-
631
- Iodine is **free** and **open source**, so why not take it out for a spin?
632
-
633
- It's installable just like any other gem on Ruby MRI, run:
634
-
635
- ```
636
- $ gem install iodine
637
- ```
638
-
639
- If building the native C extension fails, please note that some Ruby installations, such as on Ubuntu, require that you separately install the development headers (`ruby.h` and friends). I have no idea why they do that, as you will need the development headers for any native gems you want to install - so hurry up and get them.
640
-
641
- If you have the development headers but still can't compile the iodine extension, [open an issue](https://github.com/boazsegev/iodine/issues) with any messages you're getting and I'll be happy to look into it.
642
-
643
- ## Mr. Sandman, write me a server
644
-
645
- Iodine allows custom TCP/IP server authoring, for those cases where we need raw TCP/IP (UDP isn't supported just yet).
646
-
647
- Here's a short and sweet echo server - No HTTP, just use `telnet`:
648
-
649
- ```ruby
650
- USE_TLS = false
651
-
652
- require 'iodine'
653
-
654
- # an echo protocol with asynchronous notifications.
655
- class EchoProtocol
656
- # `on_message` is called when data is available.
657
- def on_message client, buffer
658
- # writing will never block and will use a buffer written in C when needed.
659
- client.write buffer
660
- # close will be performed only once all the data in the write buffer
661
- # was sent. use `force_close` to close early.
662
- client.close if buffer =~ /^bye[\r\n]/i
663
- # run asynchronous tasks... after a set number of milliseconds
664
- Iodine.run_after(1000) do
665
- # or schedule the task immediately
666
- Iodine.run do
667
- puts "Echoed data: #{buffer}"
668
- end
669
- end
670
- end
671
- end
672
-
673
- tls = USE_TLS ? Iodine::TLS.new("localhost") : nil
674
-
675
- # listen on port 3000 for the echo protocol.
676
- Iodine.listen(port: "3000", tls: tls) { EchoProtocol.new }
677
- Iodine.threads = 1
678
- Iodine.workers = 1
679
- Iodine.start
680
- ```
681
-
682
- Or a nice plain text chat room (connect using `telnet` or `nc` ):
683
-
684
- ```ruby
685
- require 'iodine'
686
-
687
- # a chat protocol with asynchronous notifications.
688
- class ChatProtocol
689
- def initialize nickname = "guest"
690
- @nickname = nickname
691
- end
692
- def on_open client
693
- client.subscribe :chat
694
- client.publish :chat, "#{@nickname} joined chat.\n"
695
- client.timeout = 40
696
- end
697
- def on_close client
698
- client.publish :chat, "#{@nickname} left chat.\n"
699
- end
700
- def on_shutdown client
701
- client.write "Server is shutting down... try reconnecting later.\n"
702
- end
703
- def on_message client, buffer
704
- if(buffer[-1] == "\n")
705
- client.publish :chat, "#{@nickname}: #{buffer}"
706
- else
707
- client.publish :chat, "#{@nickname}: #{buffer}\n"
708
- end
709
- # close will be performed only once all the data in the outgoing buffer
710
- client.close if buffer =~ /^bye[\r\n]/i
711
- end
712
- def ping client
713
- client.write "(ping) Are you there, #{@nickname}...?\n"
714
- end
715
- end
716
-
717
- # an initial login protocol
718
- class LoginProtocol
719
- def on_open client
720
- client.write "Enter nickname to log in to chat room:\n"
721
- client.timeout = 10
722
- end
723
- def ping client
724
- client.write "Time's up... goodbye.\n"
725
- client.close
726
- end
727
- def on_message client, buffer
728
- # validate nickname and switch connection callback to ChatProtocol
729
- nickname = buffer.split("\n")[0]
730
- while (nickname && nickname.length() > 0 && (nickname[-1] == '\n' || nickname[-1] == '\r'))
731
- nickname = nickname.slice(0, nickname.length() -1)
732
- end
733
- if(nickname && nickname.length() > 0 && buffer.split("\n").length() == 1)
734
- chat = ChatProtocol.new(nickname)
735
- client.handler = chat
736
- else
737
- client.write "Nickname error, try again.\n"
738
- on_open client
739
- end
740
- end
741
- end
742
-
743
- # listen on port 3000
744
- Iodine.listen(port: 3000) { LoginProtocol.new }
745
- Iodine.threads = 1
746
- Iodine.workers = 1
747
- Iodine.start
748
- ```
749
-
750
- ### Why not EventMachine?
751
-
752
- EventMachine attempts to give the developer access to the network layer while Iodine attempts to abstract the network layer away and offer the developer a distraction free platform.
753
-
754
- You can go ahead and use EventMachine if you like. They're doing amazing work on that one and it's been used a lot in Ruby-land... really, tons of good developers and people on that project.
755
-
756
- But why not take iodine out for a spin and see for yourself?
757
-
758
- ## Can I contribute?
759
-
760
- Yes, please, here are some thoughts:
761
-
762
- * I'm really not good at writing automated tests and benchmarks, any help would be appreciated. I keep testing manually and that's less then ideal (and it's mistake prone).
763
-
764
- * PRs or issues related to [the `facil.io` C framework](https://github.com/boazsegev/facil.io) should be placed in [the `facil.io` repository](https://github.com/boazsegev/facil.io).
765
-
766
- * Bug reports and pull requests are welcome on GitHub at https://github.com/boazsegev/iodine.
767
-
768
- * If we can write a Java wrapper for [the `facil.io` C framework](https://github.com/boazsegev/facil.io), it would be nice... but it could be as big a project as the whole gem, as a lot of minor details are implemented within the bridge between these two languages.
769
-
770
- * If you love the project or thought the code was nice, maybe helped you in your own project, drop me a line. I'd love to know.
771
-
772
- ### Running the Tests
773
-
774
- Running this task will compile the C extensions then run RSpec tests:
775
-
776
- ```sh
777
- bundle exec rake spec
778
- ```
779
-
780
- ## License
781
-
782
- The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
1
+ # iodine - Why Settle for a fast HTTP / WebSocket Server with native Pub/Sub?
2
+
3
+ [![Gem](https://img.shields.io/gem/dt/iodine.svg)](https://rubygems.org/gems/iodine)
4
+ [![Build Status](https://github.com/boazsegev/iodine/actions/workflows/ruby.yml/badge.svg)](https://github.com/boazsegev/iodine/actions/workflows/ruby.yml)
5
+ [![Gem Version](https://badge.fury.io/rb/iodine.svg)](https://badge.fury.io/rb/iodine)
6
+ [![Inline docs](http://inch-ci.org/github/boazsegev/iodine.svg?branch=master)](http://www.rubydoc.info/github/boazsegev/iodine/master/frames)
7
+ [![GitHub](https://img.shields.io/badge/GitHub-Open%20Source-blue.svg)](https://github.com/boazsegev/iodine)
8
+
9
+ [![Logo](https://github.com/boazsegev/iodine/raw/master/logo.png)](https://github.com/boazsegev/iodine)
10
+
11
+ 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.
12
+
13
+ Iodine is a Ruby wrapper for many of the [facil.io](https://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, free to concentrate on your application logic.
14
+
15
+ Iodine includes native support for:
16
+
17
+ * HTTP, WebSockets and EventSource (SSE) Services (server);
18
+ * WebSocket connections (server / client);
19
+ * Pub/Sub (with optional Redis Pub/Sub scaling);
20
+ * Fast(!) builtin Mustache template engine.
21
+ * Static file service (with automatic `gzip` support for pre-compressed assets);
22
+ * Optimized Logging to `stderr`.
23
+ * Asynchronous event scheduling and timers;
24
+ * HTTP/1.1 keep-alive and pipelining;
25
+ * Heap Fragmentation Protection.
26
+ * TLS 1.2 and above (Requires OpenSSL >= 1.1.0);
27
+ * TCP/IP server and client connectivity;
28
+ * Unix Socket server and client connectivity;
29
+ * Hot Restart (using the USR1 signal and without hot deployment);
30
+ * Custom protocol authoring;
31
+ * [Sequel](https://github.com/jeremyevans/sequel) and ActiveRecord forking protection.
32
+ * and more!
33
+
34
+ Since iodine wraps much of the [C facil.io framework](https://github.com/boazsegev/facil.io) to Ruby:
35
+
36
+ * Iodine can handle **thousands of concurrent connections** (tested with more then 20K connections on Linux)!
37
+
38
+ * 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).
39
+
40
+ 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.
41
+
42
+ **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 the iodine's thread until it's finished and iodine won't send any data before the loop is done.
43
+
44
+ ## Iodine - a fast & powerful HTTP + WebSockets server with native Pub/Sub
45
+
46
+ Iodine includes a light and fast HTTP and Websocket server written in C that was written according to the [Rack interface specifications](http://www.rubydoc.info/github/rack/rack/master/file/SPEC) and the [Websocket draft extension](./SPEC-Websocket-Draft.md).
47
+
48
+ With `Iodine.listen service: :http` it's possible to run multiple HTTP applications (please remember not to set more than a single application on a single TCP/IP port).
49
+
50
+ Iodine also supports native process cluster Pub/Sub and a native RedisEngine to easily scale iodine's Pub/Sub horizontally.
51
+
52
+ ### Known Issues and Reporting Issues
53
+
54
+ See the [GitHub Open Issues](https://github.com/boazsegev/iodine/issues) list for known issues and to report new issues.
55
+
56
+ ### Installing and Running Iodine
57
+
58
+ Install iodine on any Linux / BSD / macOS system using:
59
+
60
+ ```bash
61
+ gem install iodine
62
+ ```
63
+
64
+ Using the iodine server is easy, simply add iodine as a gem to your Rails / Sinatra / Rack application's `Gemfile`:
65
+
66
+ ```ruby
67
+ gem 'iodine', '~>0.7'
68
+ ```
69
+
70
+ Then start your application from the command-line / terminal using iodine:
71
+
72
+ ```bash
73
+ bundler exec iodine
74
+ ```
75
+
76
+ #### Installing with SSL/TLS
77
+
78
+ **Note**: iodine has known issues with the TLS/SSL support. TLS/SSL should **NOT** be used in production (see issues #95 and #94).
79
+
80
+ Make sure to update OpenSSL to the latest version **before installing Ruby** (`rbenv` should do this automatically).
81
+
82
+ 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.
83
+
84
+ Verbose installation should provide a confirmation message, such as:
85
+
86
+ ```bash
87
+ $ gem install iodine -f -V
88
+ ...
89
+ checking for -lcrypto... yes
90
+ checking for -lssl... yes
91
+ Detected OpenSSL library, testing for version.
92
+ Confirmed OpenSSL to be version 1.1.0 or above (OpenSSL 1.1.0j 20 Nov 2018)...
93
+ * Compiling with HAVE_OPENSSL.
94
+ ...
95
+ ```
96
+
97
+ 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.:
98
+
99
+ ```bash
100
+ NO_SSL=1 bundler exec iodine
101
+ ```
102
+
103
+ ### Running with Rails
104
+
105
+ On Rails:
106
+
107
+ 1. Replace the `puma` gem with the `iodine` gem.
108
+
109
+ 1. Remove the `config/puma.rb` file (or comment out the code).
110
+
111
+ 1. Optionally, it's possible to add a `config/initializers/iodine.rb` file. For example:
112
+
113
+ ```ruby
114
+ # Iodine setup - use conditional setup to allow command-line arguments to override these:
115
+ if(defined?(Iodine))
116
+ Iodine.threads = ENV.fetch("RAILS_MAX_THREADS", 5).to_i if Iodine.threads.zero?
117
+ Iodine.workers = ENV.fetch("WEB_CONCURRENCY", 2).to_i if Iodine.workers.zero?
118
+ Iodine::DEFAULT_SETTINGS[:port] ||= ENV.fetch("PORT") if ENV.fetch("PORT")
119
+ end
120
+ ```
121
+
122
+ 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.
123
+
124
+ **Note**: command-line instructions (CLI) should be the preferred way for configuring iodine, allowing for code-less configuration updates.
125
+
126
+ ### Optimizing Iodine's Concurrency
127
+
128
+ To get the most out of iodine, consider the amount of CPU cores available and the concurrency level the application requires.
129
+
130
+ 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.
131
+
132
+ Command line arguments allow easy access to different options, including concurrency levels. i.e., to set up 16 threads and 4 processes:
133
+
134
+ ```bash
135
+ bundler exec iodine -p $PORT -t 16 -w 4
136
+ ```
137
+
138
+ The environment variables `THREADS` and `WORKERS` are automatically recognized when iodine is first required, allowing environment specific customization. i.e.:
139
+
140
+ ```bash
141
+ export THREADS=16
142
+ export WORKERS=-1 # negative values are fractions of CPU cores.
143
+ bundler exec iodine -p $PORT
144
+ ```
145
+
146
+ 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:
147
+
148
+ ```bash
149
+ bundler exec iodine -p $PORT -t 2 -w -2
150
+ ```
151
+
152
+ ### Heap Fragmentation Protection
153
+
154
+ Iodine includes a fast, network oriented, custom memory allocator, optimizing away some of the work usually placed on the Ruby Garbage Collector (GC).
155
+
156
+ This approach helps to minimize heap fragmentation for long running processes, by grouping many short-lived objects into a common memory space.
157
+
158
+ It is still recommended to consider [jemalloc](http://jemalloc.net) or other allocators that also help mitigate heap fragmentation issues.
159
+
160
+ ### Static file serving support
161
+
162
+ Iodine supports an internal static file service that bypasses the Ruby layer and serves static files directly from "C-land".
163
+
164
+ 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.
165
+
166
+ Since the Ruby layer is unaware of these requests, logging can be performed by turning iodine's logger on.
167
+
168
+ To use native static file service, setup the public folder's address **before** starting the server.
169
+
170
+ This can be done when starting the server from the command line:
171
+
172
+ ```bash
173
+ bundler exec iodine -p $PORT -t 16 -w 4 -www /my/public/folder
174
+ ```
175
+
176
+ Or using a simple Ruby script. i.e. (a `my_server.rb` example):
177
+
178
+ ```ruby
179
+ require 'iodine'
180
+ # static file service
181
+ Iodine.listen, service: :http, public: '/my/public/folder'
182
+ # for static file service, we only need a single thread and a single worker.
183
+ Iodine.threads = 1
184
+ Iodine.start
185
+ ```
186
+
187
+ To enable logging from the command line, use the `-v` (verbose) option:
188
+
189
+ ```bash
190
+ bundler exec iodine -p $PORT -t 16 -w 4 -www /my/public/folder -v
191
+ ```
192
+
193
+ #### X-Sendfile
194
+
195
+ 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.
196
+
197
+ This allows Ruby to send very large files using a very small memory footprint and usually leverages the `sendfile` system call.
198
+
199
+ i.e. (example `config.ru` for iodine):
200
+
201
+ ```ruby
202
+ app = proc do |env|
203
+ request = Rack::Request.new(env)
204
+ if request.path_info == '/source'.freeze
205
+ [200, { 'X-Sendfile' => File.expand_path(__FILE__), 'Content-Type' => 'text/plain'}, []]
206
+ elsif request.path_info == '/file'.freeze
207
+ [200, { 'X-Header' => 'This was a Rack::Sendfile response sent as text.' }, File.open(__FILE__)]
208
+ else
209
+ [200, { 'Content-Type' => 'text/html',
210
+ 'Content-Length' => request.path_info.length.to_s },
211
+ [request.path_info]]
212
+ end
213
+ end
214
+ # # optional:
215
+ # use Rack::Sendfile
216
+ run app
217
+ ```
218
+
219
+ Benchmark [localhost:3000/source](http://localhost:3000/source) to experience the `X-Sendfile` extension at work.
220
+
221
+ #### Pre-Compressed assets / files
222
+
223
+ Rails does this automatically when compiling assets, which is: `gzip` your static files.
224
+
225
+ Iodine will automatically recognize and send the `gz` version if the client (browser) supports the `gzip` transfer-encoding.
226
+
227
+ For example, to offer a compressed version of `style.css`, run (in the terminal):
228
+
229
+ ```bash
230
+ $ gzip -k -9 style.css
231
+ ```
232
+
233
+ This results in both files, `style.css` (the original) and `style.css.gz` (the compressed).
234
+
235
+ 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.
236
+
237
+ It's as easy as that. No extra code required.
238
+
239
+ ### Special HTTP `Upgrade` and SSE support
240
+
241
+ Iodine's HTTP server implements the [WebSocket/SSE Rack Specification Draft](SPEC-Websocket-Draft.md), supporting native WebSocket/SSE connections using Rack's `env` Hash.
242
+
243
+ 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.
244
+
245
+ 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']`.
246
+
247
+ #### EventSource / SSE
248
+
249
+ Iodine treats EventSource / SSE connections as if they were a half-duplex WebSocket connection, using the exact same API and callbacks as WebSockets.
250
+
251
+ When an EventSource / SSE request is received, iodine will set the Rack Hash's upgrade property to `:sse`, so that: `env['rack.upgrade?'] == :sse`.
252
+
253
+ The rest is detailed in the WebSocket support section.
254
+
255
+ #### WebSockets
256
+
257
+ When a WebSocket connection request is received, iodine will set the Rack Hash's upgrade property to `:websocket`, so that: `env['rack.upgrade?'] == :websocket`
258
+
259
+ 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)`
260
+
261
+ 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.
262
+
263
+ Here is a simple chat-room example we can run in the terminal (`irb`) or easily paste into a `config.ru` file:
264
+
265
+ ```ruby
266
+ require 'iodine'
267
+ module WebsocketChat
268
+ def on_open client
269
+ # Pub/Sub directly to the client (or use a block to process the messages)
270
+ client.subscribe :chat
271
+ # Writing directly to the socket
272
+ client.write "You're now in the chatroom."
273
+ end
274
+ def on_message client, data
275
+ # Strings and symbol channel names are equivalent.
276
+ client.publish "chat", data
277
+ end
278
+ extend self
279
+ end
280
+ APP = Proc.new do |env|
281
+ if env['rack.upgrade?'.freeze] == :websocket
282
+ env['rack.upgrade'.freeze] = WebsocketChat
283
+ [0,{}, []] # It's possible to set cookies for the response.
284
+ elsif env['rack.upgrade?'.freeze] == :sse
285
+ puts "SSE connections can only receive data from the server, the can't write."
286
+ env['rack.upgrade'.freeze] = WebsocketChat
287
+ [0,{}, []] # It's possible to set cookies for the response.
288
+ else
289
+ [200, {"Content-Length" => "12", "Content-Type" => "text/plain"}, ["Welcome Home"] ]
290
+ end
291
+ end
292
+ # Pus/Sub can be server oriented as well as connection bound
293
+ Iodine.subscribe(:chat) {|ch, msg| puts msg if Iodine.master? }
294
+ # By default, Pub/Sub performs in process cluster mode.
295
+ Iodine.workers = 4
296
+ # # in irb:
297
+ Iodine.listen service: :http, public: "www/public", handler: APP
298
+ Iodine.start
299
+ # # or in config.ru
300
+ run APP
301
+ ```
302
+
303
+ ### Native Pub/Sub with *optional* Redis scaling
304
+
305
+ Iodine's core, `facil.io` offers a native Pub/Sub implementation that can be scaled across machine boundaries using Redis.
306
+
307
+ The default implementation covers the whole process cluster, so a single cluster doesn't need Redis
308
+
309
+ 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.:
310
+
311
+ ```bash
312
+ $ iodine -w -1 -t 8 -r redis://localhost
313
+ ```
314
+
315
+ It's also possible to initialize the iodine<=>Redis link using Ruby, directly from the application's code:
316
+
317
+ ```ruby
318
+ # initialize the Redis engine for each iodine process.
319
+ if ENV["REDIS_URL"]
320
+ Iodine::PubSub.default = Iodine::PubSub::Redis.new(ENV["REDIS_URL"])
321
+ else
322
+ puts "* No Redis, it's okay, pub/sub will still run on the whole process cluster."
323
+ end
324
+ # ... the rest of the application remains unchanged.
325
+ ```
326
+
327
+ Iodine's Redis client can also be used for asynchronous Redis command execution. i.e.:
328
+
329
+ ```ruby
330
+ if(Iodine::PubSub.default.is_a? Iodine::PubSub::Redis)
331
+ # Ask Redis about all it's client connections and print out the reply.
332
+ Iodine::PubSub.default.cmd("CLIENT LIST") { |reply| puts reply }
333
+ end
334
+ ```
335
+
336
+ **Pub/Sub Details and Limitations:**
337
+
338
+ * Iodine's Redis client does *not* support multiple databases. This is both because [database scoping is ignored by Redis during pub/sub](https://redis.io/topics/pubsub#database-amp-scoping) and because [Redis Cluster doesn't support multiple databases](https://redis.io/topics/cluster-spec). This indicated that multiple database support just isn't worth the extra effort and performance hit.
339
+
340
+ * The iodine Redis client will use two Redis connections for the whole process cluster (a single publishing connection and a single subscription connection), minimizing the Redis load and network bandwidth.
341
+
342
+ * Connections will be automatically re-established if timeouts or errors occur.
343
+
344
+ ### Hot Restart
345
+
346
+ Iodine will "hot-restart" the application by shutting down and re-spawning the worker processes.
347
+
348
+ This will clear away any memory fragmentation concerns and other issues that might plague a long running worker process or ruby application.
349
+
350
+ To hot-restart iodine, send the `SIGUSR1` signal to the root process.
351
+
352
+ The following code will hot-restart iodine every 4 hours when iodine is running in cluster mode:
353
+
354
+ ```ruby
355
+ Iodine.run_every(4 * 60 * 60 * 1000) do
356
+ Process.kill("SIGUSR1", Process.pid) unless Iodine.worker?
357
+ end
358
+ ```
359
+
360
+ Since the master / root process doesn't handle any requests (it only handles pub/sub and house-keeping), it's memory map and process data shouldn't be as affected and the new worker processes should be healthier and more performant.
361
+
362
+ **Note**: This will **not** re-load the application (any changes to the Ruby code require an actual restart).
363
+
364
+ ### Optimized HTTP logging
365
+
366
+ By default, iodine is pretty quiet. Some messages are logged to `stderr`, but not many.
367
+
368
+ However, HTTP requests can be logged using iodine's optimized logger to `stderr`. Iodine will optimize the log output by caching the output time string which updates every second rather than every request.
369
+
370
+ This can be performed by setting the `-v` flag during startup, i.e.:
371
+
372
+ ```bash
373
+ bundler exec iodine -p $PORT -t 16 -w 4 -v -www /my/public/folder
374
+ ```
375
+
376
+ The log output can be redirected to a file:
377
+
378
+ ```bash
379
+ bundler exec iodine -p $PORT -v 2>my_log.log
380
+ ```
381
+
382
+ The log output can also be redirected to a `stdout`:
383
+
384
+ ```bash
385
+ bundler exec iodine -p $PORT -v 2>&1
386
+ ```
387
+
388
+ ### Built-in support for Sequel and ActiveRecord
389
+
390
+ It's a well known fact that [Database connections require special attention when using `fork`-ing servers (multi-process servers)](https://devcenter.heroku.com/articles/concurrency-and-database-connections#multi-process-servers) such as Puma, Passenger (Pro) and iodine.
391
+
392
+ However, it's also true that [these issues go unnoticed by many developers](https://stackoverflow.com/a/45570999/4025095), since application developers are (rightfully) focused on the application rather than the infrastructure.
393
+
394
+ With iodine, there's no need to worry.
395
+
396
+ Iodine provides built-in `fork` handling for both ActiveRecord and [Sequel](https://github.com/jeremyevans/sequel), in order to protect against these possible errors.
397
+
398
+ ### Client Support
399
+
400
+ Iodine supports raw (TCP/IP and Unix Sockets) client connections as well as WebSocket connections.
401
+
402
+ This can be utilized for communicating across micro services or taking advantage of persistent connection APIs such as ActionCable APIs, socket.io APIs etc'.
403
+
404
+ Here is an example WebSocket client that will connect to the [WebSocket.org echo test service](https://www.websocket.org/echo.html) and send a number of pre-programmed messages.
405
+
406
+ ```ruby
407
+ require 'iodine'
408
+
409
+ # The client class
410
+ class EchoClient
411
+
412
+ def on_open(connection)
413
+ @messages = [ "Hello World!",
414
+ "I'm alive and sending messages",
415
+ "I also receive messages",
416
+ "now that we all know this...",
417
+ "I can stop.",
418
+ "Goodbye." ]
419
+ send_one_message(connection)
420
+ end
421
+
422
+ def on_message(connection, message)
423
+ puts "Received: #{message}"
424
+ send_one_message(connection)
425
+ end
426
+
427
+ def on_close(connection)
428
+ # in this example, we stop iodine once the client is closed
429
+ puts "* Client closed."
430
+ Iodine.stop
431
+ end
432
+
433
+ # We use this method to pop messages from the queue and send them
434
+ #
435
+ # When the queue is empty, we disconnect the client.
436
+ def send_one_message(connection)
437
+ msg = @messages.shift
438
+ if(msg)
439
+ connection.write msg
440
+ else
441
+ connection.close
442
+ end
443
+ end
444
+ end
445
+
446
+ Iodine.threads = 1
447
+ Iodine.connect url: "wss://echo.websocket.org", handler: EchoClient.new, ping: 40
448
+ Iodine.start
449
+ ```
450
+
451
+ ### TLS >= 1.2 support
452
+
453
+ > Requires OpenSSL >= `1.1.0`. On Heroku, requires `heroku-18`.
454
+
455
+ Iodine supports secure connections fore TLS version 1.2 **and up** (depending on the OpenSSL version).
456
+
457
+ A self signed certificate is available using the `-tls` flag from the command-line.
458
+
459
+ PEM encoded certificates (which is probably the most common format) can be loaded from the command-line (`-tls-cert` and `-tls-key`) or dynamically (using `Iodine::TLS`).
460
+
461
+ The TLS API is simplified but powerful, supporting the ALPN extension and peer verification (which client connections really should leverage).
462
+
463
+ When enabling peer verification for server connections (using `Iodine::TLS#trust`), clients will be required to submit a trusted certificate in order to connect to the server.
464
+
465
+ ### TCP/IP (raw) sockets
466
+
467
+ Upgrading to a custom protocol (i.e., in order to implement your own WebSocket protocol with special extensions) is available when neither WebSockets nor SSE connection upgrades were requested. In the following (terminal) example, we'll use an echo server without direct socket echo:
468
+
469
+ ```ruby
470
+ require 'iodine'
471
+ class MyProtocol
472
+ def on_message client, data
473
+ # regular socket echo - NOT websockets
474
+ client.write data
475
+ end
476
+ end
477
+ APP = Proc.new do |env|
478
+ if env["HTTP_UPGRADE".freeze] =~ /echo/i.freeze
479
+ env['upgrade.tcp'.freeze] = MyProtocol.new
480
+ # an HTTP response will be sent before changing protocols.
481
+ [101, { "Upgrade" => "echo" }, []]
482
+ else
483
+ [200, {"Content-Length" => "12", "Content-Type" => "text/plain"}, ["Welcome Home"] ]
484
+ end
485
+ end
486
+ # # in irb:
487
+ Iodine.listen service: :http, public: "www/public", handler: APP
488
+ Iodine.threads = 1
489
+ Iodine.start
490
+ # # or in config.ru
491
+ run APP
492
+ ```
493
+
494
+ ### How does it compare to other servers?
495
+
496
+ In my tests, pitching Iodine against Puma, Iodine was anywhere between x1.5 and more than x10 faster than Puma (depending on use-case and settings).
497
+
498
+ Such a big difference is suspect and I recommend that you test it yourself - even better if you test performance using your own application and a number of possible different settings (how many threads per CPU core? how many worker processes? middleware vs. server request logging, etc').
499
+
500
+ I recommend benchmarking the performance for yourself using `wrk` or `ab`:
501
+
502
+ ```bash
503
+ $ wrk -c200 -d4 -t2 http://localhost:3000/
504
+ # or
505
+ $ ab -n 100000 -c 200 -k http://127.0.0.1:3000/
506
+ ```
507
+
508
+ The best application to use for benchmarking is your actual application. Or, you could create a simple `config.ru` file with a __hello world__ app:
509
+
510
+ ```ruby
511
+ App = Proc.new do |env|
512
+ [200,
513
+ { "Content-Type" => "text/html".freeze,
514
+ "Content-Length" => "16".freeze },
515
+ ['Hello from Rack!'.freeze] ]
516
+ end
517
+
518
+ run App
519
+ ```
520
+
521
+ Then start comparing servers. Here are the settings I used to compare iodine and Puma (4 processes, 4 threads):
522
+
523
+ ```bash
524
+ $ RACK_ENV=production iodine -p 3000 -t 4 -w 4
525
+ # vs.
526
+ $ RACK_ENV=production puma -p 3000 -t 4 -w 4
527
+ # Review the `iodine -?` help for more command line options.
528
+ ```
529
+
530
+ It's recommended that the servers (Iodine/Puma) and the client (`wrk`/`ab`) run on separate machines.
531
+
532
+ It is worth noting that iodine can also speed up logging by replacing the logging middleware with `iodine -v`. This approach uses less memory and improves performance at the expense of fuzzy timing and some string caching.
533
+
534
+ On my machine, testing with the logging functionality enabled, iodine was more then 10 times faster than puma (60.9K req/sec vs. 5.3K req/sec)
535
+
536
+ ### A few notes
537
+
538
+ Iodine's upgrade / callback design has a number of benefits, some of them related to better IO handling, resource optimization (no need for two IO polling systems), etc. This also allows us to use middleware without interfering with connection upgrades and provides backwards compatibility.
539
+
540
+ Iodine's HTTP server imposes a few restrictions for performance and security reasons, such as limiting each header line to 8Kb. These restrictions shouldn't be an issue and are similar to limitations imposed by Apache or Nginx.
541
+
542
+ If you still want to use Rack's `hijack` API, iodine will support you - but be aware that you will need to implement your own reactor and thread pool for any sockets you hijack, as well as a socket buffer for non-blocking `write` operations (why do that when you can write a protocol object and have the main reactor manage the socket?).
543
+
544
+ ## Installation
545
+
546
+ To install iodine, simply install the the `iodine` gem:
547
+
548
+ ```bash
549
+ $ gem install iodine
550
+ ```
551
+
552
+ Iodine is written in C and allows some compile-time customizations, such as:
553
+
554
+ * `FIO_FORCE_MALLOC` - avoids iodine's custom memory allocator and use `malloc` instead (mostly used when debugging iodine or when using a different memory allocator).
555
+
556
+ * `FIO_MAX_SOCK_CAPACITY` - limits iodine's maximum client capacity. Defaults to 131,072 clients.
557
+
558
+ * `FIO_USE_RISKY_HASH` - replaces SipHash with RiskyHash for iodine's internal hash maps.
559
+
560
+ Since iodine hash maps have internal protection against collisions and hash flooding attacks, it's possible for iodine to leverage RiskyHash, which is faster than SipHash.
561
+
562
+ By default, SipHash will be used. This is a community related choice, since the community seems to believe a hash function should protect the hash map rather than it being enough for a hash map implementation to be attack resistance.
563
+
564
+ * `HTTP_MAX_HEADER_COUNT` - limits the number of headers the HTTP server will accept before disconnecting a client (security). Defaults to 128 headers (permissive).
565
+
566
+ * `HTTP_MAX_HEADER_LENGTH` - limits the number of bytes allowed for a single header (pre-allocated memory per connection + security). Defaults to 8Kb per header line (normal).
567
+
568
+ * `HTTP_BUSY_UNLESS_HAS_FDS` - requires at least X number of free file descriptors (for new database connections, etc') before accepting a new HTTP client.
569
+
570
+ * `FIO_ENGINE_POLL` - prefer the `poll` system call over `epoll` or `kqueue` (not recommended).
571
+
572
+ * `FIO_LOG_LENGTH_LIMIT` - sets the limit on iodine's logging messages (uses stack memory, so limits must be reasonable. Defaults to 2048.
573
+
574
+ * `FIO_TLS_PRINT_SECRET` - if true, the OpenSSL master key will be printed as debug message level log. Use only for testing (with WireShark etc'), never in production! Default: false.
575
+
576
+ These options can be used, for example, like so:
577
+
578
+ ```bash
579
+ gem install iodine -- \
580
+ --with-cflags=\"-DHTTP_MAX_HEADER_LENGTH=48000 -DFIO_FORCE_MALLOC=1 -DHTTP_MAX_HEADER_COUNT=64\"
581
+ ```
582
+
583
+ More possible compile time options can be found in the [facil.io documentation](http://facil.io).
584
+
585
+ ## Evented oriented design with extra safety
586
+
587
+ Iodine is an evented server, similar in its architecture to `nginx` and `puma`. It's different than the simple "thread-per-client" design that is often taught when we begin to learn about network programming.
588
+
589
+ By leveraging `epoll` (on Linux) and `kqueue` (on BSD), iodine can listen to multiple network events on multiple sockets using a single thread.
590
+
591
+ All these events go into a task queue, together with the application events and any user generated tasks, such as ones scheduled by [`Iodine.run`](http://www.rubydoc.info/github/boazsegev/iodine/Iodine#run-class_method).
592
+
593
+ In pseudo-code, this might look like this
594
+
595
+ ```ruby
596
+ QUEUE = Queue.new
597
+
598
+ def server_cycle
599
+ if(QUEUE.empty?)
600
+ QUEUE << get_next_32_socket_events # these events schedule the proper user code to run
601
+ end
602
+ QUEUE << server_cycle
603
+ end
604
+
605
+ def run_server
606
+ while ((event = QUEUE.pop))
607
+ event.shift.call(*event)
608
+ end
609
+ end
610
+ ```
611
+
612
+ In pure Ruby (without using C extensions or Java), it's possible to do the same by using `select`... and although `select` has some issues, it could work well for lighter loads.
613
+
614
+ The server events are fairly fast and fragmented (longer code is fragmented across multiple events), so one thread is enough to run the server including it's static file service and everything...
615
+
616
+ ...but single threaded mode should probably be avoided.
617
+
618
+
619
+ It's very common that the application's code will run slower and require external resources (i.e., databases, a custom pub/sub service, etc'). This slow code could "starve" the server, which is patiently waiting to run it's short tasks on the same thread.
620
+
621
+ The thread pool is there to help slow user code.
622
+
623
+ The slower your application code, the more threads you will need to keep the server running in a responsive manner (note that responsiveness and speed aren't always the same).
624
+
625
+ To make a thread pool easier and safer to use, iodine makes sure that no connection task / callback is called concurrently for the same connection.
626
+
627
+ For example, a is a WebSocket connection is already busy in it's `on_message` callback, no other messages will be forwarded to the callback until the current callback returns.
628
+
629
+ ## Free, as in freedom (BYO beer)
630
+
631
+ Iodine is **free** and **open source**, so why not take it out for a spin?
632
+
633
+ It's installable just like any other gem on Ruby MRI, run:
634
+
635
+ ```
636
+ $ gem install iodine
637
+ ```
638
+
639
+ If building the native C extension fails, please note that some Ruby installations, such as on Ubuntu, require that you separately install the development headers (`ruby.h` and friends). I have no idea why they do that, as you will need the development headers for any native gems you want to install - so hurry up and get them.
640
+
641
+ If you have the development headers but still can't compile the iodine extension, [open an issue](https://github.com/boazsegev/iodine/issues) with any messages you're getting and I'll be happy to look into it.
642
+
643
+ ## Mr. Sandman, write me a server
644
+
645
+ Iodine allows custom TCP/IP server authoring, for those cases where we need raw TCP/IP (UDP isn't supported just yet).
646
+
647
+ Here's a short and sweet echo server - No HTTP, just use `telnet`:
648
+
649
+ ```ruby
650
+ USE_TLS = false
651
+
652
+ require 'iodine'
653
+
654
+ # an echo protocol with asynchronous notifications.
655
+ class EchoProtocol
656
+ # `on_message` is called when data is available.
657
+ def on_message client, buffer
658
+ # writing will never block and will use a buffer written in C when needed.
659
+ client.write buffer
660
+ # close will be performed only once all the data in the write buffer
661
+ # was sent. use `force_close` to close early.
662
+ client.close if buffer =~ /^bye[\r\n]/i
663
+ # run asynchronous tasks... after a set number of milliseconds
664
+ Iodine.run_after(1000) do
665
+ # or schedule the task immediately
666
+ Iodine.run do
667
+ puts "Echoed data: #{buffer}"
668
+ end
669
+ end
670
+ end
671
+ end
672
+
673
+ tls = USE_TLS ? Iodine::TLS.new("localhost") : nil
674
+
675
+ # listen on port 3000 for the echo protocol.
676
+ Iodine.listen(port: "3000", tls: tls) { EchoProtocol.new }
677
+ Iodine.threads = 1
678
+ Iodine.workers = 1
679
+ Iodine.start
680
+ ```
681
+
682
+ Or a nice plain text chat room (connect using `telnet` or `nc` ):
683
+
684
+ ```ruby
685
+ require 'iodine'
686
+
687
+ # a chat protocol with asynchronous notifications.
688
+ class ChatProtocol
689
+ def initialize nickname = "guest"
690
+ @nickname = nickname
691
+ end
692
+ def on_open client
693
+ client.subscribe :chat
694
+ client.publish :chat, "#{@nickname} joined chat.\n"
695
+ client.timeout = 40
696
+ end
697
+ def on_close client
698
+ client.publish :chat, "#{@nickname} left chat.\n"
699
+ end
700
+ def on_shutdown client
701
+ client.write "Server is shutting down... try reconnecting later.\n"
702
+ end
703
+ def on_message client, buffer
704
+ if(buffer[-1] == "\n")
705
+ client.publish :chat, "#{@nickname}: #{buffer}"
706
+ else
707
+ client.publish :chat, "#{@nickname}: #{buffer}\n"
708
+ end
709
+ # close will be performed only once all the data in the outgoing buffer
710
+ client.close if buffer =~ /^bye[\r\n]/i
711
+ end
712
+ def ping client
713
+ client.write "(ping) Are you there, #{@nickname}...?\n"
714
+ end
715
+ end
716
+
717
+ # an initial login protocol
718
+ class LoginProtocol
719
+ def on_open client
720
+ client.write "Enter nickname to log in to chat room:\n"
721
+ client.timeout = 10
722
+ end
723
+ def ping client
724
+ client.write "Time's up... goodbye.\n"
725
+ client.close
726
+ end
727
+ def on_message client, buffer
728
+ # validate nickname and switch connection callback to ChatProtocol
729
+ nickname = buffer.split("\n")[0]
730
+ while (nickname && nickname.length() > 0 && (nickname[-1] == '\n' || nickname[-1] == '\r'))
731
+ nickname = nickname.slice(0, nickname.length() -1)
732
+ end
733
+ if(nickname && nickname.length() > 0 && buffer.split("\n").length() == 1)
734
+ chat = ChatProtocol.new(nickname)
735
+ client.handler = chat
736
+ else
737
+ client.write "Nickname error, try again.\n"
738
+ on_open client
739
+ end
740
+ end
741
+ end
742
+
743
+ # listen on port 3000
744
+ Iodine.listen(port: 3000) { LoginProtocol.new }
745
+ Iodine.threads = 1
746
+ Iodine.workers = 1
747
+ Iodine.start
748
+ ```
749
+
750
+ ### Why not EventMachine?
751
+
752
+ EventMachine attempts to give the developer access to the network layer while Iodine attempts to abstract the network layer away and offer the developer a distraction free platform.
753
+
754
+ You can go ahead and use EventMachine if you like. They're doing amazing work on that one and it's been used a lot in Ruby-land... really, tons of good developers and people on that project.
755
+
756
+ But why not take iodine out for a spin and see for yourself?
757
+
758
+ ## Can I contribute?
759
+
760
+ Yes, please, here are some thoughts:
761
+
762
+ * I'm really not good at writing automated tests and benchmarks, any help would be appreciated. I keep testing manually and that's less then ideal (and it's mistake prone).
763
+
764
+ * PRs or issues related to [the `facil.io` C framework](https://github.com/boazsegev/facil.io) should be placed in [the `facil.io` repository](https://github.com/boazsegev/facil.io).
765
+
766
+ * Bug reports and pull requests are welcome on GitHub at https://github.com/boazsegev/iodine.
767
+
768
+ * If we can write a Java wrapper for [the `facil.io` C framework](https://github.com/boazsegev/facil.io), it would be nice... but it could be as big a project as the whole gem, as a lot of minor details are implemented within the bridge between these two languages.
769
+
770
+ * If you love the project or thought the code was nice, maybe helped you in your own project, drop me a line. I'd love to know.
771
+
772
+ ### Running the Tests
773
+
774
+ Running this task will compile the C extensions then run RSpec tests:
775
+
776
+ ```sh
777
+ bundle exec rake spec
778
+ ```
779
+
780
+ ## License
781
+
782
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).