Code Snippet: Erlang Server, Common Lisp Client


(Figure 1: descriptive image)

My next project is going to use a multi-language architecture. I am working on a common lisp oriented web "front end" and using erlang for a lot of the back end batch processing. This small snippet of code shows what I started out with. Here is a basic erlang tcp/ip server and a lisp client that sends a "Hello World" message and waits for the response. The erlang code is more verbose then it should be, sometimes it can be difficult to debug mismatch/pattern matching bugs.

The lisp implementation is a little bit easier to follow, the entry point starts at the main function and then the next main function (riki-main). If you are used to any other procedural or imperative language then you should be able to follow the next sequence of operations. The erlang code is a just little bit harder to follow due to the nature erlang's use pattern matching and callbacks. The application begins at the start_server function and then the server_lib:start_link and server_listen calls. This particular server application responds to tcp events, depending if a tcp connection is closed or incoming data is received, that particular block of code gets invoked.

Full Source (download from SVN)
Unfortunately I didn't really get into too much detail of describing the code to implement the server and client. Once I am further along in the project I will explain what is going on. Here is the full source code. Use "make run" to launch the erlang server and enter the test/ directory to invoke the lisp client.

* Erlang Server Source Repository Directory
* Lisp Client Source



Common Lisp Client (tested with clisp)

;; Author: Berlin Brown <berlin dot brown at gmail.com>
;; Date: 6/6/2008
;; File: test_riki_server.lisp
;;
;; ---------------------------
;; Short Description:
;; ---------------------------
;; Simple tcp client for communicating with riki server
;;
;; Environment: Tested with GNU CLISP 2.44 (2008-02-02) Win32
;; GNU CLISP 2.42 (2007-10-16) (built 3403360776) (memory 3419736148)
;; Software: GNU C 4.1.3 20071019 (prerelease) (Ubuntu 4.1.2-17ubuntu1)
;;
;; Full Description:
;;
;; References:
;; [1] http://cl-cookbook.sourceforge.net/sockets.html
;; [2] http://clocc.sourceforge.net/dist/port.html
;; [3] http://clisp.cons.org/impnotes/socket.html
;; [4] http://www.unixuser.org/~euske/doc/cl/loop.html

