Programming With Sockets In Erlang
A socket is a communication endpoint that allows machines to communicate over the Internet using the Internet Pro- tocol (IP).
There are two main libraries for programming with sockets: gen_tcp
and gen_upd
.
Using TCP
Fetching Data from a Server
-module(download_page).
-export([get_url/1]).
get_url(Host) ->
{ok, Socket} = gen_tcp:connect(Host, 80, [binary, {packet, 0}]),
ok = gen_tcp:send(Socket, "GET / HTTP/1.0\r\n\r\n"),
receive_data(Socket, []).
receive_data(Socket, SoFar) ->
receive
{tcp, Socket, Bin} ->
receive_data(Socket, [Bin|SoFar]);
{tcp_closed, Socket} ->
list_to_binary(lists:reverse(SoFar))
end.
Test:
1> B = download_page:get_url("mitnk.com").
<<"HTTP/1.1 301 Moved Permanently\r\nServer: nginx/1.0.12\r\nDate: Sat, 19 May 2012 10:56:06 GMT\r\nContent-Type: text/html\r\n"...>>
2> io:format("~p~n", [B]).
<<"HTTP/1.1 301 Moved Permanently\r\nServer: nginx/1.0.12\r\nDate: Sat, 19 May 2012 10:56:06 GMT\r\nContent-Type: text/html\r\nContent-Length: 185\r\nConnection: close\r\nLocation: http://mitnk.com/\r\n\r\n<html>\r\n<head><title>301 Moved Permanently</title></head>\r\n<body bgcolor=\"white\">\r\n<center><h1>301 Moved Permanently</h1></center>\r\n<hr><center>nginx/1.0.12</center>\r\n</body>\r\n</html>\r\n">>
ok
3> string:tokens(binary_to_list(B), "\r\n").
["HTTP/1.1 301 Moved Permanently","Server: nginx/1.0.12",
"Date: Sat, 19 May 2012 10:56:06 GMT",
"Content-Type: text/html","Content-Length: 185",
"Connection: close","Location: http://mitnk.com/","<html>",
"<head><title>301 Moved Permanently</title></head>",
"<body bgcolor=\"white\">",
"<center><h1>301 Moved Permanently</h1></center>",
"<hr><center>nginx/1.0.12</center>","</body>","</html>"]
You might think that it would be better to write the code to accumulate the fragments like this:
receive_data(Socket, SoFar) ->
receive
{tcp,Socket,Bin} ->
receive_data(Socket, list_to_binary([SoFar,Bin]));
{tcp_closed,Socket} ->
SoFar
end.
This code is correct but less efficient than the original version. The reason is that in the latter version we are continually appending a new binary to the end of the buffer, which involves a lot of copying of data. It’s much better to accumulate all the fragments in a list (which will end up in the wrong order) and then reverse the entire list and concatenate all the fragments in one operation.
A Simple TCP Server
-module(nano_server).
-export([start/0]).
start() ->
{ok, Listen} = gen_tcp:listen(2345,
[binary, {packet, 0},
{reuseaddr, true},
{active, true}]),
{ok, Socket} = gen_tcp:accept(Listen),
gen_tcp:close(Listen),
loop(Socket).
loop(Socket) ->
receive
{tcp, Socket, <<"bye\r\n">>} ->
gen_tcp:send(Socket, <<"bye\r\n">>),
gen_tcp:close(Socket),
ok;
{tcp, Socket, Bin} ->
io:format("Server received binary = ~p~n", [Bin]),
Prefix = <<"you said: ">>,
Reply = <<Prefix/binary, Bin/binary>>,
gen_tcp:send(Socket, Reply),
loop(Socket);
{tcp_closed, Socket} ->
io:format("client closed~n");
X ->
io:format("Got Unexpected Data: ~p ~n", [X])
end.
This is the simplest of servers that illustrates how to package and encode the application data. It accepts a request, computes a reply, sends the reply, and terminates.
P.S. Use telnet
to test this server.
Client in Erlang:
-module(nano_client).
-export([connect/1]).
connect(Str) ->
{ok, Socket} = gen_tcp:connect("localhost", 2345, [binary, {packet, 0}]),
ok = gen_tcp:send(Socket, Str),
receive
{tcp, Socket, Bin} ->
io:format("Client received binary = ~p~n", [Bin]),
gen_tcp:close(Socket)
end.
Run server first, then use client to test it:
1> nano_client:connect("Hello from nano client").
A Parallel Server
-module(nano_server).
-export([start/0]).
start() ->
{ok, Listen} = gen_tcp:listen(2345,
[binary, {packet, 4},
{reuseaddr, true},
{active, true}]),
spawn(fun() -> par_connect(Listen) end).
par_connect(Listen) ->
{ok, Socket} = gen_tcp:accept(Listen),
spawn(fun() -> par_connect(Listen) end),
loop(Socket).
loop(Socket) ->
receive
{tcp, Socket, Bin} ->
io:format("Server received binary = ~p~n", [Bin]),
Str = binary_to_term(Bin),
io:format("server (unpacked) ~p~n", [Str]),
Reply = {ok, "I got it.", Str},
io:format("Server replying = ~p~n", [Reply]),
gen_tcp:send(Socket, term_to_binary(Reply)),
loop(Socket);
{tcp_close, Socket} ->
io:format("Server socket closed~n")
end.
After we have accepted a connection, it’s a good idea to explicitly set the required socket options, like this:
{ok, Socket} = gen_tcp:accept(Listen),
inet:setopts(Socket, [{packet,4},binary,
{nodelay,true},{active, true}]),
loop(Socket)
Error Handling with Sockets
Error handling with sockets is extremely easy—basically you don’t have to do anything. As we said earlier, each socket has a controlling process (that is, the process that created the socket). If the controlling process dies, then the socket will be automatically closed.
This means that if we have, for example, a client and a server and the server dies because of a programming error, the socket owned by the server will be automatically closed, and the client will be sent a {tcp_closed, Socket} message.
UDP
Writing a UDP client and server in Erlang is much easier than writ- ing in the TCP case since we don’t have to worry about maintaining connections to the server.
-module(udp_server).
-export([start/0]).
start() ->
{ok, Socket} = gen_udp:open(2345, [binary]),
loop(Socket).
loop(Socket) ->
receive
{udp, Socket, Host, Port, Bin} ->
N = binary_to_term(Bin),
Fac = fac(N),
gen_udp:send(Socket, Host, Port, term_to_binary(Fac)),
loop(Socket)
end.
fac(0) -> 1;
fac(N) -> N * fac(N - 1).
Client side:
-module(udp_client).
-export([conn/1]).
conn(Request) ->
{ok, Socket} = gen_udp:open(0, [binary]),
ok = gen_udp:send(Socket, "localhost", 2345, term_to_binary(Request)),
Value = receive
{udp, Socket, _, _, Bin} ->
{ok, binary_to_term(Bin)}
after 2000 ->
error
end,
gen_udp:close(Socket),
Value.
Additional Notes on UDP
A UDP packet can be delivered twice (which surprises some people), so you have to be careful writing code for remote procedure calls. It might happen that the reply to a second query was in fact a duplicated answer to the first query. To avoid this, we could modify the client code to include a unique reference and check that this reference is returned by the server.
client(Request) ->
{ok, Socket} = gen_udp:open(0, [binary]),
Ref = make_ref(), %% make a unique reference
B1 = term_to_binary({Ref, Request}),
ok = gen_udp:send(Socket, "localhost", 4000, B1),
wait_for_ref(Socket, Ref).
wait_for_ref(Socket, Ref) ->
receive
{udp, Socket, _, _, Bin} ->
case binary_to_term(Bin) of
{Ref, Val} ->
%% got the correct value
Val;
{_SomeOtherRef, _} ->
%% some other value throw it away
wait_for_ref(Socket, Ref)
end;
after 1000 ->
...
end.
Broadcasting to Multiple Machines
For completeness, I’ll show you how to set up a broadcast channel. This code is rather rare, but one day you might need it.
-module(broadcast).
-compile(export_all).
send(IoList) ->
case inet:ifget("eth0", [broadaddr]) of
{ok, [{broadaddr, Ip}]} ->
{ok, S} = gen_udp:open(5010, [{broadcast, true}]),
gen_udp:send(S, Ip, 6000, IoList),
gen_udp:close(S);
_ ->
io:format("Bad interface name, or\n"
"broadcasting not supported\n")
end.
listen() ->
{ok, S} = gen_udp:open(6000),
loop(S).
loop(S) ->
receive
Any ->
io:format("received:~p~n", [Any]),
loop(S)
end.
Digging Deeper
We have looked at only the most commonly used functions for manipulating sockets. You can find more information about the socket APIs in the manual pages for gen_tcp, gen_udp, and inet.