Simple multi-client server in OCaml with Lwt

Lwt is a popular cooperative multitasking library for OCaml.

There are a number of Lwt tutorials and examples around, but for some reason I couldn't find any that would explain how to write a server that serves multiple connections concurrently (as opposed to sending a reply and terminating the connection, like HTTP servers do).

While it seems obvious once you figured it out, it took me a while to figure it out, so I wrote a simple example and a walkthrough.

The server from the example allows clients to increment a shared counter and read its value.

Download the file:

Code walkthrough

We start with opening the Lwt module so we have easy access to bind, return, and other essential functions.

Then we do some magic to enable logging up to the INFO level.

open Lwt

let () = Lwt_log.add_rule "*" Lwt_log.Info

The server will keep a shared mutable counter. Clients will be able to read or increment it.

let counter = ref 0

We'll make the server listen on

let listen_address = Unix.inet_addr_loopback
let port = 9000
let backlog = 10

We accepts two commands from clients: read, that returns the current value of the counter; and inc, that increments it and sends a confirmation. Any other command will trigger Unknown command reply.

All context switches occur between Lwt binds, so we don't need to care about locking the counter explicitly.

let handle_message msg =
    match msg with
    | "read" -> string_of_int !counter
    | "inc"  -> counter := !counter + 1; "Counter has been incremented"
    | _      -> "Unknown command"

Threads that serve client connections need to read from the socket, process commands, and then read again. We'll make it a tail-recursive loop, essentially read >>= process >>= write >>= read.

We need to take care of closed connections though. Reading from a closed connection will raise an End_of_file exception. We could handle it, or we can use the Lwt_io.read_line_opt : input_channel -> string option Lwt.t function that returns None rather than raising an exception if the channel is closed.

Then we can terminate the thread if it returns None, and proceed otherwise.

let rec handle_connection ic oc () =
    Lwt_io.read_line_opt ic >>=
    (fun msg ->
        match msg with
        | Some msg ->
            let reply = handle_message msg in
            Lwt_io.write_line oc reply >>= handle_connection ic oc
        | None -> "Connection closed" >>= return)

When a connection comes in, we need to start a handle_connection thread. We don't care about its result, but we should do something about errors that may occur, so we start it with Lwt.on_failure function that also takes a callback that is executed when exception occurs. We'll just make it log the exceptions.

let accept_connection conn =
    let fd, _ = conn in
    let ic = Lwt_io.of_fd Lwt_io.Input fd in
    let oc = Lwt_io.of_fd Lwt_io.Output fd in
    Lwt.on_failure (handle_connection ic oc ()) (fun e -> Lwt_log.ign_error (Printexc.to_string e)); "New connection" >>= return

We also need a thread that accepts connections in a loop. We'll store the socket in a closure. When a client connects, it will pass the file descriptor to the accept_connection function will start a thread that serves that client.

let create_server sock =
    let rec serve () =
        Lwt_unix.accept sock >>= accept_connection >>= serve
    in serve

Socket setup is straightforward.

let create_socket () =
    let open Lwt_unix in
    let sock = socket PF_INET SOCK_STREAM 0 in
    bind sock @@ ADDR_INET(listen_address, port);
    listen sock backlog;

Now we have everything we need. All we need to start working is to start the server thread with

let () =
    let sock = create_socket () in
    let serve = create_server sock in @@ serve ()

Now we can build it with ocamlfind ocamlopt -package lwt,lwt.unix -linkpkg -o counter-server ./, start the executable, and start a few telnet sessions to to test it.

Example of a telnet session:

$ telnet localhost 9000
Trying                                                                                                                                                                                                Connected to localhost.                                                                                                                                                                                            
Escape character is '^]'.
Counter has been incremented
This page was last modified: 2015 Aug 13