Few months ago, Openshift announced support for websockets on their site, with nice examples how to use then using the Node.JS. Since using websockets in Node.JS is easy because the Node.JS web server supports them, the situlation in Ruby is a bit more complex.
The Ruby cartridge by default runs Apache with Passenger, which makes implementing websockets a bit tricky. Fortunately, Openshift permits us to replace the default web server with a different server that has support for this new cutting-edge technology. This is a quick tutorial how to make things rolling using Sinatra and Faye and deployed under Puma web server, a modern concurrent web server build for Ruby apps.
First step is to create a new ruby application:
$ rhc app create websockets ruby-1.9
$ cd websockets/
Now is the time to prepare the dependencies in Gemfile
:
source 'https://rubygems.org'
gem 'puma'
gem 'faye-websocket', :require => 'faye/websocket'
gem 'eventmachine'
gem 'sinatra', :require => 'sinatra/base'
Next step is to run bundle
and wait until all dependencies are installed.
After that, we can start modifying the config.ru
. The very first step is to
remove everything from that file and replace it with this content:
Bundler.require(:default)
load './app.rb'
Faye::WebSocket.load_adapter('puma')
# The default OpenShift websockets port is '8000' where in your local
# environment you will use default puma port which is 9292
#
ENV['WEBSOCKET_PORT'] = ENV['OPENSHIFT_RUBY_PORT'].nil? ? '9292' : '8000'
# This is needed to have Puma bind on the right IP address and port:
#
run Rack::URLMap.new("/time" => TimeApp, "/" => WebSocketApp)
The config.ru
is the file used for launching your application. By default, the
config.ru is picked up by Passenger in Apache, we just replaced that so instead
of Passenger your application will be handled by Puma.
Now let’s write the application itself. Open the app.rb
file and put the following
content there:
# This is the WebSocket routing application:
#
WebSocketApp = lambda do |env|
if Faye::WebSocket.websocket?(env)
ws = Faye::WebSocket.new(env)
# When client connects through WebSocket, then start sending the current
# time in the Thread every 1s
ws.on :open do |e|
@clock = Thread.new { loop { ws.send(Time.now.to_s); sleep(1) } }
end
# Kill the thread when client disconnects and remove the websocket
ws.on :close do |e|
Thread.kill(@clock)
ws = nil
end
ws.rack_response
else
# Redirect the client to the Sinatra application, if the request
# is not WebSocket
[301, { 'Location' => '/time'}, []]
end
end
# Puma require the 'log' method for the server request logging:
#
def WebSocketApp.log(message); $stdout.puts message; end
# This is very basic Sinatra app that will just render the HTML with WebSocket
# javascript handling:
#
class TimeApp < Sinatra::Base
get '/' do
erb :index
end
end
Now we need to write the HTML page with a JavaScript piece that will open the
web socket and actually read some data through it. You can grab the content of
the file here and
just save it to views/index.erb
.
The important piece is:
socket = new Socket('ws://' + location.hostname + ':' + '<%=ENV['WEBSOCKET_PORT']%>' + '/')
This will connect the browser to the websocket app and use the ‘right’ port.
So we are almost finished here, but we need to tell OpenShift to replace the
default web server with our Puma. To do so, we need to create the
.openshift/action_hooks/post_start_ruby-1.9
file and put following content in:
#!/bin/bash
echo "Replacing the default Passenger server with Puma"
pushd ${OPENSHIFT_REPO_DIR} > /dev/null
${HOME}/ruby/bin/control stop &> /dev/null
set -e
PUMA_PID_FILE="${OPENSHIFT_DATA_DIR}puma.pid"
PUMA_BIND_URL="tcp://${OPENSHIFT_RUBY_IP}:${OPENSHIFT_RUBY_PORT}"
PUMA_OPTS="-d --pidfile ${PUMA_PID_FILE} -e production --bind '${PUMA_BIND_URL}'"
bundle exec "puma $PUMA_OPTS"
exit 0
Now make it executable with chmod +x .openshift/action_hooks/post_start_ruby-1.9
.
So this will replace the Passenger server, but we also want to handle the
application restarts and make the Puma restart working as well. For that we
need to create the .openshift/action_hooks/deploy
file with the following
content:
#!/bin/bash
if [ -f "${OPENSHIFT_DATA_DIR}puma.pid" ]; then
echo "Stopping Puma..."
PUMA_PID=$(cat "${OPENSHIFT_DATA_DIR}puma.pid")
ps -p $PUMA_PID &> /dev/null
[ "$?" == 0 ] && kill $PUMA_PID
rm "${OPENSHIFT_DATA_DIR}puma.pid" &> /dev/null
fi
Note: I sux in writing Bash scripts, so this could be probably written better ;-)
And now is the time to test out the application we created:
$ git add -A && git commit -m 'Initial commit.'
$ git push
# ... snip ...
remote: Replacing the default Passenger server with Puma
remote: Puma starting in single mode...
remote: * Version 2.4.1, codename: Crunchy Munchy Lunchy
remote: * Min threads: 0, max threads: 16
remote: * Environment: production
remote: * Listening on tcp://127.9.4.1:8080
remote: * Daemonizing...
# ... snip ...
Now open the browser and navigate to your application URL. You should be immediately redirected to ‘/time’ URL and see the current time which updates every second. I know this is a pretty stupid app and the time could be told by just using JavaScript, but this is a PoC tutorial, right? :-)
The full sources can be found in this Github repository. Also, I have live demo running here: websockets-mfojtik.rhcloud.com/time