How2Lab Logo
tech guide & how tos..


Project 2: Beyond TCP – Fast & Furious Communication with UDP (A Time Sync Example)


So far, our journey into socket programming has focused exclusively on TCP (Transmission Control Protocol). TCP, with its reliability, ordered delivery, and connection-oriented nature, is fantastic for applications like web Browse, file transfers, and our chat application. However, it comes with overhead due to its guarantees.

But what if you need speed more than absolute reliability? What if small data loss is acceptable, or you want to send small, independent packets without the handshake overhead? Enter UDP (User Datagram Protocol).

In this article, we will explore UDP's unique characteristics and implement a practical application: a simple time synchronization server and client.


1. Introduction to UDP (Deeper Dive)

let us reiterate the core characteristics of UDP and discuss scenarios where It is the protocol of choice:

  • Connectionless: Unlike TCP, UDP does not establish a persistent connection. Each message (datagram) is an independent unit of data sent from one point to another, much like sending a postcard. THere is no handshake, no session management.

  • Unreliable: UDP offers no guarantees that a datagram will reach its destination, or that if multiple datagrams are sent, they will arrive in the order they were sent. THere is no automatic retransmission for lost packets.

  • Faster / Lower Overhead: Because it lacks the mechanisms for reliability, ordering, flow control, and congestion control, UDP has significantly less overhead than TCP. This means faster data transfer and less resource consumption.

  • Datagram-Oriented: When you send data via UDP, It is sent as distinct packets (datagrams). If you send two 100-byte UDP packets, the receiver will get two distinct 100-byte packets. With TCP (stream-oriented), these might arrive as a single 200-byte chunk.

When to Choose UDP vs. TCP:

  • Choose UDP for:

    • DNS (Domain Name System): Quick, single request-response queries where a small delay or re-query is acceptable.

    • VoIP (Voice over IP) / Video Streaming: Real-time applications where a slight loss of audio/video frames is less noticeable (or preferable) than significant delay caused by retransmissions.

    • Online Gaming (Fast-Paced): Latency is critical. Small, frequent updates (like player positions) can be sent via UDP. If a packet is lost, a new update will likely arrive shortly, making retransmission unnecessary.

    • Network Management (SNMP): Simple query-response models.

    • Discovery Services: Broadcasting messages to find other devices on a local network.

  • Choose TCP for:

    • Web Browse (HTTP/HTTPS): You need to ensure all parts of a webpage arrive correctly and in order.

    • File Transfer (FTP): Data integrity is paramount; you cannot afford lost bytes in a file.

    • Email (SMTP/POP3/IMAP): Emails must be delivered reliably and completely.

    • Secure Shell (SSH): Remote terminal access requires reliable, ordered communication.


2. Project Goal: Time Synchronization Application

Our goal is to build a simple application where:

  • A UDP Server runs, listens for requests, and when it receives one, sends back the current system time.

  • A UDP Client sends a small request to the server and prints the time it receives back.

This demonstrates UDP's simple request-response pattern and its connectionless nature, where the server doesn't need to establish a dedicated link for each client.


3. UDP Server Implementation

The UDP server will primarily use `socket()`, `bind()`, `recvfrom()`, and `sendto()`. Noticeably absent are `listen()` and `accept()` because tHere is no "connection" to establish.

  • socket() with `SOCK_DGRAM`: We create a datagram socket.

  • bind(): The server still needs to `bind()` to a specific local IP address and port so clients know where to send their requests.

  • recvfrom(): This function is used to receive data from any client. Crucially, it also populates a `sockaddr` structure with the sender's address and port, allowing the server to know where to send the response.

    
    ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
    struct sockaddr *src_addr, socklen_t *addrlen);
    		
    The `src_addr` and `addrlen` arguments are where the sender's address information is returned.
  • sendto(): This function sends data to a *specific* destination address, identified by a `sockaddr` structure.

    
    ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
    const struct sockaddr *dest_addr, socklen_t addrlen);
    		
    You will use the `src_addr` obtained from `recvfrom()` as the `dest_addr` for `sendto()`.
  • Retrieving Current Time: We will use standard C library functions like `time()` (from ) to get the current time as a `time_t` value, and `ctime()` (also from ) to convert it into a human-readable string.


4. UDP Client Implementation

