Asynchronous Socket
What’s more asynchronous than socket communication? When two programs need to talk to
each other, often from different computers on different networks in different
parts of the world, you can connect using a socket. Whether an HTTP server or
some custom protocol, you can implement both sides of that communication using
IO::Socket::Async
.
Let’s consider a simple calculator service. It listens for connections over TCP.
When a connection is established, it takes lines of input over the connection
and parses each line as a simple mathematic calculation like 2 + 2
or 6 * 7
.
We can write the server like this.
react {
whenever IO::Socket::Async.listen('127.0.0.1', 3456) -> $conn {
whenever $conn.Supply.lines -> $line {
if $line ~~ m:s/$<a> = [ \d+ ] $<op> = [ '+' | '-' | '*' | '/' ] $<b> = [ \d+ ]/ {
my $a = +$<a>;
my $b = +$<b>;
my $r = do given "$<op>".trim {
when '+' { $a + $b }
when '-' { $a - $b }
when '*' { $a * $b }
when '/' { $a div $b }
default { "Unknown Error"; }
}
$conn.print("$r\n");
}
else {
$conn.print("Syntax Error\n");
}
}
}
}
Now, the nested whenever
block might look a little odd, but this is just fine.
You can add more whenever
blocks within a react
any time you need to this
way.
The outer whenever
listens for new new connection objects. It’s sole job here
is to register the connection as another whenever
block with the server. Be
aware that using this strategy does mean that you are handling all connections
asynchronously as if from a single thread. A more scalable solution might be to
use a start block for each arriving connection (which might look something like
this):
start react whenever $conn.Supply.lines -> $line { ... }
Moving on, the inner whenever
then watches for lines of input from each
connection as it arrives. It will receive a message whenever a line of input
has been sent by the client. This code parses that line, performs the
expression (or discovers an error), and returns the result.
Simple.
We call listen
to establish a listening TCP socket on the named address and
port number. This returns a Supply
which
will emit connected IO::Socket::Async
objects. You can use the Supply
method
on the connection object to get text (or bytes via the :bin
option), whenever
the are sent from the associated client. You use the write
and print
methods
to send bytes and text back.
The client can also be written with IO::Socket::Async
as well. Here is a
client which uses our expression server to calculate the Fibonacci sequence:
my ($a, $b) = (0, 1);
say "$a";
say "$b";
await IO::Socket::Async.connect('127.0.0.1', 3456).then: -> $connection {
given $connection.result -> $conn {
$conn.print("$a + $b\n");
react {
whenever $conn.Supply.lines -> $line {
$a = $b;
$b = +$line;
say "$b";
$conn.print("$a + $b\n");
}
}
}
}
When making a client conneciton, we use the connect
method with IP address or
host name of the server to connect to. That method returns a
Promise
which is kept once the
connection has been made. The result of that promise is a connected
IO::Socket::Async
object which can be used in precisely the same way as the
server, with Supply
returning text or bytes and write
and print
being used
to send text or bytes.
Cheers.