Skip to content

Commit

Permalink
Cookbook Networking
Browse files Browse the repository at this point in the history
  • Loading branch information
Cuihtlauac ALVARADO committed May 13, 2024
1 parent 2f496c7 commit 6b1eae9
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 0 deletions.
6 changes: 6 additions & 0 deletions data/cookbook/tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ categories:
- title: Generate OCaml Bindings for a C Library
slug: generate-ocaml-bindings-c-library
C library, using the OCaml foreign function interface.
- title: Networking
tasks:
- title: TCP Client
slug: tcp-client
- title: TCP Server
slug: tcp-server
- title: Compression
tasks:
- title: Read a gzip compressed text file
Expand Down
54 changes: 54 additions & 0 deletions data/cookbook/tcp-client/00-lwt.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
packages:
- name: lwt
tested_version: "5.7.0"
used_libraries:
- lwt
- lwt.unix
- name: logs
tested_version: "0.7.0"
used_libraries:
- logs
discussion: |
- **Understanding TCP client:** Implementing a TCP client needs to initialise a socket file descriptor that will be used to both connect to the remote host and also to exchange with it.
- **Alternative Libraries:** Other concurrent libraries can be used (`Async`, `Eio`). The `Unix` library can also be used and will be simpler to use (no monadic functions or operator), especially if the protocol is a plain alternance of question/answer. If the protocol needs some concurrency, an adequate library should be used.
---

(* Defines some constants about the remote host. The `( let* )` operator permits the chaining of multiple Lwt statements. *)
let (let*) = Lwt.bind

let connect_host = "localhost"
let connect_service = "smtp"
(* We setup some `Logs` options. Afterwards `Lwt_main.run` creates a Lwt context. and schedules the following functions. *)
let () =
Logs.set_reporter (Logs.format_reporter ());
Logs.set_level (Some Logs.Info);
Lwt_main.run @@
(* We are looking for host and service names. Hostnames are typically resolved with the `/etc/host` and DNS, while service names are typically resolved with `/etc/services`. Service names are bound to port numbers. (Note: `gethostbyname` and `getservbyname` raise an exception if the host or service is not found). *)
let* host_entry = Lwt_unix.gethostbyname connect_host in
if Array.length host_entry.h_addr_list = 0 then
Logs_lwt.err (fun m -> m "No addresses not found")
else
let* service_entry = Lwt_unix.getservbyname connect_service "tcp" in
let rec handle_connection ic oc =
(* With host and service entries, we build a socket address that can be used to connect a distant host. Note: between the socket creation and its usage by `connect`, it is possible to set some options (`setsockopt`, `bind`). *)
let socket_fd = Lwt_unix.(socket PF_INET SOCK_STREAM 0) in
let* () = Lwt_unix.connect socket_fd
(Unix.ADDR_INET(host_entry.h_addr_list.(0),
service_entry.s_port)) in
let* () = Logs_lwt.info (fun m -> m "Connected") in
(* When we are connected, we can convert the socket into a pair of channels and use the available functions that deal with them. *)
let* line = Lwt_io.read_line_opt ic in
match line with
| None ->
Logs_lwt.info (fun m -> m ("Connection closed"))
| Some line' ->
let* () = Logs_lwt.info (fun m -> m "Received:%s" line') in
let* () = Lwt_io.write_line oc "EHLO localhost" in
let* line = Lwt_io.read_line_opt ic in
match line with
| None ->
Logs_lwt.info (fun m -> m ("Connection closed"))
| Some line' ->
let* () = Logs_lwt.info (fun m -> m "Received: %s" line') in
Lwt.return ()
57 changes: 57 additions & 0 deletions data/cookbook/tcp-server/00-lwt.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
packages:
- name: lwt
tested_version: "5.7.0"
used_libraries:
- lwt
- lwt.unix
- name: logs
tested_version: "0.7.0"
used_libraries:
- logs
discussion: |
- **Understanding TCP server:** Implementing a TCP server needs to initialise a main socket file descriptor that will be used to accept connections. Each connection is associated to a dedicated file descriptor, which is used to create input and output channels. Since the server has to handle multiple connections concurrently, we have to use the Lwt scheduling and have multiple concurrent promises. The I/O functions provided by `Lwt_unix` must be used instead of blocking functions from other libraries.
- **Alternative Libraries:** Other concurrent libraries can be used (`Async`, `Eio`). The `Unix` library can also be used with the `fork` function that creates a new process.
- **Credit:** The program is heavily inspired by [this article](https://medium.com/@aryangodara_19887/tcp-server-and-client-in-ocaml-13ebefd54f60)
---
(* Defines some constants. The `listen_address` is typically `Unix.inet_addr_loopback`, `Unix.inet_addr_any`. Other values may be used to listen only on one network interface. The `(let*)` operator permits the chaining of multiple Lwt statements. *)
let (let*) = Lwt.bind

let listen_address = Unix.inet_addr_loopback
let port = 9000
let backlog = 10
(* This `loop` function loop forever a given Lwt promise. *)
let rec loop f =
let* () = f () in
loop f
(* This defines a function that will handle the connection with a single client. `ic` and `oc` are input and output channels that can be used with `Lwt_io` functions. *)
let rec handle_connection ic oc =
let* () = Lwt_io.write_line oc "Give me your name:" in
let* line = Lwt_io.read_line_opt ic in
match line with
| Some line' ->
let* () = Lwt_io.write_line oc ("Hello, " ^ line') in
handle_connection ic oc
| None ->
Logs_lwt.info (fun m -> m "Connection closed")
(* This defines a function that "accepts" a new connection and runs `handle_connection` on it. `Lwt.on_failure` returns immediately and executes this function in parallel with the other tasks. *)
let accept_connection socket_fd =
let* conn = Lwt_unix.accept socket_fd in
let fd, _client_addr = conn in
let ic = Lwt_io.of_fd ~mode:Lwt_io.Input fd in
let oc = Lwt_io.of_fd ~mode:Lwt_io.Output fd in
Lwt.on_failure
(handle_connection ic oc)
(fun exc -> Logs.err
(fun m -> m "%s" (Printexc.to_string exc) ));
Logs_lwt.info (fun m -> m "New connection")
(* The main function initialises the socket that will be used to accept clients and loop forever through the `accept_connection` function. *)
let () =
Logs.set_reporter (Logs.format_reporter ());
Logs.set_level (Some Logs.Info);
Lwt_main.run @@
let socket_fd = Lwt_unix.(socket PF_INET SOCK_STREAM 0) in
let* () = Lwt_unix.bind socket_fd (ADDR_INET(listen_address, port)) in
Lwt_unix.listen socket_fd backlog;
loop (fun () ->
accept_connection socket_fd)

0 comments on commit 6b1eae9

Please sign in to comment.