The UDP client is also straightforward:

  • socket() with `SOCK_DGRAM`: Similar to the server, it creates a datagram socket.

  • No `connect()`: For simple UDP, clients typically do not call `connect()`. Instead, they directly use `sendto()` to send a datagram to the server's known address. This is the essence of connectionless communication. (Note: `connect()` can be used with UDP sockets to "lock" the socket to a single destination, meaning subsequent `send()` calls don't need the address, but It is less common for basic request/response).

  • sendto(): Sends the request (e.g., an empty string or a simple command like "GET_TIME") to the server's pre-configured IP and port.

  • recvfrom(): Waits for and receives the time response from the server. It will also capture the server's address, though for a simple client that only talks to one server, this might not be strictly necessary for sending, but useful for verification.


5. Key Differences from TCP (Code-Level)

As You will see in the code, the most significant differences are:

  • No `listen()` or `accept()`: These functions are entirely absent in UDP. UDP sockets are always ready to send or receive datagrams.

  • `sendto()` and `recvfrom()`: These replace `send()` and `recv()` (though `send()` and `recv()` can be used on UDP sockets if `connect()` was used, but that defeats some of the flexibility of UDP). They include explicit arguments for the source/destination address for each datagram.

  • Datagram-Oriented: You send a complete message at once, and It is received as a complete message. There is no concept of a continuous "stream" of bytes.

  • No Automatic Guarantees: You won't see code for retransmissions or acknowledgements. If a datagram is lost, It is up to the application layer to detect and handle it if reliability is desired.


6. Full, Commented Code for UDP Server and Client

Here are the complete code listings for our UDP time server and client.

udp_time_server.c


#include 
#include 
#include 
#include  // For time() and ctime()

// --- Platform-specific includes and definitions ---
#ifdef _WIN32
    #include 
    #include  // For inet_ntop
    #pragma comment(lib, "ws2_32.lib")
    #define CLOSE_SOCKET closesocket
    #define GET_LAST_ERROR WSAGetLastError
    // Define ssize_t for Windows compatibility if not already defined
    #ifndef _SSIZE_T_DEFINED
    typedef SSIZE_T ssize_t;
    #define _SSIZE_T_DEFINED
    #endif
#else
    #include 
    #include 
    #include 
    #include  // For close
    #include   // For errno
    #define CLOSE_SOCKET close
    #define GET_LAST_ERROR() errno
#endif
// ----------------------------------------------------

#define PORT 8888 // Choose a different port for UDP
#define BUFFER_SIZE 1024

int main() {
    // --- Windows-specific Winsock initialization ---
    #ifdef _WIN32
        WSADATA wsaData;
        if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
            fprintf(stderr, "WSAStartup failed with error: %d\n", GET_LAST_ERROR());
            return 1;
        }
    #endif
    // ----------------------------------------------------

    int sockfd;
    struct sockaddr_in servaddr, cliaddr;
    char buffer[BUFFER_SIZE];
    time_t current_time;
    char time_str[26]; // ctime returns 25 chars + null terminator
    int n;
    socklen_t len;

    // 1. Create UDP socket
    // AF_INET: IPv4 Internet protocols
    // SOCK_DGRAM: Datagram-oriented socket (UDP)
    // 0: Default protocol
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) ==
        #ifdef _WIN32
        INVALID_SOCKET
        #else
        -1
        #endif
        ) {
        fprintf(stderr, "socket creation failed with error: %d\n", GET_LAST_ERROR());
        #ifdef _WIN32
        WSACleanup();
        #endif
        exit(EXIT_FAILURE);
    }

    // Set server address structure
    memset(&servaddr, 0, sizeof(servaddr)); // Clear the structure
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY; // Listen on all available interfaces
    servaddr.sin_port = htons(PORT);       // Convert port to network byte order

    // 2. Bind the socket to the specified IP and port
    // This makes the server listen for UDP datagrams on this address/port.
    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) ==
        #ifdef _WIN32
        SOCKET_ERROR
        #else
        -1
        #endif
        ) {
        fprintf(stderr, "bind failed with error: %d\n", GET_LAST_ERROR());
        CLOSE_SOCKET(sockfd);
        #ifdef _WIN32
        WSACleanup();
        #endif
        exit(EXIT_FAILURE);
    }

    printf("UDP Time Server listening on port %d...\n", PORT);

    // Server main loop: continuously receive requests and send responses
    while (1) {
        len = sizeof(cliaddr); // Must be initialized before recvfrom call
        memset(buffer, 0, BUFFER_SIZE); // Clear buffer for incoming data

        // 3. Receive datagram from a client
        // recvfrom stores the sender's address in cliaddr
        n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE,
                     0, (struct sockaddr *)&cliaddr, &len);

        if (n ==
            #ifdef _WIN32
            SOCKET_ERROR
            #else
            -1
            #endif
            ) {
            fprintf(stderr, "recvfrom failed with error: %d\n", GET_LAST_ERROR());
            continue; // Keep server running for next request
        }
        buffer[n] = '\0'; // Null-terminate the received data

        char client_ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &cliaddr.sin_addr, client_ip, INET_ADDRSTRLEN);
        printf("Received request '%s' from client: %s:%d\n",
               buffer, client_ip, ntohs(cliaddr.sin_port));

        // Get current time
        current_time = time(NULL);
        // ctime returns a string with a newline at the end; remove it for cleaner output
        strncpy(time_str, ctime(¤t_time), sizeof(time_str) - 1);
        time_str[sizeof(time_str) - 1] = '\0'; // Ensure null termination
        time_str[strcspn(time_str, "\n")] = 0; // Remove newline

        printf("Sending time: %s\n", time_str);

        // 4. Send the time back to the requesting client
        // sendto uses the address obtained from recvfrom to send the response.
        sendto(sockfd, (const char *)time_str, strlen(time_str),
               0, (const struct sockaddr *)&cliaddr, len);
    }

    // Close socket (this part won't be reached in the infinite loop)
    CLOSE_SOCKET(sockfd);
    printf("Server socket closed.\n");

    // --- Windows-specific Winsock cleanup ---
    #ifdef _WIN32
        WSACleanup();
    #endif
    // -----------------------------------------

    return 0;
}

