Erlang's
bit syntax provides a very convenient method of decoding and encoding binary protocols like OSCAR and YMSG. MSNP is a textual protocol that is also easily parsed with
gen_tcp's
{packet, line} and
{active, once} options. Each protocol implementation has a process which reads incoming packets, does preliminary decoding, and forwards the data to a
finite state machine.
The language and VM are great, but one of the major benefits to writing programs in Erlang is OTP which provides a large
standard library and
design principles that have been developed and tested in real-world use. One example of this is
gen_fsm, a generic behavior for event-based FSMs which is particularly useful for implementing network protocols. The gen_fsm
module handles initialization, synchronous and asynchronous event delivery, error reporting, debugging, etc. All the programmer must do is implement a callback module which handles events and makes state change decisions.
Execution of a gen_fsm begins with a call to
gen_fsm:start or
gen_fsm:start_link, which invokes the callback module's
init/1 function.
init performs any necessary initialization and determines the FSM's starting state. Here is
msnp_fsm:init/1:
init([Username, Password, Client, Opts]) ->
process_flag(trap_exit, true),
Host = proplists:get_value(host, Opts, ?MSNP_HOST),
Port = proplists:get_value(port, Opts, ?MSNP_PORT),
State = #state{client = Client,
user = #user{username = Username, password = Password},
contacts = [],
pending = dict:new(),
sessions = []},
gen_fsm:send_event(self(), {connect, Host, Port}),
{ok, protocol_negotiation, State}.
The FSM begins in state
protocol_negotiation, and immediately receives an asynchronous event
{connect, Host, Port} telling it to connect to the server. Next, gen_fsm will call
msnp_fsm:protocol_negotiation/2, passing it the event and FSM state:
protocol_negotiation({connect, Host, Port}, State) ->
{ok, Sock} = msnp_sock:start_link(self(), Host, Port),
msnp_sock:send_cmd(Sock, "VER", ["MSNP15", "CVR0"]),
{next_state, protocol_negotiation, State#state{sock = Sock}};
The FSM remains in
protocol_negotiation until it receives a matching
VER command from the server, then transitions through
sso_auth,
waiting_for_profile,
synchronizing, and eventually to the
ready state. Each state function is quite simple, and with multiple function clauses to handle different events the code is easy to read. Here are two
ready/1 event handlers that handle contact status changes:
ready(#cmd{cmd = "ILN", args = [_, Code, Name, _, Nick | _]}, State) ->
State2 = send_status_update(Name, url_util:decode(Nick), Code, State),
{next_state, ready, State2};
ready(#cmd{cmd = "NLN", args = [Code, Name, _, Nick | _]}, State) ->
State2 = send_status_update(Name, url_util:decode(Nick), Code, State),
{next_state, ready, State2};
msnp_sock parses incoming packets and sends the resulting event to the
msnp_fsm as a
cmd record. Erlang's
pattern matching provides a very concise way to determine which clause to invoke and binds the parameters needed to variables such as
Name and
Nick.
OTP behaviors like
gen_fsm also provide much useful debugging functionality. When an event handler fails for any reason a log message is generated with the FSM's state, the last message received, the cause, etc.
gen_fsm:start_link can also be called with the option
{debug, [trace]} which will log every incoming event and state change. Running systems can be inspected in real-time thanks to functions such as
sys:get_status/1 which displays the state of running processes.
With the combination of Erlang's bit-syntax and OTP library it is pretty easy to implement an IM client. Indeed most of the difficulty is due to lack of official documentation and incomplete reverse engineering. In the next post I'll cover one of the most interesting and important aspects of the gateways: scalability.