(defun join (lst)
"Using reduce to join, note O(n^2)
Or:
(with-output-to-string (stream) (dolist (string strings)
(write-string string stream)))"

(reduce #'(lambda (x y) (concatenate 'string x y)) lst))

(defun list->hash (lst hashdata)
"Convert key value list data into a hash table"
(dolist (key-val lst)
(setf (gethash (first key-val) hashdata)
(second key-val)))
hashdata)

(defun split-by-one (string delim)
"Returns a list of substrings of string
divided by ONE space each.
Note: Two consecutive spaces will be seen as
if there were an empty string between them.
http://cl-cookbook.sourceforge.net/strings.html"

(loop for i = 0 then (1+ j)
as j = (position delim string :start i)
collect (subseq string i j)
while j))

(defun riki-client-hello (socket host)
" Send a hello request to the server"
(format socket "@RIKI:CLNT:HELLO"))

(defun read-data (socket)
" Read lines of data from the server until we encounter the ENDMSG"
(format t "Waiting for incoming data~%")
(loop
:for line = (read-line socket nil nil)
:for linet = (string-trim '(#\Space #\e #\t #\m) line)
:while line
:do (format t "trace: read-line [~a] ~%" linet)
:collect line into res
:when (string-equal "@RIKI:SERV:ENDMSG" linet)
:return res))

(defun riki-connect (host &optional (port 9083))
"Use of common lisp keyword arguments [(defun i (&key x &key y) (list x y))]"
;; HTTP requires the :DOS line terminator
(with-open-stream (socket
(socket:socket-connect
port host :EXTERNAL-FORMAT :DOS))
(format t "~%###############~%")
(format t "INFO: Sending Request~%")
(format t "###############~%~%")
;; Print REQUEST data to file and to STDOUT
(with-open-file
(ostream "request_data.log"
:direction :output
:EXTERNAL-FORMAT :DOS)
(riki-client-hello ostream host))
(format t "~%###############~%")
(riki-client-hello socket host)
(read-data socket)))

(defun riki-main (host &optional (port 9083))
"Entry point for a HTTP get request, return the http header object"
(let* ((http-data (riki-connect host port)))))

(defun main ()
"Main entry point for the application"
(format t "INFO: riki server - connecting~%")
(print (riki-main "localhost" 9083))
(format t "~%INFO: done~%"))

(main)
;; End of File

Full Erlang Source

%%----------------------------------------------------------
%% File: riki_server.erl
%% Simple server:
%%
%% Last tested with Erlang emulator version:
%% Erlang (BEAM) emulator version 5.6.1
%% Compiled on Mon Feb 18 18:11:22 2008
%%
%% Also see:
%% http://www.erlang.org/doc/man/gen_tcp.html
%%----------------------------------------------------------

-module(riki_server).
-include("riki_server.hrl").
-export([start_server/0, start/0, wait_for_messages0/0]).

%%
%% Server handler info:
%% lib_handler - gen_server process id created from invoking server_lib start link.
%% In the function server_handler, this variable is defined as <P>.
%% Use lib_handler to pass messages to the server library.
-record(serv_handler_info, {lib_handler}).

%%------------------------------------------------
%% Top-level Handler process, wait for termination
%%------------------------------------------------
start() ->
% Ref: spawn_link(Module, Function, ArgList)
spawn_link(?MODULE, wait_for_messages0, []).

%% Simple handler operation handles one message, terminate.
%% The application process will assign this handler for the server library.
wait_for_messages0() ->
io:format("trace: initialize wait for messages~n"),
wait_for_messages(idle).

wait_for_messages(idle) ->
io:format("trace: app:waiting for messages~n"),
receive
{ connection_closed } ->
io:format("trace: app: client connection closed~n");
{ shutdown } ->
io:format("trace: app: shutting down server.~n"),
% Exit point for the application, full shutdown.
erlang:halt();
Else ->
io:format("trace: app: invalid message~n")
end.

%%------------------------------------------------
%% Test serverlib
%%------------------------------------------------
server_handler(ServClient, idle) ->
ServLib = ServClient#serv_handler_info.lib_handler,
io:format("trace: [!] at server_handler.idle [~p]~n", [ServLib]),
io:format("trace: app: wait_messages:accept <incoming>~n"),
% Launch initial accept clients block
AcceptCall = server_lib:server_accept_call(ServLib),
server_handler(ServClient, idle).

%% Simple functional test for the server library
start_server() ->
io:format("starting server <with server library support>~n"),
AppHandler = start(),
io:format("trace: top-level app handler~p~n", [AppHandler]),
Client = #irc_server_info{app_handler=AppHandler},
ServStart = server_lib:start_link(Client),
case ServStart of
{ ok, P } ->
io:format("trace: app:server pid/lib [~p]~n", [P]),
server_lib:server_listen(P),
State = server_lib:get_cur_state(P),
io:format("trace: state [~p]~n", [State]),
server_handler(#serv_handler_info{lib_handler=P}, idle)
end.

%% End of File.

%%----------------------------------------------------------
%% File: client_handler.erl
%% Descr: IRC Server library gen_server
%% Author: Berlin Brown
%% Date: 6/4/2008
%%
%% Last tested with Erlang emulator version:
%% Erlang (BEAM) emulator version 5.6.1
%% Compiled on Mon Feb 18 18:11:22 2008
%% Additional Resources:
%% http://www.erlang.org/doc/man/gen_tcp.html
%%----------------------------------------------------------

-module(client_handler).
-include("riki_server.hrl").
-behaviour(gen_server).

-export([start_link/1, get_cur_state/1]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_info/2, code_change/3,
terminate/2]).

-import_all(data_lib).

%%--------------------------------------------------------------------
%% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
%% Description: Starts the server
%%--------------------------------------------------------------------
start_link(Client) ->
gen_server:start_link(?MODULE, [Client], []).

%%====================================================================
%% gen_server callbacks
%%====================================================================

%%--------------------------------------------------------------------
%% Function: init(Args) -> {ok, State} |
%% {ok, State, Timeout} |
%% ignore |
%% {stop, Reason}
%%
%% Description: Initiates the server
%% Whenever a gen_server is started using gen_server:start/3,4
%% or gen_server:start_link/3,4, this function is called by the new process to initialize.
%%--------------------------------------------------------------------
init([Client]) ->
AppHandler = Client#client_info.app_handler,
ClientSock = Client#client_info.client_sock,
io:format("trace: client_handler:init. handler:[~p]~n", [AppHandler]),
{ok, #client_state{app_handler=AppHandler,
connection_timeout=undefined,
client_sock=ClientSock,
client=Client,
state=starting}}.

%%--------------------------------------------------------------------
%% From = is a tuple {Pid, Tag} where Pid is the
%% pid of the client and Tag is a unique tag.
%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
%% {reply, Reply, State, Timeout} |
%% {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, Reply, State} |
%% {stop, Reason, State}
%% Description: Handling call messagesa
%%--------------------------------------------------------------------
handle_call(get_cur_state, _From, #client_state{} = State) ->
% Generic method to get the current state.
io:format("trace: lib:handle_call:get_cur_state~n"),
{reply, {ok, State}, State}.

%%--------------------------------------------------------------------
%% Function: terminate(Reason, State) -> void()
%% Description: This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any necessary
%% cleaning up. When it returns, the gen_server terminates with Reason.
%% The return value is ignored.
%%--------------------------------------------------------------------
terminate(_Reason, #client_state{client=Client}) ->
io:format("trace: client:handler:terminate reason:[~p]~n", [_Reason]),
ok;
terminate(normal, #client_state{client=Client}) ->
io:format("trace: client:handler:terminate.normal~n"),
ok;
terminate(shutdown, #client_state{client=Client}) ->
io:format("trace: client:handler:terminate.shutdown~n"),
ok;
terminate(noreply, #client_state{client=Client}) ->
io:format("trace: client:handler:terminate.<noreply>~n"),
ok;
terminate(_, State) ->
io:format("trace: client:handler:terminate.<generic> [~p]~n", [State]),
ok.

%%--------------------------------------------------------------------
%% Function: handle_info(Info, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
%% Description: Handling all non call/cast messages
%%--------------------------------------------------------------------
handle_info({tcp, Sock, Data}, State) ->
inet:setopts(Sock, [{active, once}]),
io:format("trace: lib:info.tcp data [~p]~n", [Data]),
{Prefix, Command, Args} =
try
%%data_lib:scan_string(Data)
io:format("trace: lib:info.tcp try.catch~n"),
{"", (Data), ""}
catch
_:X ->
io:format("ERROR: error attempting to parse input command error:[~p]~n",[X]),
{"", (Data), ""}
end,
% Invoke the client data handler to process incoming messages.
io:format("trace: -> before client_handle_data~n"),
HandleState=client_handle_data(State, {Prefix, Command, Args}),
io:format("trace: -> after client_handle_data~n"),
{noreply, State};
handle_info({tcp_closed, Sock}, State) ->
%*************************
% Client has closed the connection.
%*************************
io:format("trace: lib:info.tcp_closed state:[~p]~n", [State]),
AppHandler = State#client_state.app_handler,
inet:setopts(Sock, [{active, once}]),
gen_tcp:close(Sock),
% Send a request to the handler; we lost a connection.
AppHandler ! {connection_closed},
{noreply, State#client_state{state=disconn, client_sock=nil}};
handle_info({tcp_error, Sock, Reason}, State) ->
io:format("trace: lib:info.tcp_error~n"),
inet:setopts(Sock, [{active, once}]),
{noreply, State#client_state{state=disconn}};
handle_info(Msg, State) ->
% Generic handle info handler
io:format("trace: lib:info.<generic> [~p] [~p]~n", [Msg,State]),
{noreply, State}.

%%--------------------------------------------------------------------
%% Internal functions
%% Including client handle data.
%%--------------------------------------------------------------------
client_handle_data(#client_state{app_handler=AppHandler, client=Client, client_sock=Sock},
{_, <<"@RIKI:CLNT:QUIT">>, _}) ->
io:format("trace: client_lib: data: system QUIT, sock:[~p] ~n", [Sock]),
gen_tcp:send(Sock, "Shutting Down Server\r\n"),
% Send a request to the handler; we lost a connection.
AppHandler ! {shutdown},
idle;
client_handle_data(#client_state{app_handler=AppHandler, client=Client, client_sock=Sock},
{_, <<"@RIKI:CLNT:HELLO">>, _}) ->
io:format("trace: client_lib: data: Send Hello Response: [~p]~n", [Sock]),
gen_tcp:send(Sock, "@RIKI:SERV:HELLO\r\n"),
gen_tcp:send(Sock, "@RIKI:SERV:ENDMSG\r\n\r\n"),
%gen_tcp:close(Sock),
idle;
client_handle_data(#client_state{app_handler=AppHandler, client=Client},
Message) ->
% Generic handler function.
io:format("trace: client_lib (default handler): msg:[~p]~n", [Message]),
idle.

%%--------------------------------------------------------------------
%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
%% Description: Convert process state when code is changed
%%--------------------------------------------------------------------
code_change(_OldVsn, State, _Extra) ->
{ok, State}.

get_cur_state(ServLib) ->
io:format("trace: lib:get_cur_state: pid:[~p] ~n", [ServLib]),
% Return: {ok, State}
gen_server:call(ServLib, get_cur_state).

%% End of File



%%----------------------------------------------------------
%% File: server_lib
%% Descr: IRC Server library gen_server
%% Author: Berlin Brown
%% Date: 3/2/2008
%%
%% Related Quotes:
%% --------------
%% Baron Harkonnen: I will have Arrakis back for myself!
%% He who controls the Spice controls the universe and what
%% Piter did not tell you is we have control of someone
%% who is very close, very close, to Duke Leto!
%% This person, this traitor, will be worth more to
%% us than ten legions of Sardaukar!
%% --------------
%%
%% Additional Resources:
%% http://www.erlang.org/doc/man/gen_tcp.html
%% gen_tcp:listen:
%% Received Packet is delivered as a binary.
%% {ok, LSock} = gen_tcp:listen(5678, [binary, {packet, 0},
%% {active, false}]),
%%----------------------------------------------------------

-module(server_lib).
-include("riki_server.hrl").
-behaviour(gen_server).

-export([start_link/1, get_cur_state/1, server_listen/1, server_accept_call/1]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_info/2, code_change/3,
terminate/2]).

%%--------------------------------------------------------------------
%% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
%% Description: Starts the server
%%--------------------------------------------------------------------
start_link(Client) ->
gen_server:start_link(?MODULE, [Client], []).

%%====================================================================
%% gen_server callbacks
%%====================================================================

%%--------------------------------------------------------------------
%% Function: init(Args) -> {ok, State} |
%% {ok, State, Timeout} |
%% ignore |
%% {stop, Reason}
%%
%% Description: Initiates the server
%% Whenever a gen_server is started using gen_server:start/3,4
%% or gen_server:start_link/3,4, this function is called by the new process to initialize.
%%--------------------------------------------------------------------
init([Client]) ->
io:format("trace: server_lib:init~n"),
%{ok, Ref} = timer:send_interval(connection_timeout(), server_timeout),
AppHandler = Client#irc_server_info.app_handler,
{ok, #server_state{app_handler=AppHandler,
connection_timeout=undefined,
client=Client,
state=starting}}.

%%--------------------------------------------------------------------
%% From = is a tuple {Pid, Tag} where Pid is the
%% pid of the client and Tag is a unique tag.
%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
%% {reply, Reply, State, Timeout} |
%% {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, Reply, State} |
%% {stop, Reason, State}
%% Description: Handling call messages
%%--------------------------------------------------------------------
handle_call(irc_server_bind, _From,
#server_state{client=Client } = State) ->
Port = Client#irc_server_info.port,
io:format("trace: lib:handle_call:bind Port:<~p>~n", [Port]),
{ok, ServSock} = server_bind(Port),
{reply, ok, State#server_state{serv_sock=ServSock, state=connecting}};
handle_call(irc_accept_clients, _From,
#server_state{serv_sock=ServSock, app_handler=AppHandler } = State) ->

io:format("trace: lib:handle_call accept_clients. [~p]~n", [AppHandler]),
ClientSock = server_accept(ServSock),
%***************
% Start the handler process
%***************
io:format("trace: [!!] lib:handle_call client-socket: [~p]~n", [ClientSock]),
% Pass the main app handler, serv lib handler, client sock
% And launch the handler process.
ClientInfo = #client_info{app_handler=AppHandler,serv_lib=self(),
client_sock=ClientSock},
{ ok, ClientServ } = client_handler:start_link(ClientInfo),
% Assign a new controlling process.
gen_tcp:controlling_process(ClientSock, ClientServ),
% Active the socket so that the client handler can handle the
% tcp data.
inet:setopts(ClientSock, [{packet, 0}, binary,
{nodelay, true},{active, true}]),
{reply, ok, State};
handle_call(get_cur_state, _From, #server_state{} = State) ->
% Generic method to get the current state.
io:format("trace: lib:handle_call:get_cur_state~n"),
{reply, {ok, State}, State}.

%%--------------------------------------------------------------------
%% Function: terminate(Reason, State) -> void()
%% Description: This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any necessary
%% cleaning up. When it returns, the gen_server terminates with Reason.
%% The return value is ignored.
%%--------------------------------------------------------------------
terminate(_Reason, #server_state{client=Client}) ->
io:format("trace: lib:terminate reason:[~p]~n", [_Reason]),
ok;
terminate(normal, #server_state{client=Client}) ->
io:format("trace: lib:terminate.normal~n"),
ok;
terminate(shutdown, #server_state{client=Client}) ->
io:format("trace: lib:terminate.shutdown~n"),
ok.

%%--------------------------------------------------------------------
%% Function: handle_info(Info, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
%% Description: Handling all non call/cast messages
%%--------------------------------------------------------------------
handle_info(server_timeout, #server_state{client=Client} = State) ->
io:format("trace: lib:handle_info.server_timeout"),
%{noreply, State#server_state{state=timeout, connection_timeout=undefined}}.
{noreply, State}.

%%--------------------------------------------------------------------
%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
%% Description: Convert process state when code is changed
%%--------------------------------------------------------------------
code_change(_OldVsn, State, _Extra) ->
{ok, State}.

%%--------------------------------------------------------------------
%% Use the gen_server call to get ready to listen on port.
%% Parm:Client - irc_server_info
%%--------------------------------------------------------------------
server_listen(ServLib) ->
io:format("trace: lib:server listen [~p]~n", [ServLib]),
% Synchronous gen_server call
gen_server:call(ServLib, irc_server_bind).

%% Accept call and then cast to handle new client
server_accept_call(ServLib) ->
io:format("trace: lib:server accept new client~n"),
gen_server:call(ServLib, irc_accept_clients, infinity).

get_cur_state(ServLib) ->
io:format("trace: lib:get_cur_state: pid:[~p] ~n", [ServLib]),
% Return: {ok, State}
gen_server:call(ServLib, get_cur_state).

%% gen_tcp IO call - server_bind(Port) -> { ok, LSock }
server_bind(Port) ->
io:format("trace: attempting to bind server... [~p]~n", [Port]),
gen_tcp:listen(Port, [binary, {packet, 0},
{active, false}]).

%%--------------------------------------------------------------------
%% Function: server_accept(ServSock) -> ClientSock
%% Description: Handling all non call/cast messages
%%--------------------------------------------------------------------
server_accept(ServSock) ->
{ ok, ClientSock } = gen_tcp:accept(ServSock),
ClientSock.

connection_timeout() ->
% Time out delay of 1 minute
600000.

%% End of File

Makefile used to build the Erlang example (also includes a driver target to launch the application)

#**********************************************************
# Build the .beam erlang VM files
# Makefile for irc bot (based on orbitz bot)
# Laughing man IRC library uses orbitz
#
# Date: 3/1/2008
# Author: Berlin Brown
#
#**********************************************************

TOPDIR := $(shell pwd)
DATE = $(shell date +%Y%m%d)
WIN_TOPDIR := $(shell cygpath -w `pwd`)

APPLICATION = riki_server
VERSION = 0.0.1

ESRC = ./src
EBIN = ./ebin

TEST_DIR = ./test/erlang
TEST_DIR_SRC = $(TEST_DIR)/src

ERLC = erlc
ERL = erl

OPT = -W
INC = $(ESRC)/inc

SED = $(shell which sed)

TMP = $(wildcard *~) $(wildcard src/*~) $(wildcard inc/*~)
INC_FILES = $(wildcard $(INC)/*.hrl)
SRC = $(wildcard $(ESRC)/*.erl)
CONFFILES = conf/config.xml $(wildcard conf/*fortune)

TARGET = $(addsuffix .beam, $(basename \
$(addprefix $(EBIN)/, $(notdir $(SRC)))))
LIB_TARGET_OBJS = $(EBIN)/client_handler.beam \
$(EBIN)/server_lib.beam \
$(EBIN)/riki_server.beam \
$(EBIN)/lisp_parse_std.beam \
$(EBIN)/lisp_parse.beam \

${APPLICATION}: $(TARGET) $(LIB_TARGET_OBJS) $(TEST_OBJS)

all: clean ${APPLICATION}

clean:
-rm -vf $(TARGET) $(TMP)
-rm -vf erl_crash.dump
-rm -vf ebin/*.beam

ebin/%.beam: $(TEST_DIR_SRC)/%.erl
$(ERLC) $(OPT) -I $(INC) -o ebin $<

ebin/%.beam: $(ESRC)/%.erl
$(ERLC) $(OPT) -I $(INC) -o ebin $<

# Start the erl emulator process with the social stats module
# Also -s erlang halt
run: $(APPLICATION)
$(ERL) -noshell -pa $(TOPDIR)/ebin -s riki_server start_server -s erlang halt

# Note: error with running from win32/cygwin, see tests.sh
winrun: $(APPLICATION)
$(ERL) -noshell -pa $(WIN_TOPDIR)/ebin/ -s riki_server start_server -s erlang halt

# Run the test suite.
# Start the test IRC server, delay for 5 seconds and start the client.
tests: $(APPLICATION)
# Run the basic tests and then the server test.
# Ideally, test modules will have a 'run_tests' function.
$(ERL) -noshell -pa $(TOPDIR)/ebin -s simple_server_example simple_server_lib -s erlang halt &
-sleep 20

# End of the file

Comments

Popular posts from this blog

Is Java the new COBOL? Yes. What does that mean, exactly? (Part 1)

On Unit Testing, Java TDD for developers to write

JVM Notebook: Basic Clojure, Java and JVM Language performance