udp_time_client.c


#include 
#include 
#include 

// --- Platform-specific includes and definitions ---
#ifdef _WIN32
    #include 
    #include  // For inet_pton
    #pragma comment(lib, "ws2_32.lib")
    #define CLOSE_SOCKET closesocket
    #define GET_LAST_ERROR WSAGetLastError
    // Define ssize_t for Windows compatibility if not already defined
    #ifndef _SSIZE_T_DEFINED
    typedef SSIZE_T ssize_t;
    #define _SSIZE_T_DEFINED
    #endif
#else
    #include 
    #include 
    #include 
    #include  // For close
    #include   // For errno
    #define CLOSE_SOCKET close
    #define GET_LAST_ERROR() errno
#endif
// ----------------------------------------------------

#define PORT 8888
#define SERVER_IP "127.0.0.1" // Server's IP address
#define BUFFER_SIZE 1024

int main() {
    // --- Windows-specific Winsock initialization ---
    #ifdef _WIN32
        WSADATA wsaData;
        if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
            fprintf(stderr, "WSAStartup failed with error: %d\n", GET_LAST_ERROR());
            return 1;
        }
    #endif
    // ----------------------------------------------------

    int sockfd;
    struct sockaddr_in servaddr;
    char buffer[BUFFER_SIZE];
    const char *request_message = "GET_TIME";
    socklen_t len;
    int n;

    // 1. Create UDP socket
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) ==
        #ifdef _WIN32
        INVALID_SOCKET
        #else
        -1
        #endif
        ) {
        fprintf(stderr, "socket creation failed with error: %d\n", GET_LAST_ERROR());
        #ifdef _WIN32
        WSACleanup();
        #endif
        exit(EXIT_FAILURE);
    }

    // Set server address structure for sending requests
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);

    // Convert IP address from text to binary form
    if (inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr) <= 0) {
        fprintf(stderr, "Invalid address/ Address not supported\n");
        CLOSE_SOCKET(sockfd);
        #ifdef _WIN32
        WSACleanup();
        #endif
        exit(EXIT_FAILURE);
    }

    printf("Sending request '%s' to server %s:%d\n", request_message, SERVER_IP, PORT);

    // 2. Send request to the server
    // No connect() needed for UDP unless you want to fix the remote address
    sendto(sockfd, (const char *)request_message, strlen(request_message),
           0, (const struct sockaddr *)&servaddr, sizeof(servaddr));

    // 3. Receive response from the server
    // It is good practice to pass a cliaddr (source address) even if not strictly needed
    // so you can verify who sent the response.
    len = sizeof(servaddr); // reuse servaddr for receiving source address, or declare new one
    memset(buffer, 0, BUFFER_SIZE);
    n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE,
                 0, (struct sockaddr *)&servaddr, &len);

    if (n ==
        #ifdef _WIN32
        SOCKET_ERROR
        #else
        -1
        #endif
        ) {
        fprintf(stderr, "recvfrom failed with error: %d\n", GET_LAST_ERROR());
    } else {
        buffer[n] = '\0'; // Null-terminate the received data
        char server_ip_recv[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &servaddr.sin_addr, server_ip_recv, INET_ADDRSTRLEN);
        printf("Received time from server %s:%d: %s\n", server_ip_recv, ntohs(servaddr.sin_port), buffer);
    }

    // Close socket
    CLOSE_SOCKET(sockfd);
    printf("Client socket closed.\n");

    // --- Windows-specific Winsock cleanup ---
    #ifdef _WIN32
        WSACleanup();
    #endif
    // -----------------------------------------

    return 0;
}

