Networking

The overall flow of network programming in libuv (and Luv) is very similar to the BSD socket interface, but all functions take callbacks, and many inconveniences are addressed.

Networking is handled by Luv.TCP, Luv.UDP, and Luv.DNS.

Note

The code examples in this chapter exist to demonstrate certain Luv APIs. They are not always robust. For instance, they don’t always close connections properly, and they don’t handle all errors gracefully.

TCP

TCP is a connection-oriented stream protocol. TCP handles are therefore a kind of libuv stream. All this means is that a Luv.TCP.t is also a Luv.Stream.t, which, in turn, is also a Luv.Handle.t. So, functions from all three modules can be used with Luv.TCP.t. In particular, you will typically use…

Server

A server socket is typically…

  1. Created with Luv.TCP.init.

  2. Assigned an address with Luv.TCP.bind.

  3. Made to listen on the assigned address with Luv.Stream.listen. This will call its callback each time there is a client connection ready to be accepted.

  4. Told to accept a connection with Luv.Stream.accept.

After that, one has an ordinary TCP connection. Luv.Stream.read_start and Luv.Stream.write are used to communicate on it, and Luv.Handle.close is used to close it.

Here is a simple echo server that puts all this together:

tcp_echo_server.ml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
let () =
  let address = Luv.Sockaddr.ipv4 "127.0.0.1" 7000 |> Result.get_ok in
  let server = Luv.TCP.init () |> Result.get_ok in
  ignore (Luv.TCP.bind server address);

  Luv.Stream.listen server begin function
    | Error e ->
      Printf.eprintf "Listen error: %s\n" (Luv.Error.strerror e)
    | Ok () ->
      let client = Luv.TCP.init () |> Result.get_ok in

      match Luv.Stream.accept ~server ~client with
      | Error _ ->
        Luv.Handle.close client ignore
      | Ok () ->

        Luv.Stream.read_start client begin function
          | Error `EOF ->
            Luv.Handle.close client ignore
          | Error e ->
            Printf.eprintf "Read error: %s\n" (Luv.Error.strerror e);
            Luv.Handle.close client ignore
          | Ok buffer ->
            Luv.Stream.write client [buffer] (fun _ -> ignore)
        end
  end;

  ignore (Luv.Loop.run () : bool)

The most interesting thing to note is that Luv.Stream.read_start doesn’t perform only one read, but reads in a loop until either Luv.Stream.read_stop is called, or the socket is closed.

Client

Client programming is very straightforward: use Luv.TCP.connect to establish a connection. Then, use Luv.Stream.write and Luv.Stream.read_start to communicate.

The example below sends “Hello, world!” to the echo server, waits for the response, and prints it:

tcp_hello_world.ml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let () =
  let address = Luv.Sockaddr.ipv4 "127.0.0.1" 7000 |> Result.get_ok in
  let client = Luv.TCP.init () |> Result.get_ok in

  Luv.TCP.connect client address begin function
    | Error e ->
      Printf.eprintf "Connect error: %s\n" (Luv.Error.strerror e)
    | Ok () ->
      let message = Luv.Buffer.from_string "Hello, world!" in
      Luv.Stream.write client [message] (fun _result _bytes_written ->
        Luv.Stream.shutdown client ignore);

      Luv.Stream.read_start client begin function
        | Error `EOF ->
          Luv.Handle.close client ignore
        | Error e ->
          Printf.eprintf "Read error: %s\n" (Luv.Error.strerror e);
          Luv.Handle.close client ignore
        | Ok response ->
          print_endline (Luv.Buffer.to_string response)
      end
  end;

  ignore (Luv.Loop.run () : bool)

You can run the above server and this client together with these commands:

dune exec example/tcp_echo_server.exe &
dune exec example/tcp_hello_world.exe
killall tcp_echo_server.exe

In the DNS section below, we will expand this example to look up a web server and perform an HTTP GET request.

UDP

UDP is a connectionless protocol. Rather than behaving as a stream, a UDP socket allows sending and receiving discrete datagrams. So, in Luv (as in libuv), a Luv.UDP.t is not a Luv.Stream.t. It does, however, have similar functions Luv.UDP.send and Luv.UDP.recv_start. A Luv.UDP.t is still a Luv.Handle.t, however, so a UDP socket is closed with Luv.Handle.close.

Let’s write UDP versions of the echo server and the “Hello, world!” program:

udp_echo_server.ml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
let () =
  let address = Luv.Sockaddr.ipv4 "127.0.0.1" 7000 |> Result.get_ok in
  let server = Luv.UDP.init () |> Result.get_ok in
  ignore (Luv.UDP.bind server address);

  Luv.UDP.recv_start server begin function
    | Error e ->
      Printf.eprintf "Receive error: %s\n" (Luv.Error.strerror e)
    | Ok (_, None, _) ->
      ()
    | Ok (buffer, Some client_address, _flags) ->
      Luv.UDP.send server [buffer] client_address ignore
  end;

  ignore (Luv.Loop.run () : bool)

udp_hello_world.ml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
let () =
  let address = Luv.Sockaddr.ipv4 "127.0.0.1" 7000 |> Result.get_ok in
  let client = Luv.UDP.init () |> Result.get_ok in

  let message = Luv.Buffer.from_string "Hello, world!" in
  Luv.UDP.send client [message] address ignore;

  Luv.UDP.recv_start client begin function
    | Error e ->
      Printf.eprintf "Receive error: %s\n" (Luv.Error.strerror e);
      Luv.Handle.close client ignore
    | Ok (response, _server_address, _flags) ->
      print_endline (Luv.Buffer.to_string response);
      Luv.Handle.close client ignore
  end;

  ignore (Luv.Loop.run () : bool)

You can run the client and the server together with:

dune exec example/udp_echo_server.exe &
dune exec example/udp_hello_world.exe
killall udp_echo_server.exe

The two most immediate differences, compared to the TCP functions, are:

  1. There are no notions of listening or connecting.

  2. Since there is no connection, when data is received by Luv.UDP.recv_start, the sending peer’s address is provided to the callback together with the data.

Apart from ordinary peer-to-peer communication, UDP supports multicast. See Luv.UDP for multicast and other helper functions.

Querying DNS

libuv provides asynchronous DNS resolution with Luv.DNS.getaddrinfo. Let’s implement a clone of the host command:

host.ml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let () =
  Luv.DNS.getaddrinfo ~family:`INET ~node:Sys.argv.(1) () begin function
    | Error e ->
      Printf.eprintf "Cannot resolve host: %s\n" (Luv.Error.strerror e)
    | Ok addr_infos ->
      print_endline
        (Option.get (Luv.Sockaddr.to_string (List.hd addr_infos).addr))
  end;

  ignore (Luv.Loop.run () : bool)

Luv.DNS.getaddrinfo has a large number of optional arguments for fine-tuning the lookup. These correspond to the fields and flags documented at getaddrinfo(3p).

libuv also provides reverse lookup, which is exposed as Luv.DNS.getnameinfo.

Example: HTTP GET

We can use Luv.TCP and Luv.DNS to run fairly complete HTTP GET requests:

http_get.ml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
let () =
  let host = Sys.argv.(1) in
  let path = Sys.argv.(2) in

  Luv.DNS.getaddrinfo ~family:`INET ~node:host ~service:"80" () begin function
    | Error e ->
      Printf.eprintf "Cannot resolve host: %s\n" (Luv.Error.strerror e)
    | Ok addr_infos ->
      let address = (List.hd addr_infos).addr in

      let socket = Luv.TCP.init () |> Result.get_ok in
      Luv.TCP.connect socket address begin function
        | Error e ->
          Printf.eprintf "Cannot connect: %s\n" (Luv.Error.strerror e)
        | Ok () ->

          let request =
            Printf.ksprintf
              Luv.Buffer.from_string "GET %s HTTP/1.1\r\n\r\n" path
          in
          Luv.Stream.write socket [request] (fun _ _ ->
            Luv.Stream.shutdown socket ignore);

          Luv.Stream.read_start socket begin function
            | Error `EOF ->
              Luv.Handle.close socket ignore
            | Error e ->
              Printf.eprintf "Read error: %s\n" (Luv.Error.strerror e);
              Luv.Handle.close socket ignore
            | Ok response ->
              print_endline (Luv.Buffer.to_string response)
          end
      end
  end;

  ignore (Luv.Loop.run () : bool)

You can try this in the Luv repo with the command:

dune exec example/http_get.exe -- google.com /

Network interfaces

Information about the system’s network interfaces can be obtained by calling Luv.Network.interface_addresses. This simple program prints out all the interface details made available by libuv:

ifconfig.ml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let () =
  Luv.Network.interface_addresses ()
  |> Result.get_ok
  |> List.iter begin fun interface ->
    let open Luv.Network.Interface_address in

    print_endline interface.name;

    if interface.is_internal then
      print_endline " Internal";

    Printf.printf " Physical: %02x:%02x:%02x:%02x:%02x:%02x\n"
      (Char.code interface.physical.[0])
      (Char.code interface.physical.[1])
      (Char.code interface.physical.[2])
      (Char.code interface.physical.[3])
      (Char.code interface.physical.[4])
      (Char.code interface.physical.[5]);

    Printf.printf " Address:  %s\n"
      (Option.get (Luv.Sockaddr.to_string interface.address));

    Printf.printf " Netmask:  %s\n"
      (Option.get (Luv.Sockaddr.to_string interface.netmask))
  end

is_internal is true for loopback interfaces. Note that if an interface has multiple addresses (for example, an IPv4 address and an IPv6 address), the interface will be reported multiple times, once with each address.