SE 765 -
Distributed Software Development CS
610 - Introduction to Parallel and Distributed Computing |
Erlang 5 – TCP/UDP Sockets |
This
lecture uses materials and has been adapted from:
http://www.erlang.org/doc/pdf/otp-system-documentation.pdf
http://learnyousomeerlang.com/buckets-of-sockets
Basic
operations with UDP:
1. setting up a socket
2. sending messages
3. receiving messages
4. closing a connection.
First operation: open
a socket.
·
Using
gen_udp:open/1-2.
·
Done
by calling {ok, Socket} = gen_udp:open(PortNumber).
·
Port
number : any integer between 1 and 65535.
o
System ports
:
0 to 102.
§
Most
of the time, OS makes it impossible to listen to a system port unless you have
administrative rights.
o
Registered ports : 1024 through 49151.
§
Usually
require no permissions and are free to use, although some of them are registered to well known services.
o
Dynamic or private ports:
remaining ports.
§
Frequently
used for ephemeral ports.
§
For
tests here, port numbers that are somewhat safe, such as 8789, are unlikely to be
taken.
gen_udp:open/2 : second argument can be a list of
options, specifying:
o
in
what type we want to receive data (list or binary),
o
how we want them received:
as messages ({active,
true})
or as results of a function call ({active, false}).
More
options:
o
whether
the socket should be set with IPv4 (inet4) or IPv6 (inet6),
o
whether
the UDP socket can be used to broadcast information ({broadcast, true | false}),
o
size of buffers, etc..
Opening a socket: First start a given Erlang shell:
1> {ok, Socket} = gen_udp:open(8789, [binary, {active,true}]). {ok,#Port<0.676>} 2> gen_udp:open(8789,
[binary, {active,true}]). {error,eaddrinuse} |
o First command, open the socket, order it to return me
binary data, and be active.
o A new data structure is returned: #Port<0.676>.
o This is the representation of the socket just opened.
o Second function call tries to open the same socket over
again, which is impossible.
o A {error, eaddrinuse} is
returned.
Start a second Erlang shell
and open a second UDP socket, with a different port number:
1> {ok, Socket} = gen_udp:open(8790). {ok,#Port<0.587>} 2> gen_udp:send(Socket,
{127,0,0,1}, 8789, "hey there!"). ok |
Second call, gen_udp:send/4 is used to send messages.
o Arguments are: gen_udp:send(OwnSocket, RemoteAddress, RemotePort,
Message).
o The RemoteAddress can
be either:
§ a string or an atom containing a domain name
("example.org"),
§ 4-tuple describing an IPv4 address
§ 8-tuple describing an IPv6 address.
o The receiver's port number (in what mailbox are we going
to drop our slip of paper?),
o The message, which can be a string, a binary, or an IO
list.
Did the message ever get sent? Go back to your first
shell and try to flush the data:
3> flush(). Shell got {udp,#Port<0.676>,{127,0,0,1},8790,<<"hey
there!">>} ok |
o
The
process that opened the socket will receive messages of the form: {udp, Socket, FromIp, FromPort, Message}.
What about passive mode? Close the
socket from the first shell and open a new one:
4> gen_udp:close(Socket). ok 5> f(Socket). ok 6> {ok, Socket} = gen_udp:open(8789, [binary, {active,false}]). {ok,#Port<0.683>} |
The
socket is closed, the Socket variable unbounded, then rebound as it is
open again in passive mode.
Before sending a
message back, try the following:
7> gen_udp:recv(Socket, 0). |
o
The
shell should be stuck.
o
The
function here is recv/2.
o
This
function is used to poll a passive socket for messages.
o
0 is the length of the
message.
o
If
a message is never sent, recv/2 will never return.
Go back to the second
shell and send a new message:
3> gen_udp:send(Socket, {127,0,0,1}, 8789,
"hey there!"). ok |
The
first shell should have printed {ok,{{127,0,0,1},8790,<<"hey
there!">>}} as the return value.
What if you don't
want to wait forever? Just add a time out value:
8> gen_udp:recv(Socket, 0, 2000). {error,timeout} |
While
TCP sockets share a large part of their interface with UDP sockets, there are some
vital differences in how they work. The biggest one is that clients and servers
are two entirely different things.
A
client will behave with the
following operations:
A
server will rather follow this
scheme:
Weird looking, huh? The client acts a bit like what we had
with gen_udp: you connect to a port, send and
receive, stop doing so. When serving, however, we have one new mode there:
listening. That's because of how TCP works to set sessions up.
Open a new shell and
start a listen socket with gen_tcp:listen(Port, Options):
1> {ok,
ListenSocket} =
gen_tcp:listen(8091, [{active,true}, binary]). {ok,#Port<0.661>} |
The
listen socket is just in charge of waiting for connection requests.
Once
the listen socket is open, any process (and more than one) can take the listen
socket and fall into an accept state,
locked up until some client asks to talk with it:
2> {ok, AcceptSocket} = gen_tcp:accept(ListenSocket). |
And
then the process is locked.
Open a second shell:
1> {ok, Socket} = gen_tcp:connect({127,0,0,1}, 8091, [binary, {active,true}]). {ok,#Port<0.596>} |
Look
back at the first shell, it should have returned with {ok, SocketNumber}.
From
that point on, the accept socket and the client socket can communicate on a
one-on-one basis, similarly to gen_udp.
Use the second shell
and send messages to the first:
3> gen_tcp:send(Socket, "Hey there first
shell!"). ok |
And from the first
shell:
7> flush(). Shell got {tcp,#Port<0.729>,<<"Hey
there first shell!">>} ok |
Both
sockets can send messages in the same way, and can then be closed with gen_tcp:close(Socket).
Note
that closing an accept socket will close that socket alone, and closing a
listen socket will close all of the related accept sockets.
Echo server
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. |
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.
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.
Exercise: write a client
that talks to the echo server.