7. Testing and Demonstration

To run your UDP time application:

  1. Compile both programs:

    • Linux/WSL:

      
      gcc udp_time_server.c -o udp_time_server
      gcc udp_time_client.c -o udp_time_client
      				
    • Windows (MinGW-w64):

      
      gcc udp_time_server.c -o udp_time_server.exe -lws2_32
      gcc udp_time_client.c -o udp_time_client.exe -lws2_32
      				
  2. Open terminal windows:

    • In one terminal, run the server:

      
      # Linux/WSL
      ./udp_time_server
      
      # Windows
      udp_time_server.exe
      				
      You should see: `UDP Time Server listening on port 8888...`
    • In one or more separate terminals, run the client:

      
      # Linux/WSL
      ./udp_time_client
      
      # Windows
      udp_time_client.exe
      				
      You should see: `Sending request 'GET_TIME' to server 127.0.0.1:8888` followed by the received time.
  3. Observe the interaction:

    Notice how the UDP server, unlike the TCP server, does not block after handling one client. It immediately loops back to `recvfrom()` and is ready to serve the next client request without needing to `accept()` a new connection. You can run multiple client instances concurrently, and the server will respond to each one as their requests arrive.

    Potential Packet Loss: While testing on your local machine (`127.0.0.1`), packet loss is extremely rare. However, over a real network, UDP packets can be lost or arrive out of order. Our simple client doesn't handle this, but real-world UDP applications would implement their own retransmission or error recovery logic if needed (e.g., DNS re-queries if no response within a timeout).

    Example output (similar for both platforms):

    Server Terminal:

    
    UDP Time Server listening on port 8888...
    Received request 'GET_TIME' from client: 127.0.0.1:XXXXX
    Sending time: Wed Jun 18 12:31:40 2025
    Received request 'GET_TIME' from client: 127.0.0.1:YYYYY
    Sending time: Wed Jun 18 12:31:41 2025
    ... (continues to serve requests)
    		

    Client Terminal:

    
    Sending request 'GET_TIME' to server 127.0.0.1:8888
    Received time from server 127.0.0.1:8888: Wed Jun 18 12:31:40 2025
    Client socket closed.
    		

You have now successfully implemented a UDP client-server application and experienced the connectionless nature of UDP firsthand. This provides a crucial contrast to TCP and expands your understanding of network communication paradigms.

While UDP offers speed, our previous TCP chat server could only handle one client at a time. In the next article, we will tackle one of the most important concepts in server development: concurrency. You will learn how to design a TCP server that can gracefully handle multiple clients simultaneously, a critical skill for building any scalable network application.



Share:
Buy Domain & Hosting from a trusted company
Web Services Worldwide
About the Author
Rajeev Kumar
CEO, Computer Solutions
Jamshedpur, India

Rajeev Kumar is the primary author of How2Lab. He is a B.Tech. from IIT Kanpur with several years of experience in IT education and Software development. He has taught a wide spectrum of people including fresh young talents, students of premier engineering colleges & management institutes, and IT professionals.

Rajeev has founded Computer Solutions & Web Services Worldwide. He has hands-on experience of building variety of websites and business applications, that include - SaaS based erp & e-commerce systems, and cloud deployed operations management software for health-care, manufacturing and other industries.


Refer a friendSitemapDisclaimerPrivacy
Copyright © How2Lab.com. All rights reserved.