A simple echo server with Miou.

In this short tutorial, we'll look at how to implement a simple "echo" server. The idea is to manage incoming connections and "repeat" what the user has written. It is equivalent to this server with "netcat":

$ mknod fifo p
$ cat fifo | nc -l 9000 > fifo

We're going to use Miou_unix, which is an extension of Miou with some functions available via the Unix module. These functions are blocking and if you're wondering why we need a layer to implement these functions, we suggest you take a look at the tutorial on sleepers.

A simple sequential server.

Let's start at the beginning: create a function to manage our customers and another function to manage the acceptance of connections.

let handler fd =
  let buf = Bytes.create 0x100 in
  let rec go () =
    let len = Miou_unix.read fd buf ~off:0 ~len:(Bytes.length buf) in
    if len > 0 then begin
      Miou_unix.write fd (Bytes.unsafe_to_string buf) ~off:0 ~len;
      go ()
    end else Miou_unix.close fd in
  go

let listen sockaddr =
  let fd = Miou_unix.tcpv4 () in
  Miou_unix.bind_and_listen fd sockaddr; fd

let server sockaddr =
  let rec go fd =
    let fd', sockaddr = Miou_unix.accept fd in
    handler fd' ();
    go fd in
  go (listen sockaddr)

let addr = Unix.ADDR_INET (Unix.inet_addr_loopback, 9000)
let () = Miou_unix.run @@ fun () -> server addr)

This small program provides a systematic structure for implementing a server:

  1. a loop that accepts TCP/IP connections
  2. a handler which manages a connection

Concurrency.

The main problem with this implementation is that you can only manage one connection, so it can terminate and then wait for and receive a new connection. In other words, our server only manages one client.

The way to manage several clients is to create asynchronous tasks. The aim of asynchronous tasks is to run concurrently without one blocking the others. If a task appears to be blocking, it will be suspended and the scheduler will try to execute another one.

In our case, our handler is blocking on the Miou_unix.read operation. So we're going to give Miou the ability to suspend our handler in order to check whether Miou_unix.accept is also waiting or whether a connection has just arrived.

To specify an asynchronous task with Miou, we use Miou.call_cc:

