Sterling has too many projects Blogging about programming, microcontrollers & electronics, 3D printing, and whatever else...

Asynchronous Socket

| 567 words | 3 minutes | raku advent-2019
Cables plugged into a power bar

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.

The content of this site is licensed under Attribution 4.0 International (CC BY 4.0).