Erlang PubNub Client and Chat

I was thoroughly impressed with PubNub, a publish/subscribe service, when I first read their articles and played around with it some in Javascript. But obviously I need an Erlang API if I’m going to really use it! So I’ve created ePubNub.

In the ePubNub README you’ll find information on some basic usage of the application. You don’t have to do anything more than use the epubnub.erl module to publish and subscribe (by either providing a PID to send messages to or a function handler to process each).

Here I’ve built a little more complicated app/release called epubnub_chat, and the source is also on github.

The first thing we need is the epubnub app as a dependency in the epubnub_chat.app file:

   {applications, [kernel, stdlib, epubnub]},

We’ll use a simple_one_for_one for supervising channel subscribed processes. In epubnub_chat_sup we have 3 API functions for the user to use (start_link is run by the _app.erl module on startup): connect/1, connect/2, disconnect/1:

connect(Channel) ->
    supervisor:start_child(?SERVER, [Channel]).

connect(EPN, Channel) ->
    supervisor:start_child(?SERVER, [EPN, Channel]).

disconnect(PID) ->
    epubnub_chat:stop(PID).

EPN is a record containing the necessary url and keys for talking to the PubNub service and is created with the new functions in epubnub:

-spec new() -> record(epn).
new() ->
    #epn{}.

-spec new(string()) -> record(epn).
new(Origin) ->
    #epn{origin=Origin}.

-spec new(string(), string()) -> record(epn).
new(PubKey, SubKey) ->
    #epn{pubkey=PubKey, subkey=SubKey}.

-spec new(string(), string(), string()) -> record(epn).
new(Origin, PubKey, SubKey) ->
    #epn{origin=Origin, pubkey=PubKey, subkey=SubKey}.

You can pass none to connect/1 and it will send none to the chat gen_server and it will use defaults of pubsub.pubnub.com, demo and demo, for the url, publish key and subscribe key respectively.

Now in the epubnub_chat gen_server we need the following API functions:

start_link(Channel) ->
    start_link(epubnub:new(), Channel).

start_link(EPN, Channel) ->
    gen_server:start_link(?MODULE, [EPN, Channel], []).

message(Server, Msg) ->
    gen_server:cast(Server, {message, Msg}).

stop(Server) ->
    gen_server:cast(Server, stop).

The start_link functions are run when the supervisor spawns a simple_one_for_one supervisor for the process. This returns {ok, PID}. This PID must be remembered so we can talk to the process we have started thats subscribed to a specific channel. We pass this PID to the message/2 and stop/1 functions, which we’ll see at the and when we use the program.

start_link/1 and /2 call init/1 with the provided arguments:

init([EPN, Channel]) ->
    {ok, PID} = epubnub_sup:subscribe(EPN, Channel, self()),
    {ok, #state{epn=EPN, pid=PID, channel=Channel}}.

Here we use the epubnub_sup subscribe/3 function and not epubnub:subscribe because we want it to be supervised. We store the PID for this process so we can terminate it later.

The epubnub subscribe process was given the PID, returned by self/1, of the current process which is the gen_server process and will send messages that are published to the channel to that process. We handle these messages in handle_info/2:

handle_info({message, Message}, State) ->
    io:format("~p~n", [Message]),
    {noreply, State}.

Lastly, we have to handle the messages from message/2 and stop/1:

handle_cast({message, Msg}, State=#state{epn=EPN, channel=Channel}) ->
    epubnub:publish(EPN, Channel, Msg),
    {noreply, State}.

terminate(_Reason, #state{pid=PID}) ->
    epubnub:unsubscribe(PID),
    ok.

The handle_cast/2 function published your message to the channel this process subscribed to with epubnub:publish/3 and terminate calls epubnub:unsubscribe/1 before this process ending which sends a terminate message to the subscibred process.

Now lets see this program in action:

[tristan@marx ~/Devel/epubnub_chat]
09:55 (master)$ sinan dist
starting: depends
starting: build
starting: release
starting: dist
[tristan@marx ~/Devel/epubnub_chat]
09:55 (master)$ sudo faxien ir
Password:
Do you want to install the release: /Users/tristan/Devel/epubnub_chat/_build/development/tar/epubnub_chat-0.0.1.tar.gz
Enter (y)es, (n)o, or yes to (a)ll? > ? y
Replacing existing executable file at: /usr/local/lib/erlang/bin/epubnub_chat
Replacing existing executable file at: /usr/local/lib/erlang/bin/5.8.2/epubnub_chat
Replacing existing executable file at: /usr/local/lib/erlang/bin/erlware_release_start_helper
Replacing existing executable file at: /usr/local/lib/erlang/bin/5.8.2/erlware_release_start_helper
ok
[tristan@marx ~/Devel/epubnub_chat]
09:56 (master)$ epubnub_chat
....
Eshell V5.8.2  (abort with ^G)
1> {ok, Server} = epubnub_chat_sup:connect("chat").
{ok,}
2>
=PROGRESS REPORT==== 10-Apr-2011::09:57:14 ===
          supervisor: {local,inet_gethost_native_sup}
             started: [{pid,},{mfa,{inet_gethost_native,init,[[]]}}]

=PROGRESS REPORT==== 10-Apr-2011::09:57:14 ===
          supervisor: {local,kernel_safe_sup}
             started: [{pid,},
                       {name,inet_gethost_native_sup},
                       {mfargs,{inet_gethost_native,start_link,[]}},
                       {restart_type,temporary},
                       {shutdown,1000},
                       {child_type,worker}]

2> epubnub_chat:message(Server, <"hello there!">).
ok
3> <"hello there!">
<"I'm from the webapp!">

3> q().
ok

You can go to the PubNub tutorial page to chat between yourself, or get someone else to join!

That’s it! Simple and quick global communication that scales for you!

I’ve really enjoyed playing with PubNub and hope I get to use it for a real project soon.