Skip to content

Instantly share code, notes, and snippets.

@boazsegev
Last active March 12, 2018 13:32
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save boazsegev/1466442c913a8dd4271178cab9d98a27 to your computer and use it in GitHub Desktop.
Save boazsegev/1466442c913a8dd4271178cab9d98a27 to your computer and use it in GitHub Desktop.
The Priceless Ruby Websocket
# The Rack Application container
module MyRackApplication
# Rack applications use the `call` callback to handle HTTP requests.
def self.call(env)
# if upgrading...
if env['HTTP_UPGRADE'.freeze] =~ /websocket/i
# We can assign a class or an instance that implements callbacks.
# We will assign an object, passing it the request information (`env`)
env['upgrade.websocket'.freeze] = MyWebsocket.new(env)
# Rack responses must be a 3 item array with [status, {http: :headers}, ["response body"]]
return [0, {}, []]
end
# a semi-regualr HTTP response
out = File.open File.expand_path('../index.html', __FILE__)
[200, { 'X-Sendfile' => File.expand_path('../index.html', __FILE__), 'Content-Length' => out.size }, out]
end
end
# The Websocket Callback Object
class MyWebsocket
# this is optional, but I wanted the object to have the nickname provided in
# the HTTP request
def initialize(env)
# we need to change the ASCI Rack encoding to UTF-8,
# otherwise everything with the nickname will be a binary "blob" in the
# Javascript layer
@nickname = env['PATH_INFO'][1..-1].force_encoding 'UTF-8'
end
# A classic websocket callback, called when the connection is opened and
# linked to this object
def on_open
puts 'We have a websocket connection'
# iodine supports native pub/sub, using subscribe we listen to incoming data on the chat channel
subscribe channel: "chat"
end
# A classic websocket callback, called when the connection is closed
# (after disconnection).
def on_close
puts "Bye Bye... #{count} connections left..."
end
# A server-side niceness, called when the server if shutting down,
# to gracefully disconnect (before disconnection).
def on_shutdown
write 'The server is shutting down, goodbye.'
end
def on_message(data)
puts "got message: #{data} encoded as #{data.encoding}"
# iodine supports native pub/sub, using publish we broadcast the data to "chat"
publish channel: "chat", message: "#{@nickname}: #{data}"
end
end
# iodine supports Redis out of the Box
# this extends the idea of "upgrade.websocket" and allows offloading IO events to the server's IO handling logic
if ENV["REDIS_URL"]
uri = URI(ENV["REDIS_URL"])
Iodine.default_pubsub = Iodine::PubSub::RedisEngine.new(uri.host, uri.port, 0, uri.password)
else
puts "* No Redis, it's okay, pub/sub will support the process cluster."
end
# `run` is a Rack API command, telling Rack where the `call(env)` callback is located.
run MyRackApplication
# The Iodine Server
gem 'iodine'
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script>
ws = NaN
handle = ''
function onsubmit(e) {
e.preventDefault();
if($('#text')[0].value == '') {return false}
if(ws && ws.readyState == 1) {
ws.send($('#text')[0].value);
$('#text')[0].value = '';
} else {
handle = $('#text')[0].value
var url = (window.location.protocol.match(/https/) ? 'wss' : 'ws') +
'://' + window.document.location.host +
'/' + $('#text')[0].value
ws = new WebSocket(url)
ws.onopen = function(e) {
output("<b>Connected :-)</b>");
$('#text')[0].value = '';
$('#text')[0].placeholder = 'your message';
}
ws.onclose = function(e) {
output("<b>Disonnected :-/</b>")
$('#text')[0].value = '';
$('#text')[0].placeholder = 'nickname';
$('#text')[0].value = handle
}
ws.onmessage = function(e) {
output(e.data);
}
}
return false;
}
function output(data) {
$('#output').append("<li>" + data + "</li>")
$('#output').animate({ scrollTop:
$('#output')[0].scrollHeight }, "slow");
}
</script>
<style>
html, body {width:100%; height: 100%; background-color: #ddd; color: #111;}
h3, form {text-align: center;}
input {background-color: #fff; color: #111; padding: 0.3em;}
</style>
</head><body>
<h3>The Ugly Chatroom POC</h3>
<form id='form'>
<input type='text' id='text' name='text' placeholder='nickname'></input>
<input type='submit' value='send'></input>
</form>
<script> $('#form')[0].onsubmit = onsubmit </script>
<ul id='output'></ul>
</body>
</html>

A proof of concept for Rack's env['rack.websocket']

This is a proof of concept for Rack based Websocket connections, showing how the Rack API can be adjusted to support server native real-time connections.

The chosen proof of concept was the ugliest chatroom I could find.

Although my hope is that Rack will adopt the concept and make env['rack.websocket?'] and env['rack.websocket'] part of it's standard, at the moment it's an Iodine specific feature, implemented using env['iodine.websocket'].

Install

Install required gems using:

bundler install

Run

Run this application single threaded:

bundler exec iodine -- -www ./

Or both multi-threaded and forked (you'll notice that pub/sub works across process boundaries, Redis is only required for multi-machine pub/sub).

bundler exec iodine -- -www ./ -t 16 -w 4

Further reading

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment