Network Programming in Erlang
Since I'm learning Erlang I thought my first non-trivial piece of code would be in an area where the language excels: network programming.
Network programming (or socket programming) is a pain in the ass in most languages. I first learned how to do it in C using Beej's Guide to Network Programming. Read it if you dare.
The big roadblock for most server applications is concurrency. Languages like, where concurrency was an afterthought, make developing robust server software more difficult than it has to be.
Even so-called modern languages like Java, Ruby, or Python don't handle it all that well, although you are relieved from the pain of managing all the minute details of the network connections. Erlang, on the other hand, was built with this purpose on mind.
I won't be writing any user-facing applications in Erlang any time soon, but I thought, "If I'm going to learn Erlang I may as well learn its strong points first."
To that end I decided to try to replicate the suite of classic UNIX daemons like echo and chargen.
echo
Echo is a service that spits back whatever data is handed to it over a TCP connection, bit-for-bit. Here it is in Erlang.
-module(echo). -author('Jesse E.I. Farmer <jesse@20bits.com>'). -export([listen/1]). -define(TCP_OPTIONS, [binary, {packet, 0}, {active, false}, {reuseaddr, true}]). % Call echo:listen(Port) to start the service. listen(Port) -> {ok, LSocket} = gen_tcp:listen(Port, ?TCP_OPTIONS), accept(LSocket). % Wait for incoming connections and spawn the echo loop when we get one. accept(LSocket) -> {ok, Socket} = gen_tcp:accept(LSocket), spawn(fun() -> loop(Socket) end), accept(LSocket). % Echo back whatever data we receive on Socket. loop(Socket) -> case gen_tcp:recv(Socket, 0) of {ok, Data} -> gen_tcp:send(Socket, Data), loop(Socket); {error, closed} -> ok end.
We can start this service by calling echo:listen(<port number>). from the Erlang shell, e.g., echo:listen(8888). will start the echo service on port 8888 of your machine. You can then telnet to port 8888 — telnet 127.0.0.1 8888 — and see it in action.
Here's the breakdown of the program, by function.
- listen(Port)
- Creates a socket that listens for incoming connections on port Port and passes off control to accept.
- accept(LSocket)
- Waits for incoming connections on LSocket. Once it receives a connection it spawns a new process that runs the loop function and then waits for the next connection.
- loop(Socket)
- Waits for incoming data on Socket. Once it receives the data it immediately sends the same data back across the socket. If there is an error it exits.
There are a few things worth discussing in this example.
Spawning Processes
Processes in Erlang are a basic data type. They follow the actor model of concurrent computation and make network processes a breeze.
We create new processes using spawn, which takes a Fun, or functional object, as its input. You can think of them as functions. Control of the process is handed off to the functional object passed in, like a callback.
Functional Objects
Erlang, being a functional programming language, supports functions as first-class objects via the Fun, or functional object, data type. Functions can create new functions, return functions, modify functions, and pass functions around.
The syntax to create a new functional object is like this:
MyFunction = fun(...) -> % Your Erlang code here end.
CHARGEN
chargen is a service that spews back a stream of characters when you connect to it. You can read all about it, but it's not that interesting. There's a canonical pattern that it prints out.
Here it is in Erlang.
-module(chargen). -author('Jesse E.I. Farmer <jesse@20bits.com>'). -export([listen/1]). -define(START_CHAR, 33). -define(END_CHAR, 127). -define(LINE_LENGTH, 72). -define(TCP_OPTIONS, [binary, {packet, 0}, {active, false}, {reuseaddr, true}]). % Call chargen:listen(Port) to start the service. listen(Port) -> {ok, LSocket} = gen_tcp:listen(Port, ?TCP_OPTIONS), accept(LSocket). % Wait for incoming connections and spawn the chargen loop when we get one. accept(LSocket) -> {ok, Socket} = gen_tcp:accept(LSocket), spawn(fun() -> loop(Socket) end), accept(LSocket). loop(Socket) -> loop(Socket, ?START_CHAR). loop(Socket, ?END_CHAR) -> loop(Socket, ?START_CHAR); loop(Socket, StartChar) -> Line = make_line(StartChar), case gen_tcp:send(Socket, Line) of {error, _Reason} -> exit(normal); ok -> loop(Socket, StartChar+1) end. make_line(StartChar) -> make_line(StartChar, 0). % Generate a new chargen line -- [13, 10] is CRLF. make_line(_, ?LINE_LENGTH) -> [13, 10]; make_line(?END_CHAR, Pos) -> make_line(?START_CHAR, Pos); make_line(StartChar, Pos) -> [StartChar | make_line(StartChar + 1, Pos + 1)].
As with echo we can start this by dropping into the Erlang shell and running chargen:listen(8888) to start chargen running on port 8888 (or another port of your choice).
accept and listen are identical to the functions in echo, but here are the differences:
- loop(Socket, StartChar)
- Calls make_line(StartChar) to get the CHARGEN line starting with StartChar, writes it to the socket, and then advances to the next line.
- make_line(StartChar, Pos)
- Recursively generates a CHARGEN line, keeping track of the current position in the line with Pos.
There are a few key conceptual differences, too.
Definitions
As in C we can define constants in Erlang with the -define directive. These are resolved at compile-time. You can reference the definition by prefixing it with a question mark, ?, so as to differentiate it from a variable.
Function Definition Matching
As with assignment, function calls are done via matching. When you call a function it looks for the first matching definition. For example, if we invoke loop(Socket) it finds the appropriate definition, viz., the definition that takes a single argument.
We can fix arguments, too, which is how you deal with loop control in Erlang. ?END_CHAR is 127, so if we call loop(Socket, 127) it first matches that definition rather than the more general loop(Socket, StartChar) definition.
make_line works the same way. If we're at the last position in the line we return a carriage return and line feed and stop recursing.
Conclusion
I created these to be legible and easily understood. Working through them helped me understand a lot about the inner workings of Erlang and hopefully they'll do the same for you. A full-on project page will be coming shortly, but for now you can download the package here.