Blog Categories
Ad Space
Twitter Updates
    follow me on Twitter
    Currently Reading
    Powered by Squarespace
    Git Projects
    « Introducing ErlangX and ErlangXCode | Main | My top 5 Favourite Erlang Functions »
    Wednesday
    10Sep

    Erlang Tutorial: Sysinfo Server

    Say you have been doing some erlang basics and are ready to start an actual program. Luckily your manager has just asked you to create a server that listens for telnet a connection and has an interface to get the environment variables on the running system. Oh and it needs 99.9999999% uptime. I know what you are thinking. "By gosh this is perfect for erlang" was what you thought right?

    Well before you start any real software development in Erlang you really should start by reading the Programming Rules and Conventions on Ericsson's website. This should be your bible. Go ahead and read it now. Don't worry I'll wait.....

    Back? Good. Now we can start this tutorial.

    Housekeeping

    We start by creating a directory for our application. In that directory we create the subdirectories doc, ebin and src. Now we create a file in the project root called Emakefile to ease the compilation of the software. Open up Emakefile and add the following:

    {'src/sysinfo_server.erl', [{outdir, "ebin"}]}.
    

    This basically lists our modules for compilation and tells Erlang to put the finished product into the ebin subdirectory.

    Iteration 1: A Basic Server

    Now lets take a look at the src/sysinfo_server.erl file. First declare the exports and add the start functions.

    -module(sysinfo_server).
    -export ([start/0, start/1, accept_connection/1, responder_loop/1]).
    
    start() ->
        start(8023).
    
    start(Port) ->
        {ok, ListeningSocket} = gen_tcp:listen(Port, 
           [list, {packet, 0}, {active, false}, {reuseaddr, true}]),
        accept_connection(ListeningSocket).
    

    In start/1 we open up the socket using gen_tcp:listen/2 with the first parameter the port we want to use followed by the tcp options. I will not go in to details for the options but you can read about them in the gen_tcp module. We then pass the created socket to the the accept_connection/1 loop that handles accepting incoming connections.

    accept_connection(ListeningSocket) ->
        {ok, Socket} = gen_tcp:accept(ListeningSocket),
        spawn_link(?MODULE, responder_loop, [Socket]),
        accept_connection(ListeningSocket).
    

    Here we take the socket and wait for a connection to it with the command gen_tcp:accept/1. Thing to remember about gen_tcp:accept/1 command is that it waits forever for a connection and does not execute the next line until that connection is created. When we get the connection we use spawn_link/3 to create a seperate process running the responder_loop/1 loop that will handle all the communication to that client. The function then calls itself and waits for the next connection. Always keep this part simple. This way if something fails it will fail in responder_loop/1 thus only failing the single socket but our loop that accepts new connections is unlikely to fail and other socket continue to communicate. Lets now get to the responder loop.

    responder_loop(Socket) ->
        case gen_tcp:recv(Socket, 0) of
            {ok, "exit\r\n"} ->
                gen_tcp:send(Socket, "Exiting \r\n"),
                gen_tcp:close(Socket);
            {ok, Other} ->
                gen_tcp:send(Socket, Other),
                responder_loop(Socket);
            {error, closed} ->
                ok
        end.
    

    Here we take the data from gen_tcp:recv. The data includes the return character so we need to include that. If it matches "exit\r\n" we close the socket after sending a message back. Otherwise we echo the data back with gen_tcp:send/2 and recurse. Lets try it out by building the code and running it. Enter the following into the terminal at the project root.

    bash$ erl -make
    bash$ erl -pa ebin -run sysinfo_server start
    

    Now using a telnet client telnet to localhost at port 8023. You should be able to get echoed back all that you type and type exit to break the connection. Close the server by typing in q(). in the erlang shell.

    Iteration 2: Improving the server

    First thing we are going to do is add a function that helps us to get the environment variable. As it is not a part of the data communication and might be a reusable function we decide to put it into it's another module called sysinfo_tools.erl. Start by adding it to the Emakefile by adding an identical line to what defines sysinfo_server.

    {'src/sysinfo_tools.erl', [{outdir, "ebin"}]}.
    

    Now create src/sysinfo_tools.erl and add the following.

    -module (sysinfo_tools).
    -export ([get_env/1]).
    
    get_env(Var) ->
    [CleanVar|_Rest] = string:tokens(Var, "\r\n\t "),
    case os:getenv(CleanVar) of
      false -> "No Such Env";
      Result -> Result
    end.
    

    This is pretty simple. We take the input Var and remove all unwanted characters from it. We then call the os:getenv/1 function to retreive the environment variable. If it does not exist we return the value "No Such Env" or just simply return the value. no magic here so now we can get to the interesting part.

    Now lets turn back to src/sysinfo_server.erl

    First step is adding a function to assist us with sending data to the client. We want to run it with a list of strings and at the end it prints out a nice prompt for the user. So add this function.

    send_lines(Socket,[]) ->
        gen_tcp:send(Socket, "\r\nEnvGet> ");
    send_lines(Socket,[First|Rest]) ->
        gen_tcp:send(Socket,First),
        gen_tcp:send(Socket,"\r\n"),
        send_lines(Socket,Rest).
    

    send_lines/2 takes a socket and a list as parameters. It prints out the first element and loops back with the rest. Finally when the list is empty it prints out a prompt and exits. Lets put it in use by changing accept_connection/1 so it looks like this.

    accept_connection(ListeningSocket) ->
        {ok, Socket} = gen_tcp:accept(ListeningSocket),
        send_lines(Socket, ["Welcome to the EnvGet Service", 
            "Available commands are: list, get <env> and exit"]),
        spawn_link(?MODULE, responder_loop, [Socket]),
        accept_connection(ListeningSocket).
    

    Now we want to add the list and get commands along with changing the code to use send_lines/2 so that the respoder loop looks like this.

    responder_loop(Socket) ->
        case gen_tcp:recv(Socket, 0) of
            {ok, "get "++Var} ->
                Result = sysinfo_tools:get_env(Var),
                send_lines(Socket, [Result]),
                ?MODULE:responder_loop(Socket);
            {ok, "list\r\n"} ->
                Result = os:getenv(),
                send_lines(Socket, Result),
                ?MODULE:responder_loop(Socket);
            {ok, "exit\r\n"} ->
                send_lines(Socket, ["Exiting"]),
                gen_tcp:close(Socket);
            {ok, _Other} ->
                send_lines(Socket, ["Unknown Command"]),
                ?MODULE:responder_loop(Socket);
            {error, closed} ->
                ok
        end.
    

    Well. First we take a look at the get command. Here we try to match a string that starts with "get " and add the rest to the variable Var. We then call our handy sysinfo_tools:get_env/1 function that gets the data for us. Next we try to match the command "list". We call os:getenv/0 and pass it on to the user. Since os:getenv/0's output is a list of strings we don't put brackets around like usually.

    Now. Let's compile and run.

    bash$ erl -make
    bash$ erl -pa ebin -run sysinfo_server start
    

    Try it out by telneting to locahost:8023 and trying out the commands "list" and "get HOME". It should work like a charm.

    Well. Done for now. We would propably want to add a lot more before production. Like code reloading so the server doesn't have to go down for upgrades but that is a job for a later tutorial.

    download the code if you have any problems.

    Reader Comments (11)

    Very nice tutorial. Good amount of detail and a problem that's manageable enough for an Erlang novice (like me) to hold in head all at once. Thank you.

    Sep 11, 2008 at 3:21 | Unregistered CommenterNygard

    I would recommend start using OTP as early as possible. Play with pure Erlang server, with manual message handling, code upgrades and others, but when you are familiar with it switch to OTP - it'll help you a lot :)

    Writing 99.9999999% uptime server, one way or another, will force you to implement at least half of the OTP by yourself ;) Hence there is no need to reinvent wheel (and reintroduce own bugs)

    Sep 11, 2008 at 6:29 | Unregistered CommenterGleb Peregud

    I recommend more erlangish way because if semantic too different from noob can expect. Let newbie learn more common design.

    get_env(Var) ->
      [CleanVar|_Rest] = string:tokens(Var, "\r\n\t "),
        case os:getenv(CleanVar) of
          false -> "No Such Env";
          Result -> Result
      end.

    Sep 11, 2008 at 7:33 | Unregistered CommenterHynek (Pichi) Vychodil

    Let me improve your server response performance by drop down CPU usage:

    send_lines(Socket, Lines) ->
      gen_tcp:send(Socket,
        [[[Line, "\r\n"] || Line <- Lines], "\r\nEnvGet> "]).

    because gen_tcp:send automatically flatten io_list as usual and each send call is expensive. Let newbie learn effective response making.

    Sep 11, 2008 at 7:59 | Unregistered CommenterHynek (Pichi) Vychodil

    Thanks for the improvements.. I'll put them in the tutorial. Always good to hear from more experienced Erlangers. I'm a little hesitant about putting in the send_lines improvements as I'm trying to limit the numbers of concepts I'm using here for a single tutorial. So I specifically avoided list comprehensions and plan to write a special tutorial for that.

    Regarding OTP and gen_tcp...... I think the best thing I can say about that is that it's a comprimise. Even though there are tricks it seems perfectly clear to me that a TCP server was not what they were thinking about when OTP was designed. But for people wanting to get to know OTP I recommend Mitchell's blog

    Sep 11, 2008 at 9:41 | Registered CommenterJón Grétar Borgþórsson

    I think we need one more computer to have 99.9999999% uptime.

    Sep 11, 2008 at 10:31 | Unregistered Commenterpicky

    hehe.. That's right picky.. The 9x9 thing was more a reference to Erlang than my actual code.
    9x9 uptime is not really something we accomplish with a beginners tutorial. :)

    Sep 11, 2008 at 11:01 | Registered CommenterJón Grétar Borgþórsson

    OK, if you don't like put lists comprehensions early, you can still rewrite send_lines by


    format_lines([]) -> [];
    format_lines([Line|Tail]) -> [Line, "\r\n" | format_lines(Tail)].

    send_lines(Socket, Lines) ->
      gen_tcp:send(Socket, [format_lines(Lines), "\r\nEnvGet> "]).

    Sep 11, 2008 at 12:40 | Unregistered CommenterHynek (Pichi) Vychodil

    Off topic: This forum engine cripple comments little more than usual. What about <pre> tag, and stop ending code tag unexpectedly?

    Sep 11, 2008 at 12:45 | Unregistered CommenterHynek (Pichi) Vychodil

    I really likes this. Thanks very much.

    Just two suggestions:

    1.
    -module(sysinfo_server)

    you are missing a full stop at the very first iteration.

    2.

    You forgot to update the make file after including the second source file.

    Nov 15, 2008 at 6:35 | Unregistered Commenterernan

    Thanks. The period must have gotten lost in the copy.

    Nov 15, 2008 at 14:49 | Registered CommenterJón Grétar Borgþórsson

    PostPost a New Comment

    Enter your information below to add a new comment.

    My response is on my own website »
    Author Email (optional):
    Author URL (optional):
    Post:
     
    Some HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>
    Fork me on GitHub