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 .