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