UDP Client and Server in OCaml using LWT

Aryan Godara
7 min readJul 19, 2023

--

In this tutorial, I’ll show you how to write a UDP Client and a UDP Server in OCaml using socket programming concepts. So let’s hop in.

OCaml: Functional Programming Language

This technical article explores socket programming in OCaml to build a UDP server and a UDP client. Leveraging the LWT library, we construct robust network applications capable of handling asynchronous I/O operations with ease. UDP’s connectionless and lightweight nature makes it ideal for low-latency scenarios. We’ll cover socket creation, addressing, and binding for robust network communication. LWT, a powerful tool for concurrent programming, allows us to write non-blocking code for handling multiple tasks simultaneously.

Unlike TCP, UDP does not establish a connection with the server; the client simply sends a datagram. Similarly, the server only waits for incoming datagrams and does not accept a connection. Datagrams already contain the sender’s address, allowing the server to relay data to the correct client. Both TCP and UDP are fundamental communication protocols used for data transport between clients and servers. For this work, we will utilize the Ubuntu operating system, which provides a flexible environment for developing UDP client-server programs.

Implementation of UDP Client-Server Program:

UDP Server:

1. Create a UDP socket.
2. Bind the socket to the server’s address.
3. Wait for the arrival of the client’s datagram packet.
4. Process the received datagram packet and formulate a response.
5. Send the response back to the client.
6. Return to Step 3 to continue awaiting new datagram packets.

UDP Client:

1. Construct a UDP socket.
2. Send a message to the server.
3. Wait for a response from the server.
4. Receive and process the response, and if needed, return to Step 2.
5. Terminate the socket descriptor when the communication is complete.

The Code :-

Your project directory will look like this. {{WORKDIR}} is the project root.

{{WORKDIR}}/

udpClient/
_build/
bin/
dune
client.ml
main.ml
...

udpServer/
_build/
bin/
dune
server.ml
main.ml
...

{{WORKDIR}}/udpClient/bin/client.ml

{{WORKDIR}}/udpClient/bin/main.ml

{{WORKDIR}}/udpServer/bin/main.ml

{{WORKDIR}}/udpServer/bin/server.ml

Running the Code :-

# Open two terminal windows
# 1. Inside {{WORKDIR}}/udpServer/
# 2. Inside {{WORKDIR}}/udpClient/


# 1. Inside the first terminal (for the Server):-
$ dune build # Compiles code into a binary
$ ./_build/default/bin/main.exe # Launches the Server


# 2. Inside the first terminal (for the Client):-
$ dune build # Compiles code into a binary
$ ./_build/default/bin/main.exe # Launches the Client


# Messages that can be sent from the client :-
# "read" : Prints the current value of the counter.
# "inc" : Increments the value of the counter by 1.
# "_" (anything else) : Server returns "Unknown Command" response.

The Server responds accordingly to the response received by the Client.

LWT (Light Weight Threads) :-

When developing software, handling various I/O operations becomes a common task. Interacting with the kernel through system calls, reading input from users via keyboards or mice, communicating with graphical servers to build user interfaces, and connecting with other computers over networks are among the numerous resources software may need to interact with.

As this list of interactions grows, managing them all together can become increasingly challenging. Two common approaches have been proposed to address this problem:

1. Using a main loop and integrating all components into it: While this approach can work, it can make writing asynchronous sequential code very complex. For instance, graphical user interfaces may freeze and fail to redraw themselves due to blocking code portions.

2. Using preemptive system threads: Although this approach is possible, handling threads correctly can be difficult. Additionally, system threads consume considerable resources, limiting the number of threads that can be launched simultaneously. Hence, this is not a general solution.

Lwt presents a third alternative by introducing promises, which are highly efficient. A promise is essentially a reference that is filled asynchronously. Calling a function that returns a promise does not require creating a new stack, or process, or incurring any significant overhead. It behaves like a regular and fast function call. Promises can be composed elegantly, allowing developers to write highly asynchronous programs.

In the following sections, we will delve into the concepts of Lwt and explore its main modules, providing insights into how Lwt can simplify and enhance the handling of asynchronous operations in software development.

We use two Lwt operators/functions in this program :-