let server sockaddr =
  let rec go prms fd =
    let fd', sockaddr = Miou_unix.accept fd in
    let prm = Miou.call_cc (handler fd') in
    go (prm :: prms) fd in
  go [] (listen sockaddr)

Using Miou.call_cc returns a "promise". It's a kind of witness for our task that lets us know the status of it:

It is possible to manipulate this promise and wait for our task to finish:

let server sockaddr =
  let rec go fd =
    let fd', sockaddr = Miou_unix.accept fd in
    let prm = Miou.call_cc (handler fd') in
    ignore (Miou.await_exn prm);
    go fd in
  go (listen sockaddr)

The problem with the code above is that we're back to the same behaviour we started with. Namely, waiting for our handler to finish before managing a new connection (and therefore only managing one client at a time).

Background tasks and Miou.

Miou enforce good practice and one of them is to always take care of these promises. In this case, we'd like to both keep our promise and launch others concurrently.

One possibility available in other schedulers is to detach a task. Promises allow you to keep a link with a task (since they are a witness to tasks). But if we can "forget" a promise and therefore let a task run without it being linked, we could solve our initial problem.

The next problem with such an approach is that we also forget the resources associated with the task (particularly the Miou_unix.file_descrs), and these resources should be freed (Miou_unix.close) up in any case (specially the abnormal case when we trigger an exception). So you end up having to manage the termination of a task (and the release of resources) while at the same time trying to forget about the task. But initially, the simple fact of wanting to detach a task is a leakage of resources.

Miou proposes another mechanism, which is to keep our promises somewhere and 'clean up' the ones that have finished. So we're going to introduce the use of an Miou.orphans value.

let rec clean_up orphans = match Miou.care orphans with
  | Some (Some prm) -> Miou.await_exn prm; clean_up orphans
  | Some None -> ()
  | None -> ()

let server sockaddr =
  let rec go orphans fd =
    clean_up orphans;
    let fd', sockaddr = Miou_unix.accept fd in
    let _ = Miou.call_cc ~orphans (handler fd') in
    go orphans fd in
  go (Miou.orphans ()) (listen sockaddr)

The orphans value will aggregate the promises (which are the children of the server task) which then become orphans.

The advantage then resides in the Miou.care function, which will return an orphan ready to be waited for. In this case, using Miou.await on this orphan will not block. The orphans are cleaned up. This operation is repeated each time a TCP/IP connection is received. In this way, we avoid detaching our tasks and we can take a real interest in how our tasks ended (in our case, we ignore the result).

It can be annoying to have to manage promises systematically (and not be able to forget them). However, apart from the fact that it is illegal to forget one's children, detachment involves checks on the part of the developer which, if forgotten, can lead to a memory leak. Indeed, even if you could forget a task, the scheduler doesn't!

From our experience and from using and implementing large software packages, this is perhaps one of the anti-patterns that we have found most frequently and that still causes problems.

Parallelism.

One of the big advantages of Miou is that it is easy to consider the parallelisation of a task. In this case, Miou.call_cc can easily be replaced by Miou.call.

let server sockaddr =
  let rec go orphans fd =
    clean_up orphans;
    let fd', sockaddr = Miou_unix.accept fd in
    let _ = Miou.call_cc ~orphans (handler fd') in
    go orphans fd in
  go (Miou.orphans ()) (listen sockaddr)

However, there is a bottleneck. In this case, our dom0 is the only one to manage the server task. In other words: a single domain manages the reception of connections.

Once again, Miou is interested in the development of system and network applications where the availability of the application to receive events from the system is essential. We could imagine that instead of having a single domain that manages the reception of connections, we could have several?

let server sockaddr =
  let rec go orphans fd =
    clean_up orphans;
    let fd', sockaddr = Miou_unix.accept fd in
    let _ = Miou.call_cc ~orphans (handler fd') in
    go orphans fd in
  go (Miou.orphans ()) (listen sockaddr)

let addr = Unix.ADDR_INET (Unix.inet_addr_loopback, 9000)

let () = Miou.run @@ fun () ->
  let prm = Miou.call_cc @@ fun () -> server addr in
  Miou.parallel server
    (List.init (Miou.Domain.count ()) (Fun.const addr))
  |> List.iter (function Ok () -> () | Error exn -> raise exn);
  Miou.await_exn prm

So we've gone from 1 domain handling all the connections to several domains implementing a parallel echo server!

Ownership.

Miou has a mechanism for 'attaching' resources to a task. The aim of this mechanism is to bind the use of a resource to a function - a bit like Rust, but in a dynamic way. Operations such as Miou_unix.Ownership.read or Miou_unix.Ownership.write will check that the function has the given file-descriptor - if not, a Not_owner exception is raised.

let server sockaddr =
  let rec go orphans fd =
    clean_up orphans;
    let fd', _sockaddr = Miou_unix.Ownership.accept fd in
    let _ =
      Miou.call
        ~give:[ Miou_unix.Ownership.resource fd' ]
        ~orphans (handler fd')
    in
    go orphans fd
  in
  go (Miou.orphans ()) (listen sockaddr)

In our case, Miou_unix.Ownership.accept creates the file-descriptor, so it belongs to our go function. We therefore need to pass the ownership to our task handler via the give argument.

Ownership is mainly used to "finalise" a resource in an abnormal case. As a developer, we have a duty to release all our resources (even in abnormal cases). So, if our handler function raises an exception, Miou will take care of closing our associated file-descriptor.

let handler fd () =
  let rec go buf =
    match Miou_unix.Ownership.read fd buf 0 (Bytes.length buf) with
    | 0 -> Miou_unix.Ownership.close fd
    | len ->
        let str = Bytes.unsafe_to_string buf in
        Miou_unix.Ownership.write fd str 0 len;
        go buf
  in
  go (Bytes.create 0x100)

Conclusion.

Developing a system and network application with Miou addresses several points:

This tutorial shows what Miou is finally proposing. However, there are a few caveats:

  1. The extension of Miou to other system events is explained in more detail in our tutorial on sleepers.
  2. We have not attempted to implement a mechanism that stops our servers. We could do it with a signal handler and Miou.Condition.t but we let the user choose the best way according to their expectations.

We therefore recommend that you read these tutorials to learn more about Miou's design, its reasons and its implications. At the very least, we hope that this tutorial lets you imagine the possibility of implementing your service using our library.