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
.
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:
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.async
:
let server sockaddr =
let rec go prms fd =
let fd', sockaddr = Miou_unix.accept fd in
let prm = Miou.async (handler fd') in
go (prm :: prms) fd in
go [] (listen sockaddr)
Using Miou.async
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.async (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).
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_descr
s), 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.async ~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.
One of the big advantages of Miou is that it is easy to consider the parallelisation of a task. In this case, Miou.async
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.async ~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.async ~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.async @@ 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!
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)
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:
sleepers
.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.