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.