Erlang Examples: Talk with Sockets

May 29, 2008 - 4 minute read -
erlang exercises

This is part of a series on the Erlang Exercises which is a great set of programming problems that challenge you to implement solutions to some common Erlang problems. I'm going to share some of my solutions to these problems.

Erlang using UNIX sockets

Do you want to talk with a friend on another machine? Shouldn't it be nice to have a shell connected to your friend and transfer messages in between? This can be implemented using the client/server concept with a process on each side listening to a socket for messages.

-module(talk).
-compile(export_all).
start(LocalPort, RemotePort) ->
    ServerPid = spawn(?MODULE, start_server, [LocalPort]),
    cli(RemotePort, ServerPid).
cli(RemotePort, ServerPid) ->
    Data = clean(io:get_line('Message: ')),
    case should_stop(Data) of
        true ->
            ServerPid ! done;
        false ->
            spawn(?MODULE, send, [RemotePort, Data]),
            cli(RemotePort, ServerPid)
    end.
clean(Data) ->
    string:strip(Data, both, $\n).
should_stop(Str) ->
    0 =:= string:len(Str).
start_server(Port) ->
    case gen_tcp:listen(Port, [binary, {packet, 4},
                               {reuseaddr, true},
                               {active, true}]) of
        {ok, Listen} ->
            server(Listen);
        {error, Reason} ->
            {error, Reason}
    end.
server(Listen) ->
    case gen_tcp:accept(Listen) of
        {ok, Socket} ->
            server_loop(Socket),
            server(Listen);
        _ ->
            ok
    end.
server_loop(Socket) ->
    receive
        done ->
            gen_tcp:close(Socket),
            io:format("Server socket closed~n");
        {tcp, Socket, Bin} ->
            Str = clean(binary_to_term(Bin)),
            io:format("~p~n", [Str]),
            case should_stop(Str) of
                true ->
                    void;
                false ->
                    server_loop(Socket)
            end;
        {tcp_closed, _Socket} ->
            ok
    end.
send(Port, Data) ->
    case gen_tcp:connect("localhost", Port, [binary, {packet, 4}]) of
        {ok, Socket} ->
            gen_tcp:send(Socket, term_to_binary(Data)),
            gen_tcp:close(Socket);
        {error, Reason} ->
            {error, Reason}
    end.

Explanation

start(LocalPort, RemotePort) -> The entry point to the talk program. This spawns a new start_server process that will handle opening a socket and listening for messages from a remote client. It then runs the command line interface and waits for data from the user to send to the remote port.

cli(RemotePort, ServerPid) -> This is the loop that will get the data from the user and send it to the remote port.

clean(Data) This is a simple function that removes the newlines from the end of the data sent to the client.

should_stop(Str) This is a simple function that determines whether the client and server should shut down. The rule is that if it's an empty string then the processes should stop.

start_server(Port) -> This function gets run in a process and opens a Listening socket. It then hands off to server(Listen) -> to handle dealing with new incoming socket connections.

server(Listen) -> The server gets a new Socket when an incoming connection connects to the Listening port. It then goes into a receive state and waits for an incoming connection to send it data. The receive state is handled in the server_loop.

send(Port, Data) -> This is the client side of the application. It connects to a remote port and sends the data to that port.

case ... of In a number of these functions you see a common idiom:

case fun() of
    SomeMatch ->
        Do something;
    AnotherMatch ->
        Do something else
end

With socket connections and many others it is common to return a tuple like {ok, Val} when the function call is successful and will return {error, Reason} or a similar tuple when there is an error in the function. The case statement is just an easy way to handle these differences.