Write a 32-line chat client using Ruby, AMQP & EventMachine (and a GUI using Shoes)
Have you ever considered using instant messages to communicate between programs? You can do this using Jabber’s XMPP protocol, of course. But it’s also worth taking a look at AMQP, a distributed messaging protocol first used at JPMorgan Chase. AMQP is fast, easy to use, and implemented by at least 4 open source servers.
To try it out, install the excellent Ruby AMQP bindings, and set up the RabbitMQ server (which is written in Erlang using Mnesia). On a Mac, you might do something like this:
sudo gem install amqp
sudo port install python25 rabbitmq-server
sudo rabbitmq-server
Once your server is running, save the following code as chat.rb
:
require 'rubygems'
gem 'amqp'
require 'mq'
unless ARGV.length == 2
STDERR.puts "Usage: #{$0} <channel> <nick>"
exit 1
end
$channel, $nick = ARGV
AMQP.start(:host => 'localhost') do
$chat = MQ.topic('chat')
# Print any messages on our channel.
queue = MQ.queue($nick)
queue.bind('chat', :key => $channel)
queue.subscribe do |msg|
if msg.index("#{$nick}:") != 0
puts msg
end
end
# Forward console input to our channel.
module KeyboardInput
include EM::Protocols::LineText2
def receive_line data
$chat.publish("#{$nick}: #{data}",
:routing_key => $channel)
end
end
EM.open_keyboard(KeyboardInput)
end
Now, run copies in two different terminals:
ruby chat.rb channel_1 sarah
ruby chat.rb channel_1 joe
Everything you type into one terminal will be relayed to the other.
How it works
The following line creates a topic exchange named “chat”:
$chat = MQ.topic('chat')
A topic exchange allows many-to-many communication. Here, we bind a listener to our exchange, and ask to receive all messages tagged with our channel name:
queue.bind('chat', :key => $channel)
Note that :key
may be hierarchical, and it may contain wildcards. To write data to our topic exchange, we use publish
:
$chat.publish("#{$nick}: #{data}",
:routing_key => $channel)
Our keyboard input is processed using EventMachine, a Ruby library for writing high-performance, multi-protocol servers. It’s very similar to Python’s Twisted library, though it has less documentation and support for fewer protocols.
We use EventMachine’s EM.open_keyboard
to create a asynchronous keyboard input channel, and we use EM::Protocols::LineText2
to treat the keyboard input as a line-oriented protocol.
Adding a Shoes GUI
Shoes is an eccentric, entertaining, and highly-portable GUI library by _why the lucky stiff. With a certain amount of grotesque kludging (and some pointers from “s1kx” on the #shoes IRC channel), I managed to get the Mac version of Shoes to talk to EventMachine. You may find that this code fails strangely on your computer. Honestly, I don’t know anything about Shoes. And I’m doing some pretty bad things with threads.
First, the pretty pictures:
Next, the code:
Shoes.setup { gem 'amqp' }
require 'mq'
$app = Shoes.app(:width => 256) do
background(gradient('#CFF', '#FFF'))
@output = stack(:margin => 10)
def nick str
span(str, :stroke => red)
end
def display text
@output.append do
if text =~ /^([^:]+): (.*)$/
para nick("#{$1}: "), $2
else
para text
end
end
end
end
Thread.new do
begin
AMQP.start(:host => 'localhost') do
MQ.topic('chat')
queue = MQ.queue('shoes')
queue.bind('chat')
queue.subscribe do |msg|
$app.display(msg)
end
end
rescue => e
# Try to report at least _some_ errors
# where we'll be able to see them.
$app.display(e.to_s)
end
end
Note that the GUI client listens to all channels simultaneously, because it doesn’t pass a :key
to bind
. And when writing code to run in a Shoes background thread, don’t expect to see any error messages.
Learning more about AMQP
The Ruby AMQP documentation page has a good list of papers, magazine articles, and other background material on AMQP.
Want to contact me about this article? Or if you're looking for something else to read, here's a list of popular posts.
abort
. John Apps: Thank you for the link to To AMQP or to XMPP. That's a nice comparison of the two protocols. I should mention, however, that RabbitMQ is ridiculously easy to install on any platform that has the appropriate packages. In fact, the RabbitMQ folks have a pretty admirable installation policy: