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

 1let () =
 2  let address = Luv.Sockaddr.ipv4 "127.0.0.1" 7000 |> Result.get_ok in
 3  let server = Luv.TCP.init () |> Result.get_ok in
 4  ignore (Luv.TCP.bind server address);
 5
 6  Luv.Stream.listen server begin function
 7    | Error e ->
 8      Printf.eprintf "Listen error: %s\n" (Luv.Error.strerror e)
 9    | Ok () ->
10      let client = Luv.TCP.init () |> Result.get_ok in
11
12      match Luv.Stream.accept ~server ~client with
13      | Error _ ->
14        Luv.Handle.close client ignore
15      | Ok () ->
16
17        Luv.Stream.read_start client begin function
18          | Error `EOF ->
19            Luv.Handle.close client ignore
20          | Error e ->
21            Printf.eprintf "Read error: %s\n" (Luv.Error.strerror e);
22            Luv.Handle.close client ignore
23          | Ok buffer ->
24            Luv.Stream.write client [buffer] (fun _ -> ignore)
25        end
26  end;
27
28  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

 1let () =
 2  let address = Luv.Sockaddr.ipv4 "127.0.0.1" 7000 |> Result.get_ok in
 3  let client = Luv.TCP.init () |> Result.get_ok in
 4
 5  Luv.TCP.connect client address begin function
 6    | Error e ->
 7      Printf.eprintf "Connect error: %s\n" (Luv.Error.strerror e)
 8    | Ok () ->
 9      let message = Luv.Buffer.from_string "Hello, world!" in
10      Luv.Stream.write client [message] (fun _result _bytes_written ->
11        Luv.Stream.shutdown client ignore);
12
13      Luv.Stream.read_start client begin function
14        | Error `EOF ->
15          Luv.Handle.close client ignore
16        | Error e ->
17          Printf.eprintf "Read error: %s\n" (Luv.Error.strerror e);
18          Luv.Handle.close client ignore
19        | Ok response ->
20          print_endline (Luv.Buffer.to_string response)
21      end
22  end;
23
24  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

 1let () =
 2  let address = Luv.Sockaddr.ipv4 "127.0.0.1" 7000 |> Result.get_ok in
 3  let server = Luv.UDP.init () |> Result.get_ok in
 4  ignore (Luv.UDP.bind server address);
 5
 6  Luv.UDP.recv_start server begin function
 7    | Error e ->
 8      Printf.eprintf "Receive error: %s\n" (Luv.Error.strerror e)
 9    | Ok (_, None, _) ->
10      ()
11    | Ok (buffer, Some client_address, _flags) ->
12      Luv.UDP.send server [buffer] client_address ignore
13  end;
14
15  ignore (Luv.Loop.run () : bool)

udp_hello_world.ml

 1let () =
 2  let address = Luv.Sockaddr.ipv4 "127.0.0.1" 7000 |> Result.get_ok in
 3  let client = Luv.UDP.init () |> Result.get_ok in
 4
 5  let message = Luv.Buffer.from_string "Hello, world!" in
 6  Luv.UDP.send client [message] address ignore;
 7
 8  Luv.UDP.recv_start client begin function
 9    | Error e ->
10      Printf.eprintf "Receive error: %s\n" (Luv.Error.strerror e);
11      Luv.Handle.close client ignore
12    | Ok (response, _server_address, _flags) ->
13      print_endline (Luv.Buffer.to_string response);
14      Luv.Handle.close client ignore
15  end;
16
17  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

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

 1let () =
 2  let host = Sys.argv.(1) in
 3  let path = Sys.argv.(2) in
 4
 5  Luv.DNS.getaddrinfo ~family:`INET ~node:host ~service:"80" () begin function
 6    | Error e ->
 7      Printf.eprintf "Cannot resolve host: %s\n" (Luv.Error.strerror e)
 8    | Ok addr_infos ->
 9      let address = (List.hd addr_infos).addr in
10
11      let socket = Luv.TCP.init () |> Result.get_ok in
12      Luv.TCP.connect socket address begin function
13        | Error e ->
14          Printf.eprintf "Cannot connect: %s\n" (Luv.Error.strerror e)
15        | Ok () ->
16
17          let request =
18            Printf.ksprintf
19              Luv.Buffer.from_string "GET %s HTTP/1.1\r\n\r\n" path
20          in
21          Luv.Stream.write socket [request] (fun _ _ ->
22            Luv.Stream.shutdown socket ignore);
23
24          Luv.Stream.read_start socket begin function
25            | Error `EOF ->
26              Luv.Handle.close socket ignore
27            | Error e ->
28              Printf.eprintf "Read error: %s\n" (Luv.Error.strerror e);
29              Luv.Handle.close socket ignore
30            | Ok response ->
31              print_endline (Luv.Buffer.to_string response)
32          end
33      end
34  end;
35
36  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

 1let () =
 2  Luv.Network.interface_addresses ()
 3  |> Result.get_ok
 4  |> List.iter begin fun interface ->
 5    let open Luv.Network.Interface_address in
 6
 7    print_endline interface.name;
 8
 9    if interface.is_internal then
10      print_endline " Internal";
11
12    Printf.printf " Physical: %02x:%02x:%02x:%02x:%02x:%02x\n"
13      (Char.code interface.physical.[0])
14      (Char.code interface.physical.[1])
15      (Char.code interface.physical.[2])
16      (Char.code interface.physical.[3])
17      (Char.code interface.physical.[4])
18      (Char.code interface.physical.[5]);
19
20    Printf.printf " Address:  %s\n"
21      (Option.get (Luv.Sockaddr.to_string interface.address));
22
23    Printf.printf " Netmask:  %s\n"
24      (Option.get (Luv.Sockaddr.to_string interface.netmask))
25  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.