The >>= (bind) operator in the context of Lwt is used to chain together asynchronous computations represented as promises (Lwt monads). It allows you to sequence multiple asynchronous operations, ensuring that subsequent computations depend on the results of the previous ones.

Signature: 'a Lwt.t -> ('a -> 'b Lwt.t) -> 'b Lwt.t

  • The >>= operator takes two arguments:
  1. An asynchronous computation represented as a promise of type 'a Lwt.t.
  2. A function that takes the result of the first computation and returns a new asynchronous computation of type 'b Lwt.t.
  • The result of the >>= operator is a new promise representing the subsequent computation, which depends on the result of the first computation.

In the context of Lwt, Lwt.return is a function that wraps an immediate value into a promise (Lwt monad). It allows you to lift a regular value into the asynchronous world, making it compatible with Lwt-based computations.

Signature: 'a -> 'a Lwt.t

  • The Lwt.return function takes a value of type 'a and returns a promise ('a Lwt.t) representing that value.

These help us write concurrently working code.

Special Section:

You can even run both client and server from a single binary :-

This time all three .ml files are in the same directory :-

{{WORKDIR}} /
client.ml
server.ml
main.ml
dune

This way, you can now enter the client input in the same terminal window, and the server will also respond in the same terminal window. We do this using “Lwt.pick”

We use two more Lwt constructs here :-

In the context of Lwt, Lwt_main.run is a function that runs the Lwt event loop until the given Lwt promise is fulfilled. It is the entry point for starting and executing Lwt-based asynchronous computations.

Signature: 'a Lwt.t -> 'a

  • The Lwt_main.run function takes a promise of type 'a Lwt.t and executes the Lwt event loop until the promise is fulfilled, returning the final value of type 'a.

In the context of Lwt, Lwt.pick is a function that allows you to choose the first completed promise from a list of Lwt promises. It returns a new promise that resolves with the result of the first completed promise.

Signature: 'a Lwt.t list -> 'a Lwt.t

  • The Lwt.pick function takes a list of promises ('a Lwt.t list) and returns a new promise ('a Lwt.t) that resolves with the result of the first completed promise.

Now, you might wonder why one might want to use a UDP server over a TCP server, so here are the advantages and disadvantages of both approaches :-

The differences between TCP and UDP :-

UDP and TCP are two widely used Internet protocols for data transfer. TCP is a connection-oriented transport layer protocol ensuring secure and reliable data transmission between connected devices. It establishes a connection before data transfer and provides features like flow control and error control. TCP is utilized by protocols like HTTP and FTP, offering dependable data transmission over networks.

On the other hand, UDP is a connectionless transport layer protocol, allowing rapid data transmission without the overhead of connection setup and maintenance. It is commonly used for real-time data transfer scenarios where transmission delays are unacceptable. UDP’s fast transmission speed makes it suitable for protocols like DNS, DHCP, and RIP.

UDP:

- Connectionless transport layer protocol.
- Rapid data transmission without connection setup and maintenance overhead.
- Used for real-time data transfer when transmission delays are not acceptable.
- Fast transmission speed.
- Commonly used in protocols like DNS, DHCP, and RIP.
- Pros: Low startup delay, broadcasting and multicasting support, ability to manage packet borders, quick transactions like DNS lookup, bandwidth use is high.
- Cons: Packet may not reach its intended recipient or may reach twice, lacks congestion and flow control, routers can be irresponsible in case of collision, and packet loss may occur.

TCP:

  • Connection-oriented transport layer protocol.
    - Ensures secure and reliable data transmission between connected devices.
    - Establishes a connection before data transfer.
    - Provides features like flow control and error control.
    - Used by protocols like HTTP and FTP for dependable data transmission.
    - Pros: Support for multiple routing protocols, operating system independence, excellent scalability in client-server architectures.
    - Cons: Lacks support for broadcast or multicast requests, header size may lead to resource wastage, no assurance of packet delivery provided by the transport layer.

This brings us to the end of this article. Please leave a like if you found this helpful, and leave a comment to tell me how I can improve. All reviews are appreciated :)

--

--

Aryan Godara
Aryan Godara

Written by Aryan Godara

Hi, I'm a student at IIT D, India. My passions include all things tech-related, pc gaming cuz Indian parents don't like gaming consoles :( and playing guitar :3

Responses (1)