Ruby EventMachine Web Socket Redis Pub/Sub Chat Room
Yet another web socket chat room. This one uses Ruby, EventMachine, Redis pub/sub, and web sockets via jQuery.
Back end first. Install Redis if you have not already.
# install via homebrew
brew install redis
# start in background
redis-server /usr/local/etc/redis.conf &
Here’s a few required gems. file: Gemfile
source 'https://rubygems.org'
gem 'em-websocket'
gem 'sinatra'
gem 'thin'
gem 'em-hiredis'
Install them.
bundle install
Up next: EventMachine server. This script handles the backend pubsub and web socket connections. file: server_em.rb
#!/usr/bin/env ruby
require 'em-websocket'
require 'em-hiredis'
require 'json'
require 'logger'
# helper module to do some parsing and cleaning
module ChatServer
extend self
DEFAULT_CHAT_ROOM = 'default'
DEFAULT_USER_NAME = 'anonymous'
DEFAULT_MESSAGE = ':)'
VALID_MESSAGE_KEYS = ['user_name', 'chat_room', 'message']
# clean chat room name
def chat_room_name(data)
return DEFAULT_CHAT_ROOM if data.nil? || !data.respond_to?(:gsub)
chat_room_name = data.gsub(/\W/,'')
return DEFAULT_CHAT_ROOM if chat_room_name.nil? || chat_room_name.empty?
chat_room_name
end
# clean user name
def user_name(data)
return DEFAULT_USER_NAME if data.nil? || !data.respond_to?(:gsub)
user_name = data.gsub(/[^\w\ ]/, '')
return DEFAULT_USER_NAME if user_name.nil? || user_name.empty?
user_name
end
# strip tags from a message
def clean_message(message)
return DEFAULT_MESSAGE if message.nil? || !message.respond_to?(:gsub)
return message.gsub(/<(?:.|\n)*?>/, '')
end
# parse JSON message and clean data
def message_string_to_object(message_string)
begin
data = JSON.parse message_string
rescue => e
data = {}
end
# remove invalid keys
data.delete_if {|key,value| !VALID_MESSAGE_KEYS.include?(key) }
# clean data
data['message'] = clean_message data['message']
data['chat_room'] = chat_room_name data['chat_room']
data['user_name'] = user_name data['user_name']
data
end
end
EM.run do
@log = Logger.new(STDOUT)
EM::WebSocket.run(:host => "0.0.0.0", :port => 8080) do |ws|
# event: web socket open
ws.onopen do |handshake|
chat_room_name = ChatServer.chat_room_name handshake.path
# log
@log.info "WebSocket connection opened; chat room: #{chat_room_name}"
# connect to Redis and subscribe
@redis = EM::Hiredis.connect
pubsub = @redis.pubsub
pubsub.subscribe chat_room_name
pubsub.on(:message) do |channel, message|
# log
@log.debug "redis pubsub.on(:message); channel: #{channel}; message: #{message}"
ws.send ChatServer.message_string_to_object(message).to_json
end
end
# event: web socket close
ws.onclose do
# log
@log.info "WebSocket connection closed"
end
# event: web socket received message
ws.onmessage do |message|
# log
@log.debug "ws.onmessage; message: #{message}"
begin
# parse/clean json message
data = ChatServer.message_string_to_object message
# publish to redis pubsub
@redis.publish data['chat_room'], data.to_json
rescue => e
@log.error e
end
end
end
end
I then created a simple sinatra front end server. file: server.web.rb
#!/usr/bin/env ruby
require 'sinatra'
set :server, :thin
get '/' do
erb :chat_room
end
Some front end html. file: views/chat_room.erb
<div class='row'>
<div class="col-md-4">
<!-- this form is shown first and is used to submit chat room and user name -->
<!-- it is hidden once submitted -->
<form id='enter_chat_room' role='form'>
<div class="form-group">
<label for="chat_room">Chat room</label>
<input type="text" class="form-control" id="chat_room" placeholder="Enter chat room">
</div>
<div class="form-group">
<label for="user_name">Name</label>
<input type="text" class="form-control" id="user_name" placeholder="Enter name">
</div>
<button type="submit" class="btn btn-default">Enter</button>
</form>
<!-- this form is shown once the user enters chat room and user name -->
<form id='send_message' role='form'>
<div class="form-group">
<label for="chat_room">Chat Room</label>
<input type="text" class="form-control" id="chat_room_ro" readonly>
</div>
<div class="form-group">
<label for="message">Message</label>
<input type="text" class="form-control" id="message" placeholder="Enter message">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
</div>
<div class="col-md-8">
<dl class="dl-horizontal" id="chat_messages">
</dl>
</div>
</div>
Some jQuery web socket and form processing code. file: public/js/chat.js
var user_name = null;
var chat_room = null;
var ws = null;
var strip_tags = function(data) {
return data.replace(/<(?:.|\n)*?>/gm, '');
}
var clean_chat_room = function(data) {
data = strip_tags(data);
return data.replace(/[^\w]/g, '');
}
var clean_user_name = function(data) {
data = strip_tags(data);
return data.replace(/[^\w\ ]/g, '');
}
var init_enter_chat_room_form = function(){
$('#enter_chat_room').submit(function(){
$form = $(this);
$chat_room_field = $('#chat_room', $form);
$user_name_field = $('#user_name', $form);
chat_room = $chat_room_field.val();
user_name = $user_name_field.val();
// clean data
chat_room = clean_chat_room(chat_room);
user_name = clean_user_name(user_name);
// update field vals
$chat_room_field.val( chat_room );
$user_name_field.val( user_name );
// validate
if (chat_room == '') {
alert('Chat room is required.');
return false;
}
if (user_name == '') {
alert('Name is required.');
return false;
}
// hide chat/user form, show message form
$form.hide();
init_chat_session();
return false;
});
}
var init_chat_session = function(){
// open web socket
ws = new WebSocket("ws://127.0.0.1:8080/" + chat_room);
ws.onerror = function(error){};
ws.onclose = function(){};
ws.onopen = function(){
init_send_message_form();
};
ws.onmessage = function (e) {
data = JSON.parse(e.data);
new_message = "<dt>"+data.user_name+"</dt><dd>"+data.message+"</dd>";
$('#chat_messages').append( new_message );
};
}
var init_send_message_form = function(){
// show message form
$('#send_message').show();
$('#chat_room_ro', $('#send_message')).val( chat_room );
// submit handler
$('#send_message').submit(function(){
$form = $(this);
$message_field = $('#message', $form);
message = $message_field.val();
// clean data
message = strip_tags(message);
// update field vals
$message_field.val( message );
// validate
if (message == '') {
alert('Message is required.');
return false;
}
data = {
user_name: user_name,
chat_room: chat_room,
message: message
}
// send message
try {
ws.send( JSON.stringify(data) );
}
catch(err) {
// debug
//console.debug(err);
}
return false;
});
};
$(document).ready(function(){
init_enter_chat_room_form();
});
Run the servers.
chmod +x server_em.rb
chmod +x server_web.rb
./server_em.rb &
./server_web.rb &
Screenshot of chat room.
Source code on